From 54ef23923164d3942153a0a946767dcd1bc40e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E8=96=84=E4=BA=91?= Date: Mon, 10 Aug 2026 13:26:56 +0800 Subject: [PATCH] feat: integrate trajectory tracking controller runtime --- ClumsyPilot/ClumsyPilot.csproj | 3 + .../Abstractions/ILateralController.cs | 19 + .../Abstractions/ILongitudinalController.cs | 19 + .../Abstractions/LateralControlCommand.cs | 76 +++ .../Abstractions/PathTrackingContext.cs | 126 ++++ .../Control/Allocation/GcpCommandAllocator.cs | 104 ++++ .../Control/Allocation/GcpMotionCommand.cs | 67 ++ ClumsyPilot/Control/Common/PidController.cs | 307 ++++++++++ .../Control/Execution/GcpCommandExecutor.cs | 177 ++++++ .../Execution/ParkingGeometricController.cs | 551 +++++++++++++++++ .../Lateral/StanleyLateralController.cs | 250 ++++++++ .../Longitudinal/PidLongitudinalController.cs | 224 +++++++ .../Movements/TrajectoryTrackingMovement.cs | 306 ++++++++++ .../Chassis/MultiWheelChassisAdapter.cs | 576 ++++++++++++++++++ ClumsyPilot/Shared/Mathematics/AngleMath.cs | 138 +++++ .../Shared/Mathematics/FrameTransform2D.cs | 165 +++++ .../Shared/Mathematics/InterpolationMath.cs | 61 ++ ClumsyPilot/Shared/Models/ChassisCommand.cs | 183 ++++++ .../DetourVehicleStateProvider.cs | 509 ++++++++++++++++ .../FirstOrderLowPassFilter.cs | 142 +++++ .../StateEstimation/IVehicleStateProvider.cs | 14 + ClumsyPilot/StateEstimation/VehicleState.cs | 138 +++++ .../StateEstimation/VelocityEstimator2D.cs | 275 +++++++++ ClumsyPilot/Trajectory/LegacyTrackAdapter.cs | 1 + ClumsyPilot/Trajectory/Trajectory2D.cs | 260 ++++++++ ClumsyPilot/Trajectory/TrajectoryPoint.cs | 102 ++++ .../Trajectory/TrajectoryProjection.cs | 123 ++++ ClumsyPilot/Trajectory/TrajectoryProjector.cs | 219 +++++++ ClumsyPilot/ref/CommonUsage.dll | Bin 134144 -> 144384 bytes 29 files changed, 5135 insertions(+) create mode 100644 ClumsyPilot/Control/Abstractions/ILateralController.cs create mode 100644 ClumsyPilot/Control/Abstractions/ILongitudinalController.cs create mode 100644 ClumsyPilot/Control/Abstractions/LateralControlCommand.cs create mode 100644 ClumsyPilot/Control/Abstractions/PathTrackingContext.cs create mode 100644 ClumsyPilot/Control/Allocation/GcpCommandAllocator.cs create mode 100644 ClumsyPilot/Control/Allocation/GcpMotionCommand.cs create mode 100644 ClumsyPilot/Control/Common/PidController.cs create mode 100644 ClumsyPilot/Control/Execution/GcpCommandExecutor.cs create mode 100644 ClumsyPilot/Control/Execution/ParkingGeometricController.cs create mode 100644 ClumsyPilot/Control/Lateral/StanleyLateralController.cs create mode 100644 ClumsyPilot/Control/Longitudinal/PidLongitudinalController.cs create mode 100644 ClumsyPilot/Movements/TrajectoryTrackingMovement.cs create mode 100644 ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs create mode 100644 ClumsyPilot/Shared/Mathematics/AngleMath.cs create mode 100644 ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs create mode 100644 ClumsyPilot/Shared/Mathematics/InterpolationMath.cs create mode 100644 ClumsyPilot/Shared/Models/ChassisCommand.cs create mode 100644 ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs create mode 100644 ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs create mode 100644 ClumsyPilot/StateEstimation/IVehicleStateProvider.cs create mode 100644 ClumsyPilot/StateEstimation/VehicleState.cs create mode 100644 ClumsyPilot/StateEstimation/VelocityEstimator2D.cs create mode 100644 ClumsyPilot/Trajectory/LegacyTrackAdapter.cs create mode 100644 ClumsyPilot/Trajectory/Trajectory2D.cs create mode 100644 ClumsyPilot/Trajectory/TrajectoryPoint.cs create mode 100644 ClumsyPilot/Trajectory/TrajectoryProjection.cs create mode 100644 ClumsyPilot/Trajectory/TrajectoryProjector.cs diff --git a/ClumsyPilot/ClumsyPilot.csproj b/ClumsyPilot/ClumsyPilot.csproj index cbff9fa..21e8698 100644 --- a/ClumsyPilot/ClumsyPilot.csproj +++ b/ClumsyPilot/ClumsyPilot.csproj @@ -23,6 +23,9 @@ + + + diff --git a/ClumsyPilot/Control/Abstractions/ILateralController.cs b/ClumsyPilot/Control/Abstractions/ILateralController.cs new file mode 100644 index 0000000..25fbcaf --- /dev/null +++ b/ClumsyPilot/Control/Abstractions/ILateralController.cs @@ -0,0 +1,19 @@ +namespace MultiWheelC.Control.Abstractions +{ + /// + /// 定义Stanley、LQR和MPC等车体中心横向控制器的统一接口。 + /// + public interface ILateralController + { + /// + /// 根据本周期车辆状态和轨迹误差计算车体中心目标曲率。 + /// + LateralControlCommand Compute( + PathTrackingContext context); + + /// + /// 清除控制器跨周期状态,以便开始新轨迹或异常恢复后重新运行。 + /// + void Reset(); + } +} diff --git a/ClumsyPilot/Control/Abstractions/ILongitudinalController.cs b/ClumsyPilot/Control/Abstractions/ILongitudinalController.cs new file mode 100644 index 0000000..810eda3 --- /dev/null +++ b/ClumsyPilot/Control/Abstractions/ILongitudinalController.cs @@ -0,0 +1,19 @@ +namespace MultiWheelC.Control.Abstractions +{ + /// + /// 定义根据参考速度和实际纵向速度生成底盘命令速度的统一接口。 + /// + public interface ILongitudinalController + { + /// + /// 根据本周期速度目标、速度反馈和时间间隔计算有符号底盘命令速度。 + /// + double ComputeSpeedMetersPerSecond( + PathTrackingContext context); + + /// + /// 清除积分、历史误差和其他跨周期状态,以便安全开始新的控制过程。 + /// + void Reset(); + } +} diff --git a/ClumsyPilot/Control/Abstractions/LateralControlCommand.cs b/ClumsyPilot/Control/Abstractions/LateralControlCommand.cs new file mode 100644 index 0000000..cb170c9 --- /dev/null +++ b/ClumsyPilot/Control/Abstractions/LateralControlCommand.cs @@ -0,0 +1,76 @@ +using System; + +namespace MultiWheelC.Control.Abstractions +{ + /// + /// 表示横向控制器生成的前、后GCP目标转角,单位为rad,逆时针为正。 + /// + public readonly struct LateralControlCommand + { + /// + /// 创建前、后GCP目标转角命令。 + /// + public LateralControlCommand( + double frontGcpAngleRadians, + double rearGcpAngleRadians) + { + EnsureFinite( + frontGcpAngleRadians, + nameof(frontGcpAngleRadians)); + EnsureFinite( + rearGcpAngleRadians, + nameof(rearGcpAngleRadians)); + + FrontGcpAngleRadians = + frontGcpAngleRadians; + RearGcpAngleRadians = + rearGcpAngleRadians; + } + + /// + /// 获取前GCP目标转角,单位为rad,逆时针为正。 + /// + public double FrontGcpAngleRadians { get; } + + /// + /// 获取后GCP目标转角,单位为rad,逆时针为正。 + /// + public double RearGcpAngleRadians { get; } + + /// + /// 获取前后GCP的共同转角分量,主要用于横向平移修正。 + /// + public double CommonAngleRadians => + (FrontGcpAngleRadians + + RearGcpAngleRadians) / 2.0; + + /// + /// 获取前后GCP的差动转角分量,主要用于曲率前馈和航向修正。 + /// + public double DifferentialAngleRadians => + (FrontGcpAngleRadians - + RearGcpAngleRadians) / 2.0; + + /// + /// 创建前后GCP均保持车头方向的直线命令。 + /// + public static LateralControlCommand Straight => + new LateralControlCommand(0.0, 0.0); + + /// + /// 检查GCP目标转角是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "GCP目标转角必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Abstractions/PathTrackingContext.cs b/ClumsyPilot/Control/Abstractions/PathTrackingContext.cs new file mode 100644 index 0000000..578468a --- /dev/null +++ b/ClumsyPilot/Control/Abstractions/PathTrackingContext.cs @@ -0,0 +1,126 @@ +using System; +using MultiWheelC.StateEstimation; +using MultiWheelC.Trajectory; + +namespace MultiWheelC.Control.Abstractions +{ + /// + /// 保存一次轨迹跟踪控制周期使用的车辆状态、轨迹投影和真实时间间隔。 + /// + public readonly struct PathTrackingContext + { + /// + /// 创建横向和纵向控制器共享的只读控制输入快照。 + /// + public PathTrackingContext( + VehicleState vehicleState, + TrajectoryProjection projection, + double referenceSpeedMetersPerSecond, + double deltaTimeSeconds) + { + EnsureFinite( + referenceSpeedMetersPerSecond, + nameof(referenceSpeedMetersPerSecond)); + EnsureFinitePositive( + deltaTimeSeconds, + nameof(deltaTimeSeconds)); + + VehicleState = vehicleState; + Projection = projection; + ReferenceSpeedMetersPerSecond = + referenceSpeedMetersPerSecond; + DeltaTimeSeconds = deltaTimeSeconds; + } + + /// + /// 获取本周期经过校验的实际车辆位姿和速度状态。 + /// + public VehicleState VehicleState { get; } + + /// + /// 获取实际车体中心投影到参考轨迹后得到的参考状态和跟踪误差。 + /// + public TrajectoryProjection Projection { get; } + + /// + /// 获取本次控制计算距离上次计算的真实时间间隔,单位为s。 + /// + public double DeltaTimeSeconds { get; } + + /// + /// 获取轨迹投影点要求的有符号参考速度,单位为m/s。 + /// + public double ReferenceSpeedMetersPerSecond { get; } + + /// + /// 获取车辆在车体X轴方向上的实际纵向速度,单位为m/s。 + /// + public double ActualLongitudinalSpeedMetersPerSecond => + VehicleState.TwistInBody + .VxMetersPerSecond; + + /// + /// 获取轨迹投影点的参考曲率,单位为1/m,左转为正。 + /// + public double ReferenceCurvaturePerMeter => + Projection.ReferencePoint + .CurvaturePerMeter; + + /// + /// 获取参考轨迹相对车辆的有符号横向误差,单位为m,轨迹在车辆左侧时为正。 + /// + public double LateralErrorMeters => + Projection.LateralErrorMeters; + + /// + /// 获取参考航向减实际车体航向的最短角差,单位为rad,逆时针为正。 + /// + public double HeadingErrorRadians => + Projection.HeadingErrorRadians; + + /// + /// 获取当前投影位置沿参考轨迹到终点的剩余距离,单位为m。 + /// + public double RemainingDistanceMeters => + Projection.RemainingDistanceMeters; + + /// + /// 获取实际速度是否已经由至少两个连续有效定位样本估算得到。 + /// + public bool HasValidVelocityEstimate => + VehicleState.HasValidVelocityEstimate; + + /// + /// 检查控制周期是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹跟踪控制周期必须是正有限值。"); + } + } + + /// + /// 检查控制参考速度是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹跟踪参考速度必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Allocation/GcpCommandAllocator.cs b/ClumsyPilot/Control/Allocation/GcpCommandAllocator.cs new file mode 100644 index 0000000..77ccdca --- /dev/null +++ b/ClumsyPilot/Control/Allocation/GcpCommandAllocator.cs @@ -0,0 +1,104 @@ +using System; +using MultiWheelC.Control.Abstractions; + +namespace MultiWheelC.Control.Allocation +{ + /// + /// 独立限制前后GCP目标转角并与纵向速度组合成底盘运动命令。 + /// + public sealed class GcpCommandAllocator + { + /// + /// 创建使用指定前后GCP最大转角的命令分配器。 + /// + public GcpCommandAllocator(double maximumGcpAngleRadians) + { + EnsureFinitePositive( + maximumGcpAngleRadians, + nameof(maximumGcpAngleRadians)); + + if (maximumGcpAngleRadians >= Math.PI / 2.0) + { + throw new ArgumentOutOfRangeException( + nameof(maximumGcpAngleRadians), + "最大GCP转角必须小于π/2。"); + } + + MaximumGcpAngleRadians = maximumGcpAngleRadians; + } + + /// + /// 获取前后GCP允许的最大转角绝对值,单位为rad。 + /// + public double MaximumGcpAngleRadians { get; } + + /// + /// 将纵向速度和前后GCP转角组合为底盘运动命令。 + /// + public GcpMotionCommand Allocate( + double speedMetersPerSecond, + LateralControlCommand lateralCommand) + { + EnsureFinite( + speedMetersPerSecond, + nameof(speedMetersPerSecond)); + + var frontAngleRadians = ClampSymmetric( + lateralCommand.FrontGcpAngleRadians, + MaximumGcpAngleRadians); + var rearAngleRadians = ClampSymmetric( + lateralCommand.RearGcpAngleRadians, + MaximumGcpAngleRadians); + + return new GcpMotionCommand( + speedMetersPerSecond, + frontAngleRadians, + rearAngleRadians); + } + + /// + /// 将数值按正负对称方式限制在指定绝对值内。 + /// + private static double ClampSymmetric( + double value, + double maximumAbsoluteValue) + { + return Math.Max( + -maximumAbsoluteValue, + Math.Min(maximumAbsoluteValue, value)); + } + + /// + /// 检查参数是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "GCP分配参数必须是正有限值。"); + } + } + + /// + /// 检查参数或命令是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "GCP分配参数和命令必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Allocation/GcpMotionCommand.cs b/ClumsyPilot/Control/Allocation/GcpMotionCommand.cs new file mode 100644 index 0000000..f36ce0e --- /dev/null +++ b/ClumsyPilot/Control/Allocation/GcpMotionCommand.cs @@ -0,0 +1,67 @@ +using System; + +namespace MultiWheelC.Control.Allocation +{ + /// + /// 表示发送给旧版多舵轮四轮解算前的有符号速度和前后GCP角度命令。 + /// + public readonly struct GcpMotionCommand + { + /// + /// 创建统一使用m/s和rad的前后几何控制点运动命令。 + /// + public GcpMotionCommand( + double speedMetersPerSecond, + double frontAngleRadians, + double rearAngleRadians) + { + EnsureFinite( + speedMetersPerSecond, + nameof(speedMetersPerSecond)); + EnsureFinite( + frontAngleRadians, + nameof(frontAngleRadians)); + EnsureFinite( + rearAngleRadians, + nameof(rearAngleRadians)); + + SpeedMetersPerSecond = + speedMetersPerSecond; + FrontAngleRadians = + frontAngleRadians; + RearAngleRadians = + rearAngleRadians; + } + + /// + /// 获取准备交给底盘的有符号纵向速度,单位为m/s,正值表示前进。 + /// + public double SpeedMetersPerSecond { get; } + + /// + /// 获取前几何控制点相对车体X轴的目标方向,单位为rad,逆时针为正。 + /// + public double FrontAngleRadians { get; } + + /// + /// 获取后几何控制点相对车体X轴的目标方向,单位为rad,逆时针为正。 + /// + public double RearAngleRadians { get; } + + /// + /// 检查底盘中间命令是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "GCP运动命令必须由有限值组成。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Common/PidController.cs b/ClumsyPilot/Control/Common/PidController.cs new file mode 100644 index 0000000..9d81320 --- /dev/null +++ b/ClumsyPilot/Control/Common/PidController.cs @@ -0,0 +1,307 @@ +using System; + +namespace MultiWheelC.Control.Common +{ + /// + /// 使用真实控制周期计算带积分限幅、输出限幅和抗饱和的通用有状态PID输出。 + /// + public sealed class PidController + { + private double _integralState; + private double _previousError; + private double _previousMeasurement; + private bool _hasPreviousSample; + + /// + /// 创建具有指定增益、积分输出限制和微分形式的PID控制器。 + /// + public PidController( + double proportionalGain, + double integralGainPerSecond, + double derivativeGainSeconds, + double maximumIntegralOutput, + bool derivativeOnMeasurement = true) + { + EnsureFiniteNonNegative( + proportionalGain, + nameof(proportionalGain)); + EnsureFiniteNonNegative( + integralGainPerSecond, + nameof(integralGainPerSecond)); + EnsureFiniteNonNegative( + derivativeGainSeconds, + nameof(derivativeGainSeconds)); + EnsureFiniteNonNegative( + maximumIntegralOutput, + nameof(maximumIntegralOutput)); + + ProportionalGain = proportionalGain; + IntegralGainPerSecond = integralGainPerSecond; + DerivativeGainSeconds = derivativeGainSeconds; + MaximumIntegralOutput = maximumIntegralOutput; + DerivativeOnMeasurement = derivativeOnMeasurement; + } + + /// + /// 获取比例增益。 + /// + public double ProportionalGain { get; } + + /// + /// 获取积分增益,单位为1/s。 + /// + public double IntegralGainPerSecond { get; } + + /// + /// 获取微分增益,单位为s。 + /// + public double DerivativeGainSeconds { get; } + + /// + /// 获取积分项允许产生的最大输出绝对值。 + /// + public double MaximumIntegralOutput { get; } + + /// + /// 获取微分项是否作用于测量值,以避免设定值变化产生微分冲击。 + /// + public bool DerivativeOnMeasurement { get; } + + /// + /// 获取最近一次设定值减测量值的误差。 + /// + public double LastError { get; private set; } + + /// + /// 获取最近一次比例项输出。 + /// + public double LastProportionalOutput { get; private set; } + + /// + /// 获取最近一次积分项输出。 + /// + public double LastIntegralOutput { get; private set; } + + /// + /// 获取最近一次微分项输出。 + /// + public double LastDerivativeOutput { get; private set; } + + /// + /// 获取最近一次经过输出范围限制后的PID输出。 + /// + public double LastOutput { get; private set; } + + /// + /// 根据设定值、测量值、真实时间间隔和本周期输出范围更新PID。 + /// + public double Update( + double setPoint, + double measurement, + double deltaTimeSeconds, + double minimumOutput, + double maximumOutput) + { + EnsureFinite(setPoint, nameof(setPoint)); + EnsureFinite(measurement, nameof(measurement)); + EnsureFinitePositive( + deltaTimeSeconds, + nameof(deltaTimeSeconds)); + EnsureFinite(minimumOutput, nameof(minimumOutput)); + EnsureFinite(maximumOutput, nameof(maximumOutput)); + + if (minimumOutput > maximumOutput) + { + throw new ArgumentOutOfRangeException( + nameof(minimumOutput), + "PID最小输出不能大于最大输出。"); + } + + var error = setPoint - measurement; + var proportionalOutput = + ProportionalGain * error; + var derivativeOutput = CalculateDerivativeOutput( + error, + measurement, + deltaTimeSeconds); + + var candidateIntegralState = + _integralState + + error * deltaTimeSeconds; + var integralOutput = CalculateIntegralOutput( + candidateIntegralState); + + // 同步截断积分状态本身,避免积分输出虽已限幅、内部状态仍继续增长。 + candidateIntegralState = + IntegralGainPerSecond > 0.0 && + MaximumIntegralOutput > 0.0 + ? integralOutput / + IntegralGainPerSecond + : 0.0; + + var unlimitedOutput = + proportionalOutput + + integralOutput + + derivativeOutput; + var output = Clamp( + unlimitedOutput, + minimumOutput, + maximumOutput); + + // 根据实际允许输出反算积分项,避免执行器饱和期间继续积累误差。 + if (IntegralGainPerSecond > 0.0 && + output != unlimitedOutput) + { + integralOutput = Clamp( + output - + proportionalOutput - + derivativeOutput, + -MaximumIntegralOutput, + MaximumIntegralOutput); + candidateIntegralState = + integralOutput / + IntegralGainPerSecond; + } + + _integralState = + IntegralGainPerSecond > 0.0 && + MaximumIntegralOutput > 0.0 + ? candidateIntegralState + : 0.0; + _previousError = error; + _previousMeasurement = measurement; + _hasPreviousSample = true; + + LastError = error; + LastProportionalOutput = proportionalOutput; + LastIntegralOutput = integralOutput; + LastDerivativeOutput = derivativeOutput; + LastOutput = output; + + return output; + } + + /// + /// 清除积分、历史采样和最近一次PID诊断输出。 + /// + public void Reset() + { + _integralState = 0.0; + _previousError = 0.0; + _previousMeasurement = 0.0; + _hasPreviousSample = false; + LastError = 0.0; + LastProportionalOutput = 0.0; + LastIntegralOutput = 0.0; + LastDerivativeOutput = 0.0; + LastOutput = 0.0; + } + + /// + /// 使用测量值微分或误差微分计算本周期微分项输出。 + /// + private double CalculateDerivativeOutput( + double error, + double measurement, + double deltaTimeSeconds) + { + if (!_hasPreviousSample || + DerivativeGainSeconds <= 0.0) + { + return 0.0; + } + + if (DerivativeOnMeasurement) + { + return -DerivativeGainSeconds * + (measurement - _previousMeasurement) / + deltaTimeSeconds; + } + + return DerivativeGainSeconds * + (error - _previousError) / + deltaTimeSeconds; + } + + /// + /// 根据积分状态计算经过绝对值限制的积分项输出。 + /// + private double CalculateIntegralOutput( + double integralState) + { + if (IntegralGainPerSecond <= 0.0 || + MaximumIntegralOutput <= 0.0) + { + return 0.0; + } + + return Clamp( + IntegralGainPerSecond * integralState, + -MaximumIntegralOutput, + MaximumIntegralOutput); + } + + /// + /// 将数值限制在指定闭区间内。 + /// + private static double Clamp( + double value, + double minimum, + double maximum) + { + return Math.Max( + minimum, + Math.Min(maximum, value)); + } + + /// + /// 检查参数是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "PID时间间隔必须是正有限值。"); + } + } + + /// + /// 检查参数是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "PID增益和积分输出限幅必须是非负有限值。"); + } + } + + /// + /// 检查参数是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "PID参数和输入必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Execution/GcpCommandExecutor.cs b/ClumsyPilot/Control/Execution/GcpCommandExecutor.cs new file mode 100644 index 0000000..ee21a84 --- /dev/null +++ b/ClumsyPilot/Control/Execution/GcpCommandExecutor.cs @@ -0,0 +1,177 @@ +using System; +using MultiWheelC.Control.Allocation; +using MyParking.Shared; + +namespace MultiWheelC.Control.Execution +{ + /// + /// 将SI单位的GCP运动命令安全转换为现有多舵轮底盘调用。 + /// + public sealed class GcpCommandExecutor + { + private const double StopSpeedDeadbandMetersPerSecond = + 1e-6; + + private readonly MultiWheelChassisAdapter _chassisAdapter; + private double _lastFrontAngleRadians; + private double _lastRearAngleRadians; + + /// + /// 创建绑定指定单车底盘适配器的GCP命令执行器。 + /// + public GcpCommandExecutor( + MultiWheelChassisAdapter chassisAdapter, + double maximumGcpAngleRateRadiansPerSecond = + 10.0 * Math.PI / 180.0) + { + _chassisAdapter = chassisAdapter ?? + throw new ArgumentNullException( + nameof(chassisAdapter)); + EnsureFinitePositive( + maximumGcpAngleRateRadiansPerSecond, + nameof(maximumGcpAngleRateRadiansPerSecond)); + + MaximumGcpAngleRateRadiansPerSecond = + maximumGcpAngleRateRadiansPerSecond; + } + + /// + /// 获取执行器绑定的车辆编号。 + /// + public int VehicleId => + _chassisAdapter.VehicleId; + + /// + /// 获取前后GCP目标角度允许的最大变化率,单位为rad/s。 + /// + public double MaximumGcpAngleRateRadiansPerSecond { get; } + + /// + /// 获取最近一次控制器请求的未限速GCP命令。 + /// + public GcpMotionCommand? LastRequestedCommand { get; private set; } + + /// + /// 获取最近一次经过GCP角速度限制后实际发送给底盘的命令。 + /// + public GcpMotionCommand? LastSentCommand { get; private set; } + + /// + /// 获取最近一次旧版底盘运动分解失败原因。 + /// + public string LastFailureReason { get; private set; } = + string.Empty; + + /// + /// 使用真实控制周期执行一条GCP命令,并在分解失败时保持停车。 + /// + public bool Execute( + GcpMotionCommand command, + double deltaTimeSeconds) + { + EnsureFinitePositive( + deltaTimeSeconds, + nameof(deltaTimeSeconds)); + LastRequestedCommand = command; + + if (Math.Abs(command.SpeedMetersPerSecond) <= + StopSpeedDeadbandMetersPerSecond) + { + Stop(); + LastSentCommand = new GcpMotionCommand( + 0.0, + _lastFrontAngleRadians, + _lastRearAngleRadians); + return true; + } + + var maximumAngleChangeRadians = + MaximumGcpAngleRateRadiansPerSecond * + deltaTimeSeconds; + _lastFrontAngleRadians = MoveTowards( + _lastFrontAngleRadians, + command.FrontAngleRadians, + maximumAngleChangeRadians); + _lastRearAngleRadians = MoveTowards( + _lastRearAngleRadians, + command.RearAngleRadians, + maximumAngleChangeRadians); + + var limitedCommand = new GcpMotionCommand( + command.SpeedMetersPerSecond, + _lastFrontAngleRadians, + _lastRearAngleRadians); + LastSentCommand = limitedCommand; + + var success = _chassisAdapter.SendGcpMotion( + limitedCommand.SpeedMetersPerSecond, + limitedCommand.FrontAngleRadians, + limitedCommand.RearAngleRadians, + TimeSpan.FromSeconds(deltaTimeSeconds)); + + LastFailureReason = success + ? string.Empty + : BuildFailureReason(); + + return success; + } + + /// + /// 立即清零底盘驱动速度并清除执行器失败状态。 + /// + public void Stop() + { + _chassisAdapter.StopImmediately(); + LastFailureReason = string.Empty; + } + + /// + /// 以不超过指定单周期变化量的速度使当前值接近目标值。 + /// + private static double MoveTowards( + double current, + double target, + double maximumChange) + { + var difference = target - current; + + if (Math.Abs(difference) <= maximumChange) + { + return target; + } + + return current + + Math.Sign(difference) * + maximumChange; + } + + /// + /// 将底盘返回的空失败原因替换为可诊断的默认说明。 + /// + private string BuildFailureReason() + { + return string.IsNullOrWhiteSpace( + _chassisAdapter.LastFailureReason) + ? "旧版SendMotion未能完成GCP运动分解。" + : _chassisAdapter.LastFailureReason; + } + + /// + /// 检查控制周期是否为正有限值且能够转换为TimeSpan。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value <= 0.0 || + value > TimeSpan.MaxValue.TotalSeconds) + { + throw new ArgumentOutOfRangeException( + parameterName, + "GCP命令控制周期必须是TimeSpan可表示的正有限秒数。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Execution/ParkingGeometricController.cs b/ClumsyPilot/Control/Execution/ParkingGeometricController.cs new file mode 100644 index 0000000..b17c559 --- /dev/null +++ b/ClumsyPilot/Control/Execution/ParkingGeometricController.cs @@ -0,0 +1,551 @@ +using System; +using MultiWheelC.Control.Abstractions; +using MultiWheelC.Control.Allocation; +using MultiWheelC.StateEstimation; +using MultiWheelC.Trajectory; +using MyParking.Shared; + +namespace MultiWheelC.Control.Execution +{ + /// + /// 表示新版停车机器人单周期轨迹控制的执行结果。 + /// + public enum ParkingControlCycleResult + { + Inactive = 0, + CommandSent = 1, + Completed = 2, + StateUnavailable = 3, + Faulted = 4 + } + + /// + /// 组织状态读取、轨迹投影、横纵向控制、GCP分配和底盘命令执行。 + /// + public sealed class ParkingGeometricController + { + private const double ZeroReferenceSpeedToleranceMetersPerSecond = + 1e-6; + private const double StartupRegionMeters = 0.02; + private const double StartupPreviewDistanceMeters = 0.05; + private const double MaximumStartupSpeedMetersPerSecond = 0.08; + + private readonly IVehicleStateProvider _stateProvider; + private readonly ILateralController _lateralController; + private readonly ILongitudinalController _longitudinalController; + private readonly GcpCommandAllocator _gcpAllocator; + private readonly GcpCommandExecutor _commandExecutor; + + private Trajectory2D _trajectory; + + /// + /// 创建具有终点判定和轨迹偏离保护的单车轨迹控制器。 + /// + public ParkingGeometricController( + IVehicleStateProvider stateProvider, + ILateralController lateralController, + ILongitudinalController longitudinalController, + GcpCommandAllocator gcpAllocator, + GcpCommandExecutor commandExecutor, + double finishDistanceMeters = 0.04, + double finishSpeedMetersPerSecond = 0.02, + double finishHeadingToleranceRadians = + 3.0 * Math.PI / 180.0, + double maximumDistanceToTrajectoryMeters = 0.30) + { + _stateProvider = stateProvider ?? + throw new ArgumentNullException( + nameof(stateProvider)); + _lateralController = lateralController ?? + throw new ArgumentNullException( + nameof(lateralController)); + _longitudinalController = longitudinalController ?? + throw new ArgumentNullException( + nameof(longitudinalController)); + _gcpAllocator = gcpAllocator ?? + throw new ArgumentNullException( + nameof(gcpAllocator)); + _commandExecutor = commandExecutor ?? + throw new ArgumentNullException( + nameof(commandExecutor)); + + EnsureFinitePositive( + finishDistanceMeters, + nameof(finishDistanceMeters)); + EnsureFiniteNonNegative( + finishSpeedMetersPerSecond, + nameof(finishSpeedMetersPerSecond)); + EnsureFinitePositive( + finishHeadingToleranceRadians, + nameof(finishHeadingToleranceRadians)); + EnsureFinitePositive( + maximumDistanceToTrajectoryMeters, + nameof(maximumDistanceToTrajectoryMeters)); + + FinishDistanceMeters = finishDistanceMeters; + FinishSpeedMetersPerSecond = + finishSpeedMetersPerSecond; + FinishHeadingToleranceRadians = + finishHeadingToleranceRadians; + MaximumDistanceToTrajectoryMeters = + maximumDistanceToTrajectoryMeters; + } + + /// + /// 获取终点位置和剩余弧长允许的误差,单位为m。 + /// + public double FinishDistanceMeters { get; } + + /// + /// 获取判定轨迹执行完成时允许的最大实际线速度,单位为m/s。 + /// + public double FinishSpeedMetersPerSecond { get; } + + /// + /// 获取判定轨迹完成时允许的最大终点航向误差,单位为rad。 + /// + public double FinishHeadingToleranceRadians { get; } + + /// + /// 获取允许车辆偏离参考轨迹的最大距离,单位为m。 + /// + public double MaximumDistanceToTrajectoryMeters { get; } + + /// + /// 获取控制器当前是否持有并正在执行一条轨迹。 + /// + public bool IsActive { get; private set; } + + /// + /// 获取最近一次轨迹是否已经满足终点完成条件。 + /// + public bool IsCompleted { get; private set; } + + /// + /// 获取最近一次控制失败原因,正常时为空字符串。 + /// + public string LastFailureReason { get; private set; } = + string.Empty; + + /// + /// 获取最近一次控制异常,正常时为空。 + /// + public Exception LastException { get; private set; } + + /// + /// 获取最近一次有效车辆状态。 + /// + public VehicleState? LastVehicleState { get; private set; } + + /// + /// 获取最近一次车体中心到参考轨迹的投影结果。 + /// + public TrajectoryProjection? LastProjection { get; private set; } + + /// + /// 获取最近一次发送或准备发送的GCP运动命令。 + /// + public GcpMotionCommand? LastCommand { get; private set; } + + /// + /// 获取最近控制周期实际交给纵向控制器的参考速度,单位为m/s。 + /// + public double? LastReferenceSpeedMetersPerSecond { get; private set; } + + /// + /// 停止当前底盘并从起点开始执行指定二维轨迹。 + /// + public void Start(Trajectory2D trajectory) + { + if (trajectory == null) + { + throw new ArgumentNullException( + nameof(trajectory)); + } + + StopAndResetControllers(); + _trajectory = trajectory; + IsActive = true; + IsCompleted = false; + ClearDiagnostics(); + } + + /// + /// 读取本周期车辆状态并执行一次完整的轨迹跟踪控制计算。 + /// + public ParkingControlCycleResult ExecuteCycle( + double deltaTimeSeconds) + { + EnsureFinitePositive( + deltaTimeSeconds, + nameof(deltaTimeSeconds)); + + if (!IsActive || _trajectory == null) + { + return ParkingControlCycleResult.Inactive; + } + + try + { + if (!_stateProvider.TryGetState( + out var vehicleState)) + { + StopForUnavailableState(); + return ParkingControlCycleResult + .StateUnavailable; + } + + LastVehicleState = vehicleState; + + var projection = TrajectoryProjector.Project( + _trajectory, + vehicleState.PoseInWorld); + LastProjection = projection; + + if (projection.DistanceToTrajectoryMeters > + MaximumDistanceToTrajectoryMeters) + { + return EnterFault( + "车辆距离参考轨迹" + + $"{projection.DistanceToTrajectoryMeters:F3}m," + + "超过允许值" + + $"{MaximumDistanceToTrajectoryMeters:F3}m。"); + } + + if (HasReachedEnd( + vehicleState, + projection)) + { + CompleteTrajectory(); + return ParkingControlCycleResult.Completed; + } + + if (HasStoppedAtUnsatisfiedTerminal( + vehicleState, + projection, + out var terminalFailureReason)) + { + return EnterFault( + terminalFailureReason); + } + + var referenceSpeedMetersPerSecond = + ResolveReferenceSpeedForControl( + projection); + LastReferenceSpeedMetersPerSecond = + referenceSpeedMetersPerSecond; + var context = new PathTrackingContext( + vehicleState, + projection, + referenceSpeedMetersPerSecond, + deltaTimeSeconds); + var lateralCommand = + _lateralController.Compute(context); + var commandSpeedMetersPerSecond = + _longitudinalController + .ComputeSpeedMetersPerSecond(context); + var gcpCommand = _gcpAllocator.Allocate( + commandSpeedMetersPerSecond, + lateralCommand); + + if (!_commandExecutor.Execute( + gcpCommand, + deltaTimeSeconds)) + { + return EnterFault( + string.IsNullOrWhiteSpace( + _commandExecutor.LastFailureReason) + ? "GCP底盘命令执行失败。" + : _commandExecutor.LastFailureReason); + } + + LastCommand = + _commandExecutor.LastSentCommand; + + LastFailureReason = string.Empty; + LastException = null; + return ParkingControlCycleResult.CommandSent; + } + catch (Exception exception) + { + return EnterFault( + "停车机器人轨迹控制周期异常:" + + exception.Message, + exception); + } + } + + /// + /// 主动取消当前轨迹、立即停车并清除全部控制器状态。 + /// + public void Cancel() + { + StopAndResetControllers(); + _trajectory = null; + IsActive = false; + IsCompleted = false; + ClearDiagnostics(); + } + + /// + /// 在轨迹起点零速固定点处读取前方速度,并限制为低速起步命令。 + /// + private double ResolveReferenceSpeedForControl( + TrajectoryProjection projection) + { + var currentReferenceSpeed = + projection.ReferencePoint + .ReferenceSpeedMetersPerSecond; + + var requiresStartupRelease = + projection.ArcLengthMeters <= + StartupRegionMeters && + projection.RemainingDistanceMeters > + FinishDistanceMeters && + Math.Abs(currentReferenceSpeed) <= + ZeroReferenceSpeedToleranceMetersPerSecond; + + if (!requiresStartupRelease) + { + return currentReferenceSpeed; + } + + var previewArcLengthMeters = Math.Min( + _trajectory.TotalLengthMeters, + projection.ArcLengthMeters + + StartupPreviewDistanceMeters); + var previewReferenceSpeed = + _trajectory + .SampleAtArcLength( + previewArcLengthMeters) + .ReferenceSpeedMetersPerSecond; + + if (Math.Abs(previewReferenceSpeed) <= + ZeroReferenceSpeedToleranceMetersPerSecond) + { + return 0.0; + } + + return Math.Sign(previewReferenceSpeed) * + Math.Min( + Math.Abs(previewReferenceSpeed), + MaximumStartupSpeedMetersPerSecond); + } + + /// + /// 根据终点距离、剩余弧长和实际线速度判断轨迹是否完成。 + /// + private bool HasReachedEnd( + VehicleState vehicleState, + TrajectoryProjection projection) + { + if (!vehicleState.HasValidVelocityEstimate) + { + return false; + } + + return projection.RemainingDistanceMeters <= + FinishDistanceMeters && + CalculateDistanceToEndMeters( + vehicleState) <= + FinishDistanceMeters && + CalculateHeadingErrorToEndRadians( + vehicleState) <= + FinishHeadingToleranceRadians && + CalculateActualLinearSpeedMetersPerSecond( + vehicleState) <= + FinishSpeedMetersPerSecond; + } + + /// + /// 检查车辆是否已在终点零速参考处停稳但最终位置或航向仍不合格。 + /// + private bool HasStoppedAtUnsatisfiedTerminal( + VehicleState vehicleState, + TrajectoryProjection projection, + out string failureReason) + { + failureReason = string.Empty; + + var isTerminalZeroSpeedReference = + projection.RemainingDistanceMeters <= + FinishDistanceMeters && + Math.Abs( + projection.ReferencePoint + .ReferenceSpeedMetersPerSecond) <= + ZeroReferenceSpeedToleranceMetersPerSecond; + + if (!isTerminalZeroSpeedReference || + !vehicleState.HasValidVelocityEstimate || + CalculateActualLinearSpeedMetersPerSecond( + vehicleState) > + FinishSpeedMetersPerSecond) + { + return false; + } + + var positionErrorMeters = + CalculateDistanceToEndMeters( + vehicleState); + var headingErrorRadians = + CalculateHeadingErrorToEndRadians( + vehicleState); + + failureReason = + "车辆已在终点零速参考处停稳,但终点精度不满足要求:" + + $"位置误差={positionErrorMeters:F3}m," + + "航向误差=" + + $"{AngleMath.RadiansToDegrees(headingErrorRadians):F2}°。"; + return true; + } + + /// + /// 计算实际车体中心到轨迹终点的欧氏距离,单位为m。 + /// + private double CalculateDistanceToEndMeters( + VehicleState vehicleState) + { + var endPoint = _trajectory.EndPoint.PoseInWorld; + var deltaX = + vehicleState.PoseInWorld.XMeters - + endPoint.XMeters; + var deltaY = + vehicleState.PoseInWorld.YMeters - + endPoint.YMeters; + + return Math.Sqrt( + deltaX * deltaX + + deltaY * deltaY); + } + + /// + /// 计算实际车体航向到轨迹终点航向的最短角度误差绝对值,单位为rad。 + /// + private double CalculateHeadingErrorToEndRadians( + VehicleState vehicleState) + { + return Math.Abs( + AngleMath.ShortestDifferenceRadians( + _trajectory.EndPoint + .PoseInWorld.YawRadians, + vehicleState + .PoseInWorld.YawRadians)); + } + + /// + /// 计算车体坐标系实际线速度的合速度绝对值,单位为m/s。 + /// + private static double CalculateActualLinearSpeedMetersPerSecond( + VehicleState vehicleState) + { + return Math.Sqrt( + vehicleState.TwistInBody.VxMetersPerSecond * + vehicleState.TwistInBody.VxMetersPerSecond + + vehicleState.TwistInBody.VyMetersPerSecond * + vehicleState.TwistInBody.VyMetersPerSecond); + } + + /// + /// 在状态暂不可用时停车并重置反馈控制器,同时保留轨迹等待下一周期恢复。 + /// + private void StopForUnavailableState() + { + _commandExecutor.Stop(); + _lateralController.Reset(); + _longitudinalController.Reset(); + LastCommand = null; + LastFailureReason = + "当前无法获得有效车辆状态,底盘已停车并等待定位恢复。"; + LastException = null; + } + + /// + /// 完成当前轨迹并停车,但保留最后状态和投影供实验记录读取。 + /// + private void CompleteTrajectory() + { + StopAndResetControllers(); + IsActive = false; + IsCompleted = true; + LastCommand = new GcpMotionCommand( + 0.0, + 0.0, + 0.0); + LastFailureReason = string.Empty; + LastException = null; + } + + /// + /// 发生不可继续的控制故障时停车、退出活动状态并保存诊断信息。 + /// + private ParkingControlCycleResult EnterFault( + string reason, + Exception exception = null) + { + StopAndResetControllers(); + IsActive = false; + IsCompleted = false; + LastCommand = null; + LastFailureReason = reason; + LastException = exception; + return ParkingControlCycleResult.Faulted; + } + + /// + /// 立即停止底盘并清除横向和纵向控制器的跨周期状态。 + /// + private void StopAndResetControllers() + { + _commandExecutor.Stop(); + _lateralController.Reset(); + _longitudinalController.Reset(); + } + + /// + /// 清除上一条轨迹留下的状态、命令和故障诊断信息。 + /// + private void ClearDiagnostics() + { + LastVehicleState = null; + LastProjection = null; + LastCommand = null; + LastReferenceSpeedMetersPerSecond = null; + LastFailureReason = string.Empty; + LastException = null; + } + + /// + /// 检查控制参数是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹控制器距离和周期参数必须是正有限值。"); + } + } + + /// + /// 检查控制参数是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹控制器速度参数必须是非负有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Lateral/StanleyLateralController.cs b/ClumsyPilot/Control/Lateral/StanleyLateralController.cs new file mode 100644 index 0000000..9c281a7 --- /dev/null +++ b/ClumsyPilot/Control/Lateral/StanleyLateralController.cs @@ -0,0 +1,250 @@ +using System; +using MultiWheelC.Control.Abstractions; + +namespace MultiWheelC.Control.Lateral +{ + /// + /// 将参考曲率、横向误差和航向误差分别转换为前、后GCP目标转角。 + /// + public sealed class StanleyLateralController : ILateralController + { + /// + /// 创建使用指定GCP几何、Stanley增益和转角保护参数的横向控制器。 + /// + public StanleyLateralController( + double controlPointRadiusMeters, + double crossTrackGainPerSecond, + double headingErrorGain, + double minimumSpeedMetersPerSecond, + bool useActualSpeedForGain = true, + double maximumCrossTrackCorrectionRadians = + 10.0 * Math.PI / 180.0, + double maximumHeadingCorrectionRadians = + 10.0 * Math.PI / 180.0) + { + EnsureFinitePositive( + controlPointRadiusMeters, + nameof(controlPointRadiusMeters)); + EnsureFiniteNonNegative( + crossTrackGainPerSecond, + nameof(crossTrackGainPerSecond)); + EnsureFiniteNonNegative( + headingErrorGain, + nameof(headingErrorGain)); + EnsureFinitePositive( + minimumSpeedMetersPerSecond, + nameof(minimumSpeedMetersPerSecond)); + EnsureFinitePositive( + maximumCrossTrackCorrectionRadians, + nameof(maximumCrossTrackCorrectionRadians)); + EnsureFinitePositive( + maximumHeadingCorrectionRadians, + nameof(maximumHeadingCorrectionRadians)); + + ControlPointRadiusMeters = controlPointRadiusMeters; + CrossTrackGainPerSecond = crossTrackGainPerSecond; + HeadingErrorGain = headingErrorGain; + MinimumSpeedMetersPerSecond = minimumSpeedMetersPerSecond; + UseActualSpeedForGain = useActualSpeedForGain; + MaximumCrossTrackCorrectionRadians = + maximumCrossTrackCorrectionRadians; + MaximumHeadingCorrectionRadians = + maximumHeadingCorrectionRadians; + } + + /// + /// 获取车体中心到前、后GCP的距离,单位为m。 + /// + public double ControlPointRadiusMeters { get; } + + /// + /// 获取横向误差增益,单位为1/s。 + /// + public double CrossTrackGainPerSecond { get; } + + /// + /// 获取航向误差的无量纲增益。 + /// + public double HeadingErrorGain { get; } + + /// + /// 获取Stanley分母使用的最小速度绝对值,单位为m/s。 + /// + public double MinimumSpeedMetersPerSecond { get; } + + /// + /// 获取是否优先使用当前状态源提供的实际纵向速度计算横向修正。 + /// + public bool UseActualSpeedForGain { get; } + + /// + /// 获取横向误差共同转角分量的最大绝对值,单位为rad。 + /// + public double MaximumCrossTrackCorrectionRadians { get; } + + /// + /// 获取航向误差差动转角分量的最大绝对值,单位为rad。 + /// + public double MaximumHeadingCorrectionRadians { get; } + + /// + /// 分别计算横向共同转角以及曲率和航向差动转角,并生成前后GCP命令。 + /// + public LateralControlCommand Compute( + PathTrackingContext context) + { + var speedForGain = SelectSpeedForGain(context); + var speedMagnitude = Math.Max( + Math.Abs(speedForGain), + MinimumSpeedMetersPerSecond); + var travelDirection = SelectTravelDirection(context); + + // 参考曲率决定前后反向的差动转角,使无跟踪误差时也能沿曲线行驶。 + var feedforwardAngleRadians = Math.Atan( + context.ReferenceCurvaturePerMeter * + ControlPointRadiusMeters); + + // 横向误差生成前后同向的共同转角,使四舵轮车辆平稳靠近轨迹。 + var crossTrackCorrectionRadians = + ClampSymmetric( + Math.Atan( + CrossTrackGainPerSecond * + context.LateralErrorMeters / + speedMagnitude), + MaximumCrossTrackCorrectionRadians); + + // 航向误差生成前后反向的差动转角,只负责调整车身朝向。 + var headingCorrectionRadians = + ClampSymmetric( + HeadingErrorGain * + context.HeadingErrorRadians, + MaximumHeadingCorrectionRadians); + + var commonAngleRadians = + travelDirection * + crossTrackCorrectionRadians; + var differentialAngleRadians = + feedforwardAngleRadians + + travelDirection * + headingCorrectionRadians; + + return new LateralControlCommand( + commonAngleRadians + + differentialAngleRadians, + commonAngleRadians - + differentialAngleRadians); + } + + /// + /// 清除横向控制器状态;当前Stanley实现没有跨周期状态。 + /// + public void Reset() + { + } + + /// + /// 选择Stanley横向误差项使用的实际速度或参考速度。 + /// + private double SelectSpeedForGain( + PathTrackingContext context) + { + if (UseActualSpeedForGain && + context.HasValidVelocityEstimate) + { + return context + .ActualLongitudinalSpeedMetersPerSecond; + } + + return context.ReferenceSpeedMetersPerSecond; + } + + /// + /// 根据有符号参考速度确定前进或倒车时的反馈修正方向。 + /// + private static double SelectTravelDirection( + PathTrackingContext context) + { + const double directionDeadbandMetersPerSecond = 1e-6; + + if (Math.Abs(context.ReferenceSpeedMetersPerSecond) > + directionDeadbandMetersPerSecond) + { + return Math.Sign( + context.ReferenceSpeedMetersPerSecond); + } + + if (context.HasValidVelocityEstimate && + Math.Abs( + context.ActualLongitudinalSpeedMetersPerSecond) > + directionDeadbandMetersPerSecond) + { + return Math.Sign( + context.ActualLongitudinalSpeedMetersPerSecond); + } + + return 1.0; + } + + /// + /// 将数值按正负对称方式限制在指定绝对值内。 + /// + private static double ClampSymmetric( + double value, + double maximumAbsoluteValue) + { + return Math.Max( + -maximumAbsoluteValue, + Math.Min(maximumAbsoluteValue, value)); + } + + /// + /// 检查控制参数是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "Stanley控制器的几何尺寸、速度和角度限制必须是正有限值。"); + } + } + + /// + /// 检查控制增益是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "Stanley控制增益必须是非负有限值。"); + } + } + + /// + /// 检查控制参数是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "Stanley控制参数必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Control/Longitudinal/PidLongitudinalController.cs b/ClumsyPilot/Control/Longitudinal/PidLongitudinalController.cs new file mode 100644 index 0000000..ecf61f0 --- /dev/null +++ b/ClumsyPilot/Control/Longitudinal/PidLongitudinalController.cs @@ -0,0 +1,224 @@ +using System; +using MultiWheelC.Control.Abstractions; +using MultiWheelC.Control.Common; + +namespace MultiWheelC.Control.Longitudinal +{ + /// + /// 将轨迹参考速度前馈与通用PID速度反馈组合为有符号底盘命令速度。 + /// + public sealed class PidLongitudinalController + : ILongitudinalController + { + private const double ReferenceStopDeadbandMetersPerSecond = + 1e-6; + + private readonly PidController _feedbackPid; + + /// + /// 创建具有积分抗饱和和命令速度限幅的纵向速度外环。 + /// + public PidLongitudinalController( + double proportionalGain, + double integralGainPerSecond, + double derivativeGainSeconds, + double maximumIntegralCorrectionMetersPerSecond, + double maximumCommandSpeedMetersPerSecond, + double speedErrorDeadbandMetersPerSecond = 0.025) + { + EnsureFinitePositive( + maximumCommandSpeedMetersPerSecond, + nameof(maximumCommandSpeedMetersPerSecond)); + EnsureFiniteNonNegative( + speedErrorDeadbandMetersPerSecond, + nameof(speedErrorDeadbandMetersPerSecond)); + + _feedbackPid = new PidController( + proportionalGain, + integralGainPerSecond, + derivativeGainSeconds, + maximumIntegralCorrectionMetersPerSecond, + derivativeOnMeasurement: true); + MaximumCommandSpeedMetersPerSecond = + maximumCommandSpeedMetersPerSecond; + SpeedErrorDeadbandMetersPerSecond = + speedErrorDeadbandMetersPerSecond; + } + + /// + /// 获取负责计算速度误差修正量的通用PID控制器。 + /// + public PidController FeedbackPid => _feedbackPid; + + /// + /// 获取底盘命令速度的最大绝对值,单位为m/s。 + /// + public double MaximumCommandSpeedMetersPerSecond { get; } + + /// + /// 获取不触发纵向PID修正的速度误差死区,单位为m/s。 + /// + public double SpeedErrorDeadbandMetersPerSecond { get; } + + /// + /// 获取最近一次有效控制周期的参考速度减实际速度,单位为m/s。 + /// + public double LastSpeedErrorMetersPerSecond => + _feedbackPid.LastError; + + /// + /// 获取最近一次比例项产生的速度修正,单位为m/s。 + /// + public double LastProportionalCorrectionMetersPerSecond => + _feedbackPid.LastProportionalOutput; + + /// + /// 获取最近一次积分项产生的速度修正,单位为m/s。 + /// + public double LastIntegralCorrectionMetersPerSecond => + _feedbackPid.LastIntegralOutput; + + /// + /// 获取最近一次微分项产生的速度修正,单位为m/s。 + /// + public double LastDerivativeCorrectionMetersPerSecond => + _feedbackPid.LastDerivativeOutput; + + /// + /// 根据轨迹参考速度和Detour实际纵向速度计算底盘命令速度。 + /// + public double ComputeSpeedMetersPerSecond( + PathTrackingContext context) + { + var referenceSpeedMetersPerSecond = + context.ReferenceSpeedMetersPerSecond; + + // 轨迹明确要求停车时直接输出零,防止速度反馈使车辆在终点反向纠偏。 + if (Math.Abs(referenceSpeedMetersPerSecond) <= + ReferenceStopDeadbandMetersPerSecond) + { + Reset(); + return 0.0; + } + + // 定位速度尚不可用时只透传参考速度,不使用无效反馈更新PID状态。 + if (!context.HasValidVelocityEstimate) + { + Reset(); + return LimitReferenceSpeed( + referenceSpeedMetersPerSecond); + } + + var speedErrorMetersPerSecond = + referenceSpeedMetersPerSecond - + context.ActualLongitudinalSpeedMetersPerSecond; + + // Detour差分速度在参考速度附近会有小幅波动;死区内只使用速度前馈, + // 同时清除PID历史,避免噪声持续积累后产生突发修正。 + if (Math.Abs(speedErrorMetersPerSecond) <= + SpeedErrorDeadbandMetersPerSecond) + { + Reset(); + return LimitReferenceSpeed( + referenceSpeedMetersPerSecond); + } + + GetCorrectionOutputRange( + referenceSpeedMetersPerSecond, + out var minimumCorrectionMetersPerSecond, + out var maximumCorrectionMetersPerSecond); + + var correctionMetersPerSecond = + _feedbackPid.Update( + referenceSpeedMetersPerSecond, + context + .ActualLongitudinalSpeedMetersPerSecond, + context.DeltaTimeSeconds, + minimumCorrectionMetersPerSecond, + maximumCorrectionMetersPerSecond); + + return referenceSpeedMetersPerSecond + + correctionMetersPerSecond; + } + + /// + /// 清除纵向速度外环的积分、历史测量值和诊断输出。 + /// + public void Reset() + { + _feedbackPid.Reset(); + } + + /// + /// 根据参考行驶方向计算PID修正量允许使用的动态输出范围。 + /// + private void GetCorrectionOutputRange( + double referenceSpeedMetersPerSecond, + out double minimumCorrectionMetersPerSecond, + out double maximumCorrectionMetersPerSecond) + { + if (referenceSpeedMetersPerSecond > 0.0) + { + minimumCorrectionMetersPerSecond = + -referenceSpeedMetersPerSecond; + maximumCorrectionMetersPerSecond = + MaximumCommandSpeedMetersPerSecond - + referenceSpeedMetersPerSecond; + return; + } + + minimumCorrectionMetersPerSecond = + -MaximumCommandSpeedMetersPerSecond - + referenceSpeedMetersPerSecond; + maximumCorrectionMetersPerSecond = + -referenceSpeedMetersPerSecond; + } + + /// + /// 在没有有效速度反馈时限制参考速度的绝对值。 + /// + private double LimitReferenceSpeed( + double referenceSpeedMetersPerSecond) + { + return Math.Max( + -MaximumCommandSpeedMetersPerSecond, + Math.Min( + MaximumCommandSpeedMetersPerSecond, + referenceSpeedMetersPerSecond)); + } + + /// + /// 检查最大命令速度是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "纵向控制器最大命令速度必须是正有限值。"); + } + } + + /// + /// 检查速度误差死区是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "纵向控制器速度误差死区必须是非负有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Movements/TrajectoryTrackingMovement.cs b/ClumsyPilot/Movements/TrajectoryTrackingMovement.cs new file mode 100644 index 0000000..ad6f2cb --- /dev/null +++ b/ClumsyPilot/Movements/TrajectoryTrackingMovement.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using CommonUsage.Chassis; +using MultiWheelC.Control.Allocation; +using MultiWheelC.Control.Execution; +using MultiWheelC.Control.Lateral; +using MultiWheelC.Control.Longitudinal; +using MultiWheelC.StateEstimation; +using MultiWheelC.Trajectory; +using MyParking.Shared; + +namespace MultiWheelC +{ + /// + /// 使用新版横纵向控制器持续跟踪一条世界坐标系二维轨迹。 + /// + public sealed class TrajectoryTrackingMovement + : MovementDefinition + { + /// + /// 获取或设置本次动作需要跟踪的世界坐标系轨迹。 + /// + public Trajectory2D Trajectory; + + /// + /// 获取或设置本次动作使用的车辆状态源;为空时自动创建Detour状态源。 + /// + public IVehicleStateProvider StateProvider; + + /// + /// 获取或设置每个有效控制周期结束后的诊断数据观察回调。 + /// + public Action CycleObserver; + + /// + /// Stanley横向误差增益,单位为1/s。 + /// + public double StanleyCrossTrackGainPerSecond = 0.4; + + /// + /// Stanley航向误差增益。 + /// + public double StanleyHeadingErrorGain = 1.0; + + /// + /// Stanley低速分母保护速度,单位为m/s。 + /// + public double StanleyMinimumSpeedMetersPerSecond = 0.15; + + /// + /// 获取或设置Stanley是否优先使用当前状态源提供的实际纵向速度。 + /// + public bool StanleyUsesActualSpeed = true; + + /// + /// Stanley横向误差共同转角分量的最大绝对值,单位为rad。 + /// + public double MaximumCrossTrackCorrectionRadians = + AngleMath.DegreesToRadians(10.0); + + /// + /// Stanley航向误差差动转角分量的最大绝对值,单位为rad。 + /// + public double MaximumHeadingCorrectionRadians = + AngleMath.DegreesToRadians(10.0); + + /// + /// 纵向速度外环比例增益。 + /// + public double LongitudinalKp = 0.5; + + /// + /// 纵向速度外环积分增益,单位为1/s。 + /// + public double LongitudinalKiPerSecond; + + /// + /// 纵向速度外环微分增益,单位为s。 + /// + public double LongitudinalKdSeconds; + + /// + /// 纵向积分项允许产生的最大速度修正绝对值,单位为m/s。 + /// + public double MaximumIntegralCorrectionMetersPerSecond = 0.05; + + /// + /// 纵向PID不进行反馈修正的速度误差死区,单位为m/s。 + /// + public double LongitudinalSpeedErrorDeadbandMetersPerSecond = + 0.025; + + /// + /// 底盘纵向命令速度绝对值上限,单位为m/s。 + /// + public double MaximumCommandSpeedMetersPerSecond = 0.50; + + /// + /// 前后GCP允许的最大转角绝对值,单位为rad。 + /// + public double MaximumGcpAngleRadians = + AngleMath.DegreesToRadians(45.0); + + /// + /// 前后GCP目标转角最大变化率,单位为rad/s。 + /// + public double MaximumGcpAngleRateRadiansPerSecond = + AngleMath.DegreesToRadians(15.0); + + /// + /// 终点位置和剩余弧长的完成容差,单位为m。 + /// + public double FinishDistanceMeters = 0.03; + + /// + /// 终点停稳判定允许的实际线速度,单位为m/s。 + /// + public double FinishSpeedMetersPerSecond = 0.02; + + /// + /// 终点航向完成容差,单位为rad。 + /// + public double FinishHeadingToleranceRadians = + AngleMath.DegreesToRadians(3.0); + + /// + /// 车辆允许偏离参考轨迹的最大欧氏距离,单位为m。 + /// + public double MaximumDistanceToTrajectoryMeters = 0.30; + + /// + /// 单次轨迹动作允许的最长执行时间,单位为s。 + /// + public double ExecutionTimeoutSeconds = 120.0; + + /// + /// 获取本次动作创建的控制器,尚未开始时为空。 + /// + public ParkingGeometricController Controller { get; private set; } + + /// + /// 创建控制器并持续执行控制周期,直到轨迹完成、失败或动作被取消。 + /// + public override IEnumerable Get() + { + ValidateParameters(); + + var chassis = + PilotDefinition.Chassis as MultiWheelChassis; + if (chassis == null) + { + throw new InvalidOperationException( + "当前底盘不是MultiWheelChassis,无法执行新版轨迹跟踪动作。"); + } + + var adapter = new MultiWheelChassisAdapter( + chassis, + PilotDefinition.Self.CarNum); + + // 新版GCP控制统一以真实车头为车体X正方向,避免继承上一次蟹行偏置。 + adapter.ResetToBodyFrame(); + + var stateProvider = + StateProvider ?? + new DetourVehicleStateProvider(); + var controlPointRadiusMeters = + chassis.ControlPointRadius / 1000.0; + + var lateralController = + new StanleyLateralController( + controlPointRadiusMeters, + StanleyCrossTrackGainPerSecond, + StanleyHeadingErrorGain, + StanleyMinimumSpeedMetersPerSecond, + StanleyUsesActualSpeed, + MaximumCrossTrackCorrectionRadians, + MaximumHeadingCorrectionRadians); + var longitudinalController = + new PidLongitudinalController( + LongitudinalKp, + LongitudinalKiPerSecond, + LongitudinalKdSeconds, + MaximumIntegralCorrectionMetersPerSecond, + MaximumCommandSpeedMetersPerSecond, + LongitudinalSpeedErrorDeadbandMetersPerSecond); + var gcpAllocator = + new GcpCommandAllocator( + MaximumGcpAngleRadians); + var commandExecutor = + new GcpCommandExecutor( + adapter, + MaximumGcpAngleRateRadiansPerSecond); + + Controller = new ParkingGeometricController( + stateProvider, + lateralController, + longitudinalController, + gcpAllocator, + commandExecutor, + FinishDistanceMeters, + FinishSpeedMetersPerSecond, + FinishHeadingToleranceRadians, + MaximumDistanceToTrajectoryMeters); + + var clock = Stopwatch.StartNew(); + var previousCycleSeconds = + clock.Elapsed.TotalSeconds; + Controller.Start(Trajectory); + + try + { + while (true) + { + if (clock.Elapsed.TotalSeconds > + ExecutionTimeoutSeconds) + { + throw new TimeoutException( + $"新版轨迹跟踪超过{ExecutionTimeoutSeconds:F1}s仍未完成。"); + } + + var currentCycleSeconds = + clock.Elapsed.TotalSeconds; + var deltaTimeSeconds = + currentCycleSeconds - + previousCycleSeconds; + previousCycleSeconds = + currentCycleSeconds; + + // 极短首周期不参与PID和GCP角速度限制,等待调度器进入下一周期。 + if (deltaTimeSeconds <= 1e-6) + { + yield return true; + continue; + } + + var result = + Controller.ExecuteCycle( + deltaTimeSeconds); + + if (Controller.LastVehicleState.HasValue) + { + CycleObserver?.Invoke(Controller); + } + + if (result == + ParkingControlCycleResult.Completed) + { + break; + } + + if (result == + ParkingControlCycleResult.Faulted) + { + throw new InvalidOperationException( + string.IsNullOrWhiteSpace( + Controller.LastFailureReason) + ? "新版轨迹跟踪控制器发生未知故障。" + : Controller.LastFailureReason, + Controller.LastException); + } + + if (result == + ParkingControlCycleResult.Inactive) + { + throw new InvalidOperationException( + "新版轨迹跟踪控制器在轨迹完成前意外停止活动。"); + } + + // CommandSent和短暂StateUnavailable均继续下一控制周期; + // 后者已经由控制器主动停车,等待Detour恢复。 + yield return true; + } + } + finally + { + Controller.Cancel(); + } + + yield return false; + } + + /// + /// 在接管实际底盘前检查动作自身无法由子控制器检查的参数。 + /// + private void ValidateParameters() + { + if (Trajectory == null) + { + throw new InvalidOperationException( + "新版轨迹跟踪动作没有设置Trajectory。"); + } + + if (double.IsNaN(ExecutionTimeoutSeconds) || + double.IsInfinity(ExecutionTimeoutSeconds) || + ExecutionTimeoutSeconds <= 0.0) + { + throw new ArgumentOutOfRangeException( + nameof(ExecutionTimeoutSeconds), + "轨迹跟踪超时时间必须是正有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs b/ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs new file mode 100644 index 0000000..5d2fbe2 --- /dev/null +++ b/ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs @@ -0,0 +1,576 @@ +// 将统一命令转换为原 Chassis API 调用 +using System; +using CommonUsage.Chassis; + +namespace MyParking.Shared +{ + /// + /// 将统一的单车车体速度命令转换为旧版MultiWheelChassis调用。 + /// 车体坐标系固定为X向前、Y向左、逆时针为正。 + /// + public sealed class MultiWheelChassisAdapter + { + #region 辅助内容 + private const double RadiansToDegrees = 180.0 / Math.PI; + private const float BiasTolerance = 0.001f; + private readonly MultiWheelChassis _chassis; + /// + /// 当前适配器对应的车辆编号。 + /// + public int VehicleId { get; } + + /// + /// Maximum distance from the body origin to a wheel center, in metres. + /// + public double MaximumWheelRadiusMeters { get; } + + /// + /// Maximum longitudinal wheel offset from the body origin, in metres. + /// For a symmetric four-wheel-steering chassis this is half the wheelbase. + /// + public double HalfWheelBaseMeters { get; } + + /// + /// 车体原点到最外侧舵轮中心的最大横向距离,单位为米。 + /// 对称四舵轮底盘中,它也是蟹行虚拟阿克曼模型的半轴距。 + /// + public double HalfTrackWidthMeters { get; } + + /// + /// Width of the steering-alignment speed gate, in degrees. + /// + public double SteeringAlignmentSigmaDegrees + { + get => _chassis.SteeringAlignmentSigmaDegrees; + set + { + if (double.IsNaN(value) || + double.IsInfinity(value) || + value <= 0.0 || + value > float.MaxValue) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "Steering alignment sigma must be a positive finite value."); + } + + _chassis.SteeringAlignmentSigmaDegrees = + (float)value; + } + } + + /// + /// 检查旧底盘是否仍处于无偏置的真实车体坐标系。 + /// + private void EnsureBodyFrameIsActive() + { + EnsureMotionFrameIsActive(0.0); + } + + /// + /// 检查旧底盘当前是否处于指定的运动坐标系。 + /// motionDirectionRadians表示该运动系X轴在真实车体坐标系中的方向。 + /// + private void EnsureMotionFrameIsActive( + double motionDirectionRadians) + { + ValidateFinite( + motionDirectionRadians, + nameof(motionDirectionRadians)); + + var expectedBiasDegrees = + (float)( + -FrameTransform2D.NormalizeAngle( + motionDirectionRadians) * + RadiansToDegrees); + var bias = _chassis.GetOriginBias(); + var angleErrorDegrees = + NormalizeDegrees( + bias.Z - expectedBiasDegrees); + + if (Math.Abs(bias.X) <= BiasTolerance && + Math.Abs(bias.Y) <= BiasTolerance && + Math.Abs(angleErrorDegrees) <= + BiasTolerance) + { + return; + } + + throw new InvalidOperationException( + "MultiWheelChassis当前运动坐标系与命令不一致。" + + $"当前偏置为X={bias.X}, Y={bias.Y}, Th={bias.Z}°," + + $"期望Th={expectedBiasDegrees}°。"); + } + + /// + /// 将角度归一化到[-180°,180°]附近。 + /// + private static float NormalizeDegrees(float degrees) + { + return (float)( + degrees - + Math.Round(degrees / 360.0) * 360.0); + } + /// + /// 检查底盘命令是否包含无效数值。 + /// + private static void ValidateTwist(Twist2D twist) + { + ValidateFinite( + twist.VxMetersPerSecond, + nameof(twist.VxMetersPerSecond)); + + ValidateFinite( + twist.VyMetersPerSecond, + nameof(twist.VyMetersPerSecond)); + + ValidateFinite( + twist.OmegaRadiansPerSecond, + nameof(twist.OmegaRadiansPerSecond)); + } + /// + /// 检查数值是否为有限值。 + /// + private static void ValidateFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "底盘速度命令不能是NaN或无穷大。"); + } + + if (value > float.MaxValue || + value < -float.MaxValue) + { + throw new ArgumentOutOfRangeException( + parameterName, + "底盘速度命令超过float可表示范围。"); + } + } + + /// + /// 获取最近一次底盘运动分解失败原因。 + /// + public string LastFailureReason => + _chassis.LastMotionDecomposeFailureReason; + + #endregion + + /// + /// 将旧底盘的原点偏置恢复为真实单车车体坐标系。 + /// + public void ResetToBodyFrame() + { + ActivateMotionFrame(0.0); + } + + /// + /// 激活指定运动方向对应的SendMotion坐标系。 + /// 0表示真实车头,正90度表示将车体左侧作为虚拟车头。 + /// + public void ActivateMotionFrame( + double motionDirectionRadians) + { + ValidateFinite( + motionDirectionRadians, + nameof(motionDirectionRadians)); + + var biasDegrees = + (float)( + -FrameTransform2D.NormalizeAngle( + motionDirectionRadians) * + RadiansToDegrees); + var currentBias = + _chassis.GetOriginBias(); + + if (Math.Abs(currentBias.X) <= + BiasTolerance && + Math.Abs(currentBias.Y) <= + BiasTolerance && + Math.Abs( + NormalizeDegrees( + currentBias.Z - + biasDegrees)) <= + BiasTolerance) + { + return; + } + + _chassis.SetOriginBias( + x: 0.0f, + y: 0.0f, + th: biasDegrees); + } + public MultiWheelChassisAdapter(MultiWheelChassis chassis, int vehicleId) + { + _chassis = chassis ?? throw new ArgumentNullException(nameof(chassis)); + if (vehicleId <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(vehicleId), + "车辆编号必须大于零。"); + } + VehicleId = vehicleId; +#pragma warning disable CS0612, CS0618 + var wheels = _chassis.GetSteerWheels(); +#pragma warning restore CS0612, CS0618 + + if (wheels.Count == 0) + { + throw new InvalidOperationException( + "MultiWheelChassis尚未完成舵轮初始化," + + "不能创建底盘适配器。"); + } + // 禁用旧版DirectionAngle/ZeroDirection坐标偏置, + // 保证SendXYThSpeed直接使用真实车体坐标系。 + var maximumWheelRadiusMillimeters = 0.0; + var maximumLongitudinalOffsetMillimeters = 0.0; + var maximumLateralOffsetMillimeters = 0.0; + foreach (var wheel in wheels) + { + maximumWheelRadiusMillimeters = Math.Max( + maximumWheelRadiusMillimeters, + wheel.PhysicalPosition.Length()); + + maximumLongitudinalOffsetMillimeters = Math.Max( + maximumLongitudinalOffsetMillimeters, + Math.Abs(wheel.PhysicalPosition.X)); + + maximumLateralOffsetMillimeters = Math.Max( + maximumLateralOffsetMillimeters, + Math.Abs(wheel.PhysicalPosition.Y)); + } + + MaximumWheelRadiusMeters = + maximumWheelRadiusMillimeters / 1000.0; + HalfWheelBaseMeters = + maximumLongitudinalOffsetMillimeters / 1000.0; + HalfTrackWidthMeters = + maximumLateralOffsetMillimeters / 1000.0; + + if (MaximumWheelRadiusMeters <= 0.0 || + HalfWheelBaseMeters <= 0.0 || + HalfTrackWidthMeters <= 0.0) + { + throw new InvalidOperationException( + "Wheel positions cannot produce valid chassis dimensions."); + } + + // 通过反转轮速表达反向运动,避免蟹行正反切换时舵轮无意义地旋转180°。 + _chassis.PreferMinimumSteeringTravel = true; + } + + /// + /// 将车体坐标系速度命令发送给多舵轮底盘。 + /// + public bool Send(ChassisCommand command, TimeSpan? interval = null) + { + if (command.VehicleId != VehicleId) + { + throw new InvalidOperationException( + $"命令车辆编号{command.VehicleId}与适配器车辆编号" + + $"{VehicleId}不一致。"); + } + ValidateTwist(command.BodyTwist); + // 防止其他旧逻辑再次调用DirectionAngle或 + // SetOriginBias改变底盘坐标语义。 + EnsureBodyFrameIsActive(); + var vxMetersPerSecond = + (float)command.BodyTwist.VxMetersPerSecond; + var vyMetersPerSecond = + (float)command.BodyTwist.VyMetersPerSecond; + var omegaDegreesPerSecond = + (float)( + command.BodyTwist.OmegaRadiansPerSecond * + RadiansToDegrees); + var success = _chassis.SendXYThSpeed( + vxMetersPerSecond, + vyMetersPerSecond, + omegaDegreesPerSecond, + interval, + enableDifferentialSteerFeedforward: true); + if (!success) + { + // 防止分解失败后继续执行上一条运动命令。 + _chassis.PredefinedDriveStop(); + } + return success; + } + + + /// + /// 在已经激活的运动坐标系中使用SendMotion执行虚拟阿克曼运动。 + /// 转向角均相对该运动坐标系表达;正90度运动系对应车体左侧蟹行。 + /// + public bool SendVirtualAckermannMotion( + double motionDirectionRadians, + double speedMetersPerSecond, + double steeringRadians, + TimeSpan? interval = null) + { + ValidateFinite( + motionDirectionRadians, + nameof(motionDirectionRadians)); + ValidateFinite( + speedMetersPerSecond, + nameof(speedMetersPerSecond)); + ValidateFinite( + steeringRadians, + nameof(steeringRadians)); + EnsureMotionFrameIsActive( + motionDirectionRadians); + + if (Math.Abs(steeringRadians) >= + Math.PI / 2.0) + { + throw new ArgumentOutOfRangeException( + nameof(steeringRadians), + "虚拟阿克曼转向角必须位于正负90度以内。"); + } + + var steeringDegrees = + (float)( + steeringRadians * + RadiansToDegrees); + var success = + _chassis.SendMotion( + (float)speedMetersPerSecond, + steeringDegrees, + -steeringDegrees, + interval); + + if (!success) + { + _chassis.PredefinedDriveStop(); + } + + return success; + } + + /// + /// 在真实车体坐标系中将有符号速度和独立前后GCP角度发送给旧版SendMotion。 + /// + public bool SendGcpMotion( + double speedMetersPerSecond, + double frontAngleRadians, + double rearAngleRadians, + TimeSpan? interval = null) + { + ValidateFinite( + speedMetersPerSecond, + nameof(speedMetersPerSecond)); + ValidateFinite( + frontAngleRadians, + nameof(frontAngleRadians)); + ValidateFinite( + rearAngleRadians, + nameof(rearAngleRadians)); + EnsureBodyFrameIsActive(); + + if (Math.Abs(frontAngleRadians) >= + Math.PI / 2.0 || + Math.Abs(rearAngleRadians) >= + Math.PI / 2.0) + { + throw new ArgumentOutOfRangeException( + nameof(frontAngleRadians), + "前后GCP角度必须位于正负90度以内,避免四轮几何解算出现奇异值。"); + } + + var success = _chassis.SendMotion( + (float)speedMetersPerSecond, + (float)(frontAngleRadians * + RadiansToDegrees), + (float)(rearAngleRadians * + RadiansToDegrees), + interval); + + if (!success) + { + // 分解失败后立即清除上一条驱动速度,避免车辆继续执行陈旧命令。 + _chassis.PredefinedDriveStop(); + } + + return success; + } + + /// + /// 立即将所有驱动轮速度下发为零。 + /// + public void StopImmediately() + { + _chassis.PredefinedDriveStop(); + } + + /// + /// 清零XYTh驱动速度,但保留已经准备好的自转舵角和轮速方向。 + /// + public void StopXYThDrivePreserveSteeringState() + { + _chassis.StopXYThDrivePreserveSteeringState(); + } + + /// + /// 停车并将所有舵轮转到指定的车体角度。 + /// 只调整舵轮角度,不产生车辆线速度。 + /// + public bool PrepareParallelDirection( + double directionRadians) + { + EnsureBodyFrameIsActive(); + var targetDegrees = (float)(FrameTransform2D.NormalizeAngle(directionRadians) * + RadiansToDegrees); + +#pragma warning disable CS0612, CS0618 + var wheels = _chassis.GetSteerWheels(); +#pragma warning restore CS0612, CS0618 + + // 没有舵轮时不能认为预对齐成功。 + if (wheels.Count == 0) + { + return false; + } + + // 先检查所有舵轮能否到达目标机械角度。 + foreach (var wheel in wheels) + { + if (targetDegrees < wheel.AngleLowerLimit || + targetDegrees > wheel.AngleUpperLimit) + { + return false; + } + } + + // 模式切换前立即停止驱动轮。 + _chassis.PredefinedDriveStop(); + // 检查完成后再统一下发,避免只转动一部分舵轮。 + foreach (var wheel in wheels) + { + wheel.WriteAngle(targetDegrees); + } + + return true; + } + + /// + /// 检查所有舵轮是否已经对准给定方向。 + /// + public bool AreParallelWheelsAligned( + double directionRadians, + double toleranceRadians) + { + if (double.IsNaN(toleranceRadians) || + double.IsInfinity(toleranceRadians) || + toleranceRadians < 0.0) + { + throw new ArgumentOutOfRangeException( + nameof(toleranceRadians), + "舵轮到位容差必须是非负有限值。"); + } + + EnsureBodyFrameIsActive(); + var targetDegrees = (float)( + FrameTransform2D.NormalizeAngle(directionRadians) * + 180.0 / Math.PI); + + var toleranceDegrees = (float)( + Math.Abs(toleranceRadians) * + 180.0 / Math.PI); + +#pragma warning disable CS0612, CS0618 + var wheels = _chassis.GetSteerWheels(); +#pragma warning restore CS0612, CS0618 + + foreach (var wheel in wheels) + { + var angleErrorDegrees = targetDegrees - wheel.ReadAngle(); + + if (Math.Abs(angleErrorDegrees) > + toleranceDegrees) + { + return false; + } + } + + return true; + } + + /// + /// 停车并将舵轮预对齐到原地自转方向。 + /// 返回是否成功生成舵轮目标。 + /// + public bool PrepareSpin( + TimeSpan? interval = null, + double alignmentToleranceDegrees = 2.0) + { + ValidateFinite( + alignmentToleranceDegrees, + nameof(alignmentToleranceDegrees)); + + if (alignmentToleranceDegrees < 0.0) + { + throw new ArgumentOutOfRangeException( + nameof(alignmentToleranceDegrees), + "自转舵轮到位容差必须是非负有限值。"); + } + + EnsureBodyFrameIsActive(); + + var success = + _chassis.PrepareRotateWheels( + alignmentToleranceDegrees: + (float)alignmentToleranceDegrees); + + if (!success) + { + _chassis.PredefinedDriveStop(); + } + return success; + } + + /// + /// 将已到位的自转舵角和轮速方向一次性交接给XYTh, + /// 防止普通SendXYThSpeed正式运动首帧重新初始化运动状态。 + /// + public bool AdoptPreparedSpinForXYTh( + double toleranceRadians = + 2.0 * Math.PI / 180.0) + { + if (double.IsNaN(toleranceRadians) || + double.IsInfinity(toleranceRadians) || + toleranceRadians < 0.0) + { + throw new ArgumentOutOfRangeException( + nameof(toleranceRadians), + "自转状态交接容差必须是非负有限值。"); + } + + EnsureBodyFrameIsActive(); + + var success = + _chassis + .AdoptPreparedRotateWheelsForXYTh( + (float)( + toleranceRadians * + RadiansToDegrees)); + + if (!success) + { + _chassis.PredefinedDriveStop(); + } + + return success; + } + /// + /// 所有舵轮是否已对齐到原地自转方向。 + /// + public bool AreSpinWheelsAligned => _chassis.LastRotateAligned; + + + + } +} diff --git a/ClumsyPilot/Shared/Mathematics/AngleMath.cs b/ClumsyPilot/Shared/Mathematics/AngleMath.cs new file mode 100644 index 0000000..ca3803f --- /dev/null +++ b/ClumsyPilot/Shared/Mathematics/AngleMath.cs @@ -0,0 +1,138 @@ +using System; + +namespace MyParking.Shared +{ + /// + /// 提供与坐标系无关的角度归一化、角度差和单位转换功能。 + /// + public static class AngleMath + { + public const double TwoPi = 2.0 * Math.PI; + + /// + /// 将弧度归一化到[-π, π)区间。 + /// -π包含在结果中,+π不包含在结果中,因此+π会返回-π。 + /// + public static double NormalizeRadians(double angleRadians) + { + EnsureFinite(angleRadians, nameof(angleRadians)); + + var normalized = angleRadians % TwoPi; + + if (normalized >= Math.PI) + { + normalized -= TwoPi; + } + else if (normalized < -Math.PI) + { + normalized += TwoPi; + } + + return normalized == 0.0 ? 0.0 : normalized; + } + + /// + /// 将角度归一化到[-180°, 180°)区间。 + /// -180°包含在结果中,+180°不包含在结果中,因此+180°会返回-180°。 + /// + public static double NormalizeDegrees(double angleDegrees) + { + EnsureFinite(angleDegrees, nameof(angleDegrees)); + + var normalized = angleDegrees % 360.0; + + if (normalized >= 180.0) + { + normalized -= 360.0; + } + else if (normalized < -180.0) + { + normalized += 360.0; + } + + return normalized == 0.0 ? 0.0 : normalized; + } + + /// + /// 计算从当前方向旋转到目标方向的最短有符号角度差,单位为弧度。 + /// 返回值位于[-π, π);正值表示逆时针,负值表示顺时针。 + /// + public static double ShortestDifferenceRadians( + double targetRadians, + double currentRadians) + { + EnsureFinite(targetRadians, nameof(targetRadians)); + EnsureFinite(currentRadians, nameof(currentRadians)); + + return NormalizeRadians(targetRadians - currentRadians); + } + + /// + /// 计算从当前方向旋转到目标方向的最短有符号角度差,单位为度。 + /// 返回值位于[-180°, 180°);正值表示逆时针,负值表示顺时针。 + /// + public static double ShortestDifferenceDegrees( + double targetDegrees, + double currentDegrees) + { + EnsureFinite(targetDegrees, nameof(targetDegrees)); + EnsureFinite(currentDegrees, nameof(currentDegrees)); + + return NormalizeDegrees(targetDegrees - currentDegrees); + } + + /// + /// 沿圆周最短方向在两个航向角之间插值,输入和结果单位均为弧度。 + /// ratio为0时返回起始角,ratio为1时返回终止角;本方法不限制ratio, + /// 轨迹线段内插值时应先使用InterpolationMath.Clamp01进行限制。 + /// 结果归一化到[-π, π)区间;角度差恰好为π时按负方向插值。 + /// + public static double LerpRadians( + double startRadians, + double endRadians, + double ratio) + { + EnsureFinite(startRadians, nameof(startRadians)); + EnsureFinite(endRadians, nameof(endRadians)); + EnsureFinite(ratio, nameof(ratio)); + + var shortestDifference = ShortestDifferenceRadians( + endRadians, + startRadians); + + return NormalizeRadians( + startRadians + ratio * shortestDifference); + } + + /// + /// 将角度从度转换为弧度,不进行归一化。 + /// + public static double DegreesToRadians(double angleDegrees) + { + EnsureFinite(angleDegrees, nameof(angleDegrees)); + return angleDegrees * Math.PI / 180.0; + } + + /// + /// 将角度从弧度转换为度,不进行归一化。 + /// + public static double RadiansToDegrees(double angleRadians) + { + EnsureFinite(angleRadians, nameof(angleRadians)); + return angleRadians * 180.0 / Math.PI; + } + + /// + /// 验证角度是可用于计算的有限数值。 + /// + private static void EnsureFinite(double angle, string parameterName) + { + if (double.IsNaN(angle) || double.IsInfinity(angle)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "角度必须是有限数值。"); + } + } + } +} diff --git a/ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs b/ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs new file mode 100644 index 0000000..58e33db --- /dev/null +++ b/ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs @@ -0,0 +1,165 @@ +// 车体、运动、车队坐标系之间的转换 +using System; + +namespace MyParking.Shared +{ + /// + /// 提供二维刚体坐标系之间的点、向量、位姿和速度变换。 + /// 坐标系采用X向前、Y向左、逆时针为正的右手系。 + /// + public static class FrameTransform2D + { + /// + /// 将角度归一化到[-π, π)范围。 + /// + public static double NormalizeAngle(double angleRadians) + { + return AngleMath.NormalizeRadians(angleRadians); + } + + /// + /// 计算从current到target的最短角度差。 + /// 返回正值表示逆时针旋转。 + /// + public static double ShortestAngleDifference( + double targetRadians, + double currentRadians) + { + return AngleMath.ShortestDifferenceRadians( + targetRadians, + currentRadians); + } + + /// + /// 将源坐标系中的点变换到目标坐标系。 + /// sourcePoseInTarget表示源坐标系在目标坐标系中的位姿。 + /// + public static Point2D TransformPoint( + Pose2D sourcePoseInTarget, + Point2D pointInSource) + { + var cos = Math.Cos(sourcePoseInTarget.YawRadians); + var sin = Math.Sin(sourcePoseInTarget.YawRadians); + + return new Point2D( + sourcePoseInTarget.XMeters + + cos * pointInSource.XMeters - + sin * pointInSource.YMeters, + + sourcePoseInTarget.YMeters + + sin * pointInSource.XMeters + + cos * pointInSource.YMeters); + } + + /// + /// 将目标坐标系中的点反向变换到源坐标系。 + /// + public static Point2D InverseTransformPoint( + Pose2D sourcePoseInTarget, + Point2D pointInTarget) + { + var dx = pointInTarget.XMeters - sourcePoseInTarget.XMeters; + + var dy = pointInTarget.YMeters - sourcePoseInTarget.YMeters; + + var cos = Math.Cos(sourcePoseInTarget.YawRadians); + var sin = Math.Sin(sourcePoseInTarget.YawRadians); + + return new Point2D( + cos * dx + sin * dy, + -sin * dx + cos * dy); + } + + /// + /// 将源坐标系中的向量旋转到目标坐标系。 + /// 向量没有位置,因此不叠加平移量。 + /// + public static Point2D TransformVector( + Pose2D sourcePoseInTarget, + Point2D vectorInSource) + { + var cos = Math.Cos(sourcePoseInTarget.YawRadians); + var sin = Math.Sin(sourcePoseInTarget.YawRadians); + + return new Point2D( + cos * vectorInSource.XMeters - + sin * vectorInSource.YMeters, + + sin * vectorInSource.XMeters + + cos * vectorInSource.YMeters); + } + + /// + /// 组合两级坐标变换。 + /// parentFromMiddle表示middle在parent中的位姿; + /// middleFromChild表示child在middle中的位姿; + /// 返回child在parent中的位姿。 + /// + public static Pose2D Compose( + Pose2D parentFromMiddle, + Pose2D middleFromChild) + { + var childPositionInParent = TransformPoint( + parentFromMiddle, + middleFromChild.Position); + + return new Pose2D( + childPositionInParent.XMeters, + childPositionInParent.YMeters, + NormalizeAngle( + parentFromMiddle.YawRadians + + middleFromChild.YawRadians)); + } + + /// + /// 对坐标变换求逆。 + /// 输入child在parent中的位姿,返回parent在child中的位姿。 + /// + public static Pose2D Inverse(Pose2D childPoseInParent) + { + var cos = Math.Cos(childPoseInParent.YawRadians); + var sin = Math.Sin(childPoseInParent.YawRadians); + + return new Pose2D( + -cos * childPoseInParent.XMeters - + sin * childPoseInParent.YMeters, + + sin * childPoseInParent.XMeters - + cos * childPoseInParent.YMeters, + + NormalizeAngle( + -childPoseInParent.YawRadians)); + } + + + /// + /// 将源坐标系中的位姿变换到目标坐标系。 + /// + public static Pose2D TransformPose( + Pose2D sourcePoseInTarget, + Pose2D poseInSource) + { + return Compose(sourcePoseInTarget, poseInSource); + } + + /// + /// 转换同一物理参考点处的速度表达坐标系。 + /// 只旋转线速度,角速度保持不变。 + /// + public static Twist2D TransformTwistAtSamePoint( + Pose2D sourcePoseInTarget, + Twist2D twistInSource) + { + var linearVelocityInTarget = TransformVector( + sourcePoseInTarget, + new Point2D( + twistInSource.VxMetersPerSecond, + twistInSource.VyMetersPerSecond)); + + return new Twist2D( + linearVelocityInTarget.XMeters, + linearVelocityInTarget.YMeters, + twistInSource.OmegaRadiansPerSecond); + } + } +} diff --git a/ClumsyPilot/Shared/Mathematics/InterpolationMath.cs b/ClumsyPilot/Shared/Mathematics/InterpolationMath.cs new file mode 100644 index 0000000..61db6e3 --- /dev/null +++ b/ClumsyPilot/Shared/Mathematics/InterpolationMath.cs @@ -0,0 +1,61 @@ + +using System; + +namespace MyParking.Shared +{ + /// + /// 提供与具体业务和坐标系无关的基础插值功能。 + /// + public static class InterpolationMath + { + /// + /// 将插值比例限制到[0, 1]闭区间。 + /// + public static double Clamp01(double value) + { + EnsureFinite(value, nameof(value)); + + if (value <= 0.0) + { + return 0.0; + } + + if (value >= 1.0) + { + return 1.0; + } + + return value; + } + + /// + /// 对两个标量执行线性插值。 + /// ratio为0时返回start,ratio为1时返回end;本方法不限制ratio, + /// 因此也支持区间外的线性外插。 + /// + public static double Lerp( + double start, + double end, + double ratio) + { + EnsureFinite(start, nameof(start)); + EnsureFinite(end, nameof(end)); + EnsureFinite(ratio, nameof(ratio)); + + return start + ratio * (end - start); + } + + /// + /// 验证输入是可用于插值计算的有限数值。 + /// + private static void EnsureFinite(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "插值参数必须是有限数值。"); + } + } + } +} diff --git a/ClumsyPilot/Shared/Models/ChassisCommand.cs b/ClumsyPilot/Shared/Models/ChassisCommand.cs new file mode 100644 index 0000000..73545dd --- /dev/null +++ b/ClumsyPilot/Shared/Models/ChassisCommand.cs @@ -0,0 +1,183 @@ +// 纯数据层:只描述坐标、速度和命令 +// 定义二维坐标、位姿、速度、车队布局和单车底盘命令。 +// Shared层统一使用SI单位:位置m、线速度m/s、角度rad、角速度rad/s。 +// 车体坐标系采用右手系:X向前、Y向左、逆时针角度和角速度为正。 +// 命名约定:XxxInYyy表示Xxx在Yyy坐标系中的表达。 + +namespace MyParking.Shared +{ + /// + /// 二维坐标点,X、Y单位均为米。 + /// + public readonly struct Point2D + { + public Point2D(double xMeters, double yMeters) + { + XMeters = xMeters; + YMeters = yMeters; + } + + public double XMeters { get; } + + public double YMeters { get; } + + public static Point2D Zero => new Point2D(0.0, 0.0); + } + + /// + /// 二维局部坐标系在父坐标系中的位姿。 + /// 位置单位为米,朝向单位为弧度,逆时针为正。 + /// 具体父子关系由变量名称说明,例如RadarPoseInBody。 + /// + public readonly struct Pose2D + { + public Pose2D( + double xMeters, + double yMeters, + double yawRadians) + { + XMeters = xMeters; + YMeters = yMeters; + YawRadians = yawRadians; + } + + public double XMeters { get; } + + public double YMeters { get; } + + public double YawRadians { get; } + + public Point2D Position => + new Point2D(XMeters, YMeters); + + public static Pose2D Identity => + new Pose2D(0.0, 0.0, 0.0); + } + + /// + /// 二维刚体速度。 + /// 线速度单位为m/s,角速度单位为rad/s。 + /// 速度所属坐标系由持有该Twist2D的外层类型或变量名称确定。 + /// + public readonly struct Twist2D + { + public Twist2D( + double vxMetersPerSecond, + double vyMetersPerSecond, + double omegaRadiansPerSecond) + { + VxMetersPerSecond = vxMetersPerSecond; + VyMetersPerSecond = vyMetersPerSecond; + OmegaRadiansPerSecond = omegaRadiansPerSecond; + } + + public double VxMetersPerSecond { get; } + + public double VyMetersPerSecond { get; } + + public double OmegaRadiansPerSecond { get; } + + public static Twist2D Zero => + new Twist2D(0.0, 0.0, 0.0); + } + + /// + /// 发送给单辆车的车体坐标系速度命令。 + /// + public readonly struct ChassisCommand + { + public ChassisCommand( + int vehicleId, + Twist2D bodyTwist) + { + VehicleId = vehicleId; + BodyTwist = bodyTwist; + } + + public int VehicleId { get; } + + /// + /// 单车车体坐标系速度:X向前、Y向左、逆时针旋转为正。 + /// + public Twist2D BodyTwist { get; } + + /// + /// 创建指定车辆的停止命令。 + /// + public static ChassisCommand Stop(int vehicleId) + { + return new ChassisCommand( + vehicleId, + Twist2D.Zero); + } + } + + /// + /// 单辆车的车体坐标系在车队坐标系中的位姿。 + /// + public readonly struct VehicleLayout + { + public VehicleLayout( + int vehicleId, + Pose2D poseInFleet) + { + VehicleId = vehicleId; + PoseInFleet = poseInFleet; + } + + public int VehicleId { get; } + + public Pose2D PoseInFleet { get; } + } + + /// + /// 车队整体运动命令,速度分量均在车队坐标系中表达。 + /// + public readonly struct FleetMotionCommand + { + public FleetMotionCommand( + Point2D referencePointInFleet, + Twist2D twistAtReferencePoint) + { + ReferencePointInFleet = referencePointInFleet; + TwistAtReferencePoint = twistAtReferencePoint; + } + + /// + /// 速度命令对应的参考点,也可作为自定义旋转中心。 + /// + public Point2D ReferencePointInFleet { get; } + + /// + /// 参考点处的车队速度。 + /// + public Twist2D TwistAtReferencePoint { get; } + + /// + /// 创建绕指定中心原地旋转的车队命令。 + /// + public static FleetMotionCommand RotateAround( + Point2D rotationCenterInFleet, + double omegaRadiansPerSecond) + { + return new FleetMotionCommand( + rotationCenterInFleet, + new Twist2D( + 0.0, + 0.0, + omegaRadiansPerSecond)); + } + + /// + /// 创建车队停止命令。 + /// + public static FleetMotionCommand Stop() + { + return new FleetMotionCommand( + Point2D.Zero, + Twist2D.Zero); + } + } + + +} diff --git a/ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs b/ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs new file mode 100644 index 0000000..4a07a9b --- /dev/null +++ b/ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs @@ -0,0 +1,509 @@ +using System; +using System.Diagnostics; +using ClumsyCore.Interfaces; +using MyParking.Shared; + +namespace MultiWheelC.StateEstimation +{ + /// + /// 读取Detour位姿,忽略重复或明显异常的观测,并估算车辆二维速度。 + /// + public sealed class DetourVehicleStateProvider + : IVehicleStateProvider + { + public const double DefaultMaximumLinearSpeedMetersPerSecond = + 1.20; + public const double DefaultMaximumAngularSpeedRadiansPerSecond = + Math.PI / 4.0; + public const double DefaultPositionJumpMarginMeters = + 0.03; + public const double DefaultHeadingJumpMarginRadians = + 5.0 * Math.PI / 180.0; + public const double DefaultVelocityPositionResidualMeters = + 0.04; + public const double DefaultVelocityHeadingResidualRadians = + 5.0 * Math.PI / 180.0; + public const double DefaultStationaryConfirmationSeconds = + 0.35; + + private const double MillimetersPerMeter = 1000.0; + private const double PositionEqualityToleranceMeters = 1e-9; + private const double HeadingEqualityToleranceRadians = 1e-8; + + private readonly object _syncRoot = new object(); + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private readonly VelocityEstimator2D _velocityEstimator; + private readonly double _maximumLinearSpeedMetersPerSecond; + private readonly double _maximumAngularSpeedRadiansPerSecond; + private readonly double _positionJumpMarginMeters; + private readonly double _headingJumpMarginRadians; + private readonly double _velocityPositionResidualMeters; + private readonly double _velocityHeadingResidualRadians; + private readonly double _stationaryConfirmationSeconds; + + private bool _hasAcceptedPose; + private Pose2D _acceptedPoseInWorld; + private double _acceptedTimestampSeconds; + private VehicleState _latestState; + private bool _stationaryHoldActive; + + /// + /// 创建使用停车机器人默认物理边界和速度滤波参数的Detour状态源。 + /// + public DetourVehicleStateProvider() + : this( + new VelocityEstimator2D(), + DefaultMaximumLinearSpeedMetersPerSecond, + DefaultMaximumAngularSpeedRadiansPerSecond, + DefaultPositionJumpMarginMeters, + DefaultHeadingJumpMarginRadians, + DefaultVelocityPositionResidualMeters, + DefaultVelocityHeadingResidualRadians, + DefaultStationaryConfirmationSeconds) + { + } + + /// + /// 创建使用指定物理边界、静止确认时间和速度估计器的Detour状态源。 + /// + public DetourVehicleStateProvider( + VelocityEstimator2D velocityEstimator, + double maximumLinearSpeedMetersPerSecond, + double maximumAngularSpeedRadiansPerSecond, + double positionJumpMarginMeters, + double headingJumpMarginRadians, + double velocityPositionResidualMeters, + double velocityHeadingResidualRadians, + double stationaryConfirmationSeconds) + { + _velocityEstimator = velocityEstimator ?? + throw new ArgumentNullException( + nameof(velocityEstimator)); + + EnsureFinitePositive( + maximumLinearSpeedMetersPerSecond, + nameof(maximumLinearSpeedMetersPerSecond)); + EnsureFinitePositive( + maximumAngularSpeedRadiansPerSecond, + nameof(maximumAngularSpeedRadiansPerSecond)); + EnsureFiniteNonNegative( + positionJumpMarginMeters, + nameof(positionJumpMarginMeters)); + EnsureFiniteNonNegative( + headingJumpMarginRadians, + nameof(headingJumpMarginRadians)); + EnsureFinitePositive( + velocityPositionResidualMeters, + nameof(velocityPositionResidualMeters)); + EnsureFinitePositive( + velocityHeadingResidualRadians, + nameof(velocityHeadingResidualRadians)); + EnsureFinitePositive( + stationaryConfirmationSeconds, + nameof(stationaryConfirmationSeconds)); + + _maximumLinearSpeedMetersPerSecond = + maximumLinearSpeedMetersPerSecond; + _maximumAngularSpeedRadiansPerSecond = + maximumAngularSpeedRadiansPerSecond; + _positionJumpMarginMeters = + positionJumpMarginMeters; + _headingJumpMarginRadians = + headingJumpMarginRadians; + _velocityPositionResidualMeters = + velocityPositionResidualMeters; + _velocityHeadingResidualRadians = + velocityHeadingResidualRadians; + _stationaryConfirmationSeconds = + stationaryConfirmationSeconds; + } + + /// + /// 获取最近一次读取失败或异常观测被忽略的原因,正常时为空字符串。 + /// + public string LastFailureReason { get; private set; } = ""; + + /// + /// 尝试读取Detour;重复帧保留最近状态,明显异常帧只忽略本次观测。 + /// + public bool TryGetState(out VehicleState state) + { + lock (_syncRoot) + { + try + { + var poseInWorld = + ReadDetourPoseInWorld(); + var timestampSeconds = + _clock.Elapsed.TotalSeconds; + + if (!_hasAcceptedPose) + { + state = AcceptPoseAfterReset( + poseInWorld, + timestampSeconds); + LastFailureReason = ""; + return true; + } + + if (ArePosesEquivalent( + poseInWorld, + _acceptedPoseInWorld)) + { + state = HandleRepeatedPose( + timestampSeconds); + LastFailureReason = ""; + return true; + } + + // 静止保持后出现新定位时重新建立差分基准, + // 避免用很长的静止时间稀释第一次运动速度。 + if (_stationaryHoldActive) + { + state = AcceptPoseAfterReset( + poseInWorld, + timestampSeconds); + LastFailureReason = ""; + return true; + } + + var elapsedSeconds = + timestampSeconds - + _acceptedTimestampSeconds; + + if (!IsMotionPlausible( + _acceptedPoseInWorld, + poseInWorld, + elapsedSeconds)) + { + // 单帧异常不进入差分器,也不中断调用方;下一次 + // 正常观测仍相对最近有效位姿和真实时间差计算。 + state = _latestState; + LastFailureReason = + "Detour位姿变化超过车辆绝对运动边界,本次观测已忽略。"; + return true; + } + + if (IsVelocityInnovationAbnormal( + poseInWorld, + elapsedSeconds)) + { + state = AcceptPoseAfterReset( + poseInWorld, + timestampSeconds); + LastFailureReason = + "Detour位姿偏离速度预测,已重新建立速度估计基准。"; + return true; + } + + state = AcceptContinuousPose( + poseInWorld, + timestampSeconds); + LastFailureReason = ""; + return true; + } + catch (Exception exception) + { + state = default; + LastFailureReason = + "Detour车辆状态读取失败:" + + exception.Message; + return false; + } + } + } + + /// + /// 清除Detour位姿历史和速度估计状态。 + /// + public void Reset() + { + lock (_syncRoot) + { + _velocityEstimator.Reset(); + _hasAcceptedPose = false; + _acceptedPoseInWorld = Pose2D.Identity; + _acceptedTimestampSeconds = 0.0; + _latestState = default; + _stationaryHoldActive = false; + LastFailureReason = ""; + } + } + + /// + /// 读取Detour毫米和角度数据并转换为世界坐标SI位姿。 + /// + private static Pose2D ReadDetourPoseInWorld() + { + var location = + DetourInterface.getCartLocation(); + + EnsureFinite(location.x, "DetourX"); + EnsureFinite(location.y, "DetourY"); + EnsureFinite(location.th, "DetourTheta"); + + return new Pose2D( + location.x / MillimetersPerMeter, + location.y / MillimetersPerMeter, + AngleMath.NormalizeRadians( + AngleMath.DegreesToRadians( + location.th))); + } + + /// + /// 接受连续有效定位并更新速度估计和差分基准。 + /// + private VehicleState AcceptContinuousPose( + Pose2D poseInWorld, + double timestampSeconds) + { + _latestState = + _velocityEstimator.Update( + poseInWorld, + timestampSeconds); + _acceptedPoseInWorld = poseInWorld; + _acceptedTimestampSeconds = + timestampSeconds; + _stationaryHoldActive = false; + return _latestState; + } + + /// + /// 接受跳变后的新位姿基准,但不让该位移进入速度差分和低通滤波器。 + /// + private VehicleState AcceptPoseAfterVelocityRebase( + Pose2D poseInWorld, + double timestampSeconds) + { + _latestState = + _velocityEstimator + .RebasePreservingVelocity( + poseInWorld, + timestampSeconds); + _acceptedPoseInWorld = poseInWorld; + _acceptedTimestampSeconds = + timestampSeconds; + _stationaryHoldActive = false; + return _latestState; + } + + /// + /// 接受首帧或静止后的首个新位姿并重新建立零速差分基准。 + /// + private VehicleState AcceptPoseAfterReset( + Pose2D poseInWorld, + double timestampSeconds) + { + _latestState = + _velocityEstimator.Reset( + poseInWorld, + timestampSeconds); + _acceptedPoseInWorld = poseInWorld; + _acceptedTimestampSeconds = + timestampSeconds; + _hasAcceptedPose = true; + _stationaryHoldActive = false; + return _latestState; + } + + /// + /// 对重复Detour观测保留最近状态,并在持续不变后将速度归零。 + /// + private VehicleState HandleRepeatedPose( + double timestampSeconds) + { + var unchangedSeconds = + timestampSeconds - + _acceptedTimestampSeconds; + + if (!_stationaryHoldActive && + unchangedSeconds >= + _stationaryConfirmationSeconds) + { + _latestState = + new VehicleState( + timestampSeconds, + _acceptedPoseInWorld, + Twist2D.Zero, + true); + _stationaryHoldActive = true; + } + + return _latestState; + } + + /// + /// 判断两次有效Detour观测之间的变化是否超过车辆绝对运动能力。 + /// + private bool IsMotionPlausible( + Pose2D startPoseInWorld, + Pose2D endPoseInWorld, + double deltaTimeSeconds) + { + if (!IsFinite(deltaTimeSeconds) || + deltaTimeSeconds <= 0.0) + { + return false; + } + + var deltaX = + endPoseInWorld.XMeters - + startPoseInWorld.XMeters; + var deltaY = + endPoseInWorld.YMeters - + startPoseInWorld.YMeters; + var displacementMeters = + Math.Sqrt( + deltaX * deltaX + + deltaY * deltaY); + var headingChangeRadians = + Math.Abs( + AngleMath.ShortestDifferenceRadians( + endPoseInWorld.YawRadians, + startPoseInWorld.YawRadians)); + + var maximumDisplacementMeters = + _maximumLinearSpeedMetersPerSecond * + deltaTimeSeconds + + _positionJumpMarginMeters; + var maximumHeadingChangeRadians = + _maximumAngularSpeedRadiansPerSecond * + deltaTimeSeconds + + _headingJumpMarginRadians; + + return displacementMeters <= + maximumDisplacementMeters && + headingChangeRadians <= + maximumHeadingChangeRadians; + } + + /// + /// 判断新位姿是否明显偏离上一滤波速度给出的恒速预测。 + /// + private bool IsVelocityInnovationAbnormal( + Pose2D poseInWorld, + double deltaTimeSeconds) + { + if (!_latestState.HasValidVelocityEstimate) + { + return false; + } + + var predictedX = + _acceptedPoseInWorld.XMeters + + _latestState.TwistInWorld + .VxMetersPerSecond * + deltaTimeSeconds; + var predictedY = + _acceptedPoseInWorld.YMeters + + _latestState.TwistInWorld + .VyMetersPerSecond * + deltaTimeSeconds; + var predictedYaw = + AngleMath.NormalizeRadians( + _acceptedPoseInWorld.YawRadians + + _latestState.TwistInWorld + .OmegaRadiansPerSecond * + deltaTimeSeconds); + + var positionResidualX = + poseInWorld.XMeters - predictedX; + var positionResidualY = + poseInWorld.YMeters - predictedY; + var positionResidualMeters = + Math.Sqrt( + positionResidualX * positionResidualX + + positionResidualY * positionResidualY); + var headingResidualRadians = + Math.Abs( + AngleMath.ShortestDifferenceRadians( + poseInWorld.YawRadians, + predictedYaw)); + + return positionResidualMeters > + _velocityPositionResidualMeters || + headingResidualRadians > + _velocityHeadingResidualRadians; + } + + /// + /// 判断两次读取是否为Detour保持输出的同一数值帧。 + /// + private static bool ArePosesEquivalent( + Pose2D firstPose, + Pose2D secondPose) + { + return Math.Abs( + firstPose.XMeters - + secondPose.XMeters) <= + PositionEqualityToleranceMeters && + Math.Abs( + firstPose.YMeters - + secondPose.YMeters) <= + PositionEqualityToleranceMeters && + Math.Abs( + AngleMath.ShortestDifferenceRadians( + firstPose.YawRadians, + secondPose.YawRadians)) <= + HeadingEqualityToleranceRadians; + } + + /// + /// 检查数值是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "状态源参数必须是正有限值。"); + } + } + + /// + /// 检查数值是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "状态源参数必须是非负有限值。"); + } + } + + /// + /// 检查数值是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (!IsFinite(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "状态源参数和Detour位姿必须是有限值。"); + } + } + + /// + /// 判断数值是否可用于状态估计。 + /// + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && + !double.IsInfinity(value); + } + } +} diff --git a/ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs b/ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs new file mode 100644 index 0000000..29464bc --- /dev/null +++ b/ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs @@ -0,0 +1,142 @@ +using System; + +namespace MultiWheelC.StateEstimation +{ + /// + /// 使用真实采样时间间隔对单个连续量执行在线一阶低通滤波。 + /// + public sealed class FirstOrderLowPassFilter + { + private readonly double _timeConstantSeconds; + private bool _isInitialized; + private double _value; + + /// + /// 创建使用指定时间常数的一阶低通滤波器。 + /// + public FirstOrderLowPassFilter( + double timeConstantSeconds) + { + EnsureFinitePositive( + timeConstantSeconds, + nameof(timeConstantSeconds)); + + _timeConstantSeconds = + timeConstantSeconds; + } + + /// + /// 获取滤波时间常数,单位为s;数值越大,滤波越强但响应越慢。 + /// + public double TimeConstantSeconds => + _timeConstantSeconds; + + /// + /// 获取滤波器是否已经接收过有效初值。 + /// + public bool IsInitialized => + _isInitialized; + + /// + /// 获取当前滤波输出;尚未初始化时读取会抛出异常。 + /// + public double Value + { + get + { + if (!_isInitialized) + { + throw new InvalidOperationException( + "一阶低通滤波器尚未初始化。"); + } + + return _value; + } + } + + /// + /// 使用当前输入和真实采样间隔更新滤波结果。 + /// + public double Update( + double input, + double deltaTimeSeconds) + { + EnsureFinite( + input, + nameof(input)); + EnsureFinitePositive( + deltaTimeSeconds, + nameof(deltaTimeSeconds)); + + if (!_isInitialized) + { + _value = input; + _isInitialized = true; + return _value; + } + + var alpha = + deltaTimeSeconds / + (_timeConstantSeconds + + deltaTimeSeconds); + + _value += alpha * (input - _value); + return _value; + } + + /// + /// 清除历史输出,使下一次有效输入直接成为新的初值。 + /// + public void Reset() + { + _value = 0.0; + _isInitialized = false; + } + + /// + /// 将滤波器立即重置到指定的有限初值。 + /// + public void Reset(double initialValue) + { + EnsureFinite( + initialValue, + nameof(initialValue)); + + _value = initialValue; + _isInitialized = true; + } + + /// + /// 检查数值是否为正有限值。 + /// + private static void EnsureFinitePositive( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value <= 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "滤波时间常数和采样间隔必须是正有限值。"); + } + } + + /// + /// 检查数值是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "滤波输入必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/StateEstimation/IVehicleStateProvider.cs b/ClumsyPilot/StateEstimation/IVehicleStateProvider.cs new file mode 100644 index 0000000..fcb25d8 --- /dev/null +++ b/ClumsyPilot/StateEstimation/IVehicleStateProvider.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.StateEstimation +{ + /// + /// 为轨迹控制器提供与具体定位来源无关的统一车辆状态读取接口。 + /// + public interface IVehicleStateProvider + { + /// + /// 尝试读取当前有效车辆状态;定位不可用或过期时返回false。 + /// + bool TryGetState(out VehicleState state); + } +} + diff --git a/ClumsyPilot/StateEstimation/VehicleState.cs b/ClumsyPilot/StateEstimation/VehicleState.cs new file mode 100644 index 0000000..af4814c --- /dev/null +++ b/ClumsyPilot/StateEstimation/VehicleState.cs @@ -0,0 +1,138 @@ +using System; +using MyParking.Shared; + +namespace MultiWheelC.StateEstimation +{ + /// + /// 保存一次经过校验的车辆位姿和速度估计快照,统一使用SI单位。 + /// + public readonly struct VehicleState + { + /// + /// 创建车辆状态,并将世界坐标速度同步转换到车体坐标系。 + /// + public VehicleState( + double sampleTimestampSeconds, + Pose2D poseInWorld, + Twist2D twistInWorld, + bool hasValidVelocityEstimate) + { + EnsureFiniteNonNegative( + sampleTimestampSeconds, + nameof(sampleTimestampSeconds)); + EnsureFinitePose( + poseInWorld, + nameof(poseInWorld)); + EnsureFiniteTwist( + twistInWorld, + nameof(twistInWorld)); + + SampleTimestampSeconds = + sampleTimestampSeconds; + PoseInWorld = new Pose2D( + poseInWorld.XMeters, + poseInWorld.YMeters, + AngleMath.NormalizeRadians( + poseInWorld.YawRadians)); + HasValidVelocityEstimate = + hasValidVelocityEstimate; + + // 第一帧或定位重置后的速度不可用于闭环控制, + // 此时显式置零,避免调用方误用残留速度。 + TwistInWorld = hasValidVelocityEstimate + ? twistInWorld + : Twist2D.Zero; + + var worldPoseInBody = + FrameTransform2D.Inverse( + PoseInWorld); + TwistInBody = + FrameTransform2D.TransformTwistAtSamePoint( + worldPoseInBody, + TwistInWorld); + } + + /// + /// 获取状态源单调时钟中的采样时刻,单位为s。 + /// + public double SampleTimestampSeconds { get; } + + /// + /// 获取车体中心在Detour世界坐标系中的位姿,单位为m和rad。 + /// + public Pose2D PoseInWorld { get; } + + /// + /// 获取在世界坐标系中表达的车辆速度,单位为m/s和rad/s。 + /// + public Twist2D TwistInWorld { get; } + + /// + /// 获取在车体坐标系中表达的车辆速度,X向前、Y向左、逆时针为正。 + /// + public Twist2D TwistInBody { get; } + + /// + /// 获取当前速度是否已由至少两个连续有效定位样本估算得到。 + /// + public bool HasValidVelocityEstimate { get; } + + /// + /// 检查位姿是否由有限数值组成。 + /// + private static void EnsureFinitePose( + Pose2D pose, + string parameterName) + { + if (!IsFinite(pose.XMeters) || + !IsFinite(pose.YMeters) || + !IsFinite(pose.YawRadians)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "车辆位姿必须由有限数值组成。"); + } + } + + /// + /// 检查速度是否由有限数值组成。 + /// + private static void EnsureFiniteTwist( + Twist2D twist, + string parameterName) + { + if (!IsFinite(twist.VxMetersPerSecond) || + !IsFinite(twist.VyMetersPerSecond) || + !IsFinite(twist.OmegaRadiansPerSecond)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "车辆速度必须由有限数值组成。"); + } + } + + /// + /// 检查数值是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + if (!IsFinite(value) || value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "采样时刻必须是非负有限值。"); + } + } + + /// + /// 判断数值是否可用于车辆状态计算。 + /// + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && + !double.IsInfinity(value); + } + } +} diff --git a/ClumsyPilot/StateEstimation/VelocityEstimator2D.cs b/ClumsyPilot/StateEstimation/VelocityEstimator2D.cs new file mode 100644 index 0000000..3d8a85d --- /dev/null +++ b/ClumsyPilot/StateEstimation/VelocityEstimator2D.cs @@ -0,0 +1,275 @@ +using System; +using MyParking.Shared; + +namespace MultiWheelC.StateEstimation +{ + /// + /// 根据连续有效的Detour世界位姿和真实时间差估算车辆二维速度。 + /// + public sealed class VelocityEstimator2D + { + public const double DefaultLinearFilterTimeConstantSeconds = + 0.15; + public const double DefaultAngularFilterTimeConstantSeconds = + 0.20; + + private readonly FirstOrderLowPassFilter + _worldVelocityXFilter; + private readonly FirstOrderLowPassFilter + _worldVelocityYFilter; + private readonly FirstOrderLowPassFilter + _angularVelocityFilter; + + private bool _hasPreviousSample; + private Pose2D _previousPoseInWorld; + private double _previousTimestampSeconds; + + /// + /// 创建使用默认0.15s线速度和0.20s角速度时间常数的估计器。 + /// + public VelocityEstimator2D() + : this( + DefaultLinearFilterTimeConstantSeconds, + DefaultAngularFilterTimeConstantSeconds) + { + } + + /// + /// 创建使用指定线速度和角速度滤波时间常数的估计器。 + /// + public VelocityEstimator2D( + double linearFilterTimeConstantSeconds, + double angularFilterTimeConstantSeconds) + { + _worldVelocityXFilter = + new FirstOrderLowPassFilter( + linearFilterTimeConstantSeconds); + _worldVelocityYFilter = + new FirstOrderLowPassFilter( + linearFilterTimeConstantSeconds); + _angularVelocityFilter = + new FirstOrderLowPassFilter( + angularFilterTimeConstantSeconds); + } + + /// + /// 获取是否已经保存了可用于下一次差分的位姿基准。 + /// + public bool HasPreviousSample => + _hasPreviousSample; + + /// + /// 使用一个新的有效定位样本更新并返回车辆状态。 + /// + public VehicleState Update( + Pose2D poseInWorld, + double sampleTimestampSeconds) + { + EnsureFinitePose( + poseInWorld, + nameof(poseInWorld)); + EnsureFiniteNonNegative( + sampleTimestampSeconds, + nameof(sampleTimestampSeconds)); + + var normalizedPoseInWorld = + new Pose2D( + poseInWorld.XMeters, + poseInWorld.YMeters, + AngleMath.NormalizeRadians( + poseInWorld.YawRadians)); + + if (!_hasPreviousSample) + { + return Reset( + normalizedPoseInWorld, + sampleTimestampSeconds); + } + + var deltaTimeSeconds = + sampleTimestampSeconds - + _previousTimestampSeconds; + + if (deltaTimeSeconds <= 0.0) + { + throw new ArgumentOutOfRangeException( + nameof(sampleTimestampSeconds), + "新定位样本的单调时间戳必须严格大于上一帧。"); + } + + var rawVelocityXInWorld = + (normalizedPoseInWorld.XMeters - + _previousPoseInWorld.XMeters) / + deltaTimeSeconds; + var rawVelocityYInWorld = + (normalizedPoseInWorld.YMeters - + _previousPoseInWorld.YMeters) / + deltaTimeSeconds; + var rawAngularVelocity = + AngleMath.ShortestDifferenceRadians( + normalizedPoseInWorld.YawRadians, + _previousPoseInWorld.YawRadians) / + deltaTimeSeconds; + + var filteredTwistInWorld = + new Twist2D( + _worldVelocityXFilter.Update( + rawVelocityXInWorld, + deltaTimeSeconds), + _worldVelocityYFilter.Update( + rawVelocityYInWorld, + deltaTimeSeconds), + _angularVelocityFilter.Update( + rawAngularVelocity, + deltaTimeSeconds)); + + _previousPoseInWorld = + normalizedPoseInWorld; + _previousTimestampSeconds = + sampleTimestampSeconds; + + return new VehicleState( + sampleTimestampSeconds, + normalizedPoseInWorld, + filteredTwistInWorld, + true); + } + + /// + /// 更新位姿差分基准但保留当前滤波速度,避免定位跳变形成虚假速度尖峰。 + /// + public VehicleState RebasePreservingVelocity( + Pose2D poseInWorld, + double sampleTimestampSeconds) + { + EnsureFinitePose( + poseInWorld, + nameof(poseInWorld)); + EnsureFiniteNonNegative( + sampleTimestampSeconds, + nameof(sampleTimestampSeconds)); + + var normalizedPoseInWorld = + new Pose2D( + poseInWorld.XMeters, + poseInWorld.YMeters, + AngleMath.NormalizeRadians( + poseInWorld.YawRadians)); + + _previousPoseInWorld = + normalizedPoseInWorld; + _previousTimestampSeconds = + sampleTimestampSeconds; + _hasPreviousSample = true; + + var hasValidVelocityEstimate = + _worldVelocityXFilter.IsInitialized && + _worldVelocityYFilter.IsInitialized && + _angularVelocityFilter.IsInitialized; + + var retainedTwistInWorld = + hasValidVelocityEstimate + ? new Twist2D( + _worldVelocityXFilter.Value, + _worldVelocityYFilter.Value, + _angularVelocityFilter.Value) + : Twist2D.Zero; + + return new VehicleState( + sampleTimestampSeconds, + normalizedPoseInWorld, + retainedTwistInWorld, + hasValidVelocityEstimate); + } + + /// + /// 使用当前定位重新建立差分基准,并返回速度无效的零速状态。 + /// + public VehicleState Reset( + Pose2D poseInWorld, + double sampleTimestampSeconds) + { + EnsureFinitePose( + poseInWorld, + nameof(poseInWorld)); + EnsureFiniteNonNegative( + sampleTimestampSeconds, + nameof(sampleTimestampSeconds)); + + _previousPoseInWorld = + new Pose2D( + poseInWorld.XMeters, + poseInWorld.YMeters, + AngleMath.NormalizeRadians( + poseInWorld.YawRadians)); + _previousTimestampSeconds = + sampleTimestampSeconds; + _hasPreviousSample = true; + + _worldVelocityXFilter.Reset(); + _worldVelocityYFilter.Reset(); + _angularVelocityFilter.Reset(); + + return new VehicleState( + sampleTimestampSeconds, + _previousPoseInWorld, + Twist2D.Zero, + false); + } + + /// + /// 清除差分基准和全部滤波历史,使下一帧重新初始化估计器。 + /// + public void Reset() + { + _hasPreviousSample = false; + _previousPoseInWorld = Pose2D.Identity; + _previousTimestampSeconds = 0.0; + + _worldVelocityXFilter.Reset(); + _worldVelocityYFilter.Reset(); + _angularVelocityFilter.Reset(); + } + + /// + /// 检查位姿是否由有限数值组成。 + /// + private static void EnsureFinitePose( + Pose2D pose, + string parameterName) + { + if (!IsFinite(pose.XMeters) || + !IsFinite(pose.YMeters) || + !IsFinite(pose.YawRadians)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "速度估计使用的车辆位姿必须由有限数值组成。"); + } + } + + /// + /// 检查数值是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + if (!IsFinite(value) || value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "速度估计使用的采样时刻必须是非负有限值。"); + } + } + + /// + /// 判断数值是否可用于速度估计。 + /// + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && + !double.IsInfinity(value); + } + } +} diff --git a/ClumsyPilot/Trajectory/LegacyTrackAdapter.cs b/ClumsyPilot/Trajectory/LegacyTrackAdapter.cs new file mode 100644 index 0000000..d8a2ebb --- /dev/null +++ b/ClumsyPilot/Trajectory/LegacyTrackAdapter.cs @@ -0,0 +1 @@ +// 兼容现有 MDCS 的 AbstractTrack \ No newline at end of file diff --git a/ClumsyPilot/Trajectory/Trajectory2D.cs b/ClumsyPilot/Trajectory/Trajectory2D.cs new file mode 100644 index 0000000..a991bde --- /dev/null +++ b/ClumsyPilot/Trajectory/Trajectory2D.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using MyParking.Shared; + +namespace MultiWheelC.Trajectory +{ + /// + /// 保存一条经过基本合法性检查的只读二维参考轨迹。 + /// + public sealed class Trajectory2D + { + private const double StartArcLengthToleranceMeters = 1e-9; + private const double MinimumSegmentLengthMeters = 1e-6; + + private readonly TrajectoryPoint[] _points; + private readonly ReadOnlyCollection _readOnlyPoints; + + /// + /// 复制并验证按累计弧长升序排列的参考轨迹点。 + /// + public Trajectory2D( + IEnumerable points) + { + if (points == null) + { + throw new ArgumentNullException( + nameof(points)); + } + + _points = points.ToArray(); + + if (_points.Length < 2) + { + throw new ArgumentException( + "二维轨迹至少需要两个轨迹点。", + nameof(points)); + } + + if (Math.Abs(_points[0].ArcLengthMeters) > + StartArcLengthToleranceMeters) + { + throw new ArgumentException( + "二维轨迹起点的累计弧长必须为0m。", + nameof(points)); + } + + for (var index = 1; + index < _points.Length; + index++) + { + ValidateSegment( + _points[index - 1], + _points[index], + index, + nameof(points)); + } + + _readOnlyPoints = + Array.AsReadOnly(_points); + } + + /// + /// 获取轨迹点数量。 + /// + public int Count => _points.Length; + + /// + /// 获取指定索引处的轨迹点。 + /// + public TrajectoryPoint this[int index] => + _points[index]; + + /// + /// 获取不可修改的有序轨迹点集合。 + /// + public IReadOnlyList Points => + _readOnlyPoints; + + /// + /// 获取轨迹起点。 + /// + public TrajectoryPoint StartPoint => + _points[0]; + + /// + /// 获取轨迹终点。 + /// + public TrajectoryPoint EndPoint => + _points[_points.Length - 1]; + + /// + /// 获取轨迹总弧长,单位为m。 + /// + public double TotalLengthMeters => + EndPoint.ArcLengthMeters; + + /// + /// 根据当前累计弧长计算到轨迹终点的剩余距离。 + /// + public double GetRemainingDistanceMeters( + double arcLengthMeters) + { + EnsureFinite( + arcLengthMeters, + nameof(arcLengthMeters)); + + if (arcLengthMeters <= 0.0) + return TotalLengthMeters; + + if (arcLengthMeters >= TotalLengthMeters) + return 0.0; + + return TotalLengthMeters - arcLengthMeters; + } + + /// + /// 按累计弧长在线性位置、航向、曲率和参考速度之间插值得到轨迹点。 + /// + public TrajectoryPoint SampleAtArcLength( + double arcLengthMeters) + { + EnsureFinite( + arcLengthMeters, + nameof(arcLengthMeters)); + + if (arcLengthMeters <= 0.0) + { + return StartPoint; + } + + if (arcLengthMeters >= TotalLengthMeters) + { + return EndPoint; + } + + var segmentStartIndex = + FindSegmentStartIndex( + arcLengthMeters); + var segmentStart = + _points[segmentStartIndex]; + var segmentEnd = + _points[segmentStartIndex + 1]; + var interpolationRatio = + (arcLengthMeters - + segmentStart.ArcLengthMeters) / + (segmentEnd.ArcLengthMeters - + segmentStart.ArcLengthMeters); + + return new TrajectoryPoint( + arcLengthMeters, + new Pose2D( + InterpolationMath.Lerp( + segmentStart.PoseInWorld.XMeters, + segmentEnd.PoseInWorld.XMeters, + interpolationRatio), + InterpolationMath.Lerp( + segmentStart.PoseInWorld.YMeters, + segmentEnd.PoseInWorld.YMeters, + interpolationRatio), + AngleMath.LerpRadians( + segmentStart.PoseInWorld.YawRadians, + segmentEnd.PoseInWorld.YawRadians, + interpolationRatio)), + InterpolationMath.Lerp( + segmentStart.CurvaturePerMeter, + segmentEnd.CurvaturePerMeter, + interpolationRatio), + InterpolationMath.Lerp( + segmentStart.ReferenceSpeedMetersPerSecond, + segmentEnd.ReferenceSpeedMetersPerSecond, + interpolationRatio)); + } + + /// + /// 使用二分查找获取包含指定累计弧长的线段起点索引。 + /// + private int FindSegmentStartIndex( + double arcLengthMeters) + { + var lowerIndex = 0; + var upperIndex = _points.Length - 1; + + while (upperIndex - lowerIndex > 1) + { + var middleIndex = + lowerIndex + + (upperIndex - lowerIndex) / 2; + + if (_points[middleIndex].ArcLengthMeters <= + arcLengthMeters) + { + lowerIndex = middleIndex; + } + else + { + upperIndex = middleIndex; + } + } + + return lowerIndex; + } + + /// + /// 检查相邻轨迹点是否构成有效的非零长度有序线段。 + /// + private static void ValidateSegment( + TrajectoryPoint previous, + TrajectoryPoint current, + int currentIndex, + string parameterName) + { + if (current.ArcLengthMeters <= + previous.ArcLengthMeters) + { + throw new ArgumentException( + $"轨迹点{currentIndex}的累计弧长必须严格大于前一个点。", + parameterName); + } + + var deltaX = + current.PoseInWorld.XMeters - + previous.PoseInWorld.XMeters; + var deltaY = + current.PoseInWorld.YMeters - + previous.PoseInWorld.YMeters; + var segmentLengthSquared = + deltaX * deltaX + + deltaY * deltaY; + var minimumLengthSquared = + MinimumSegmentLengthMeters * + MinimumSegmentLengthMeters; + + if (segmentLengthSquared < + minimumLengthSquared) + { + throw new ArgumentException( + $"轨迹点{currentIndex}与前一个点的位置过近,无法构成有效投影线段。", + parameterName); + } + } + + /// + /// 检查数值是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹弧长必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/Trajectory/TrajectoryPoint.cs b/ClumsyPilot/Trajectory/TrajectoryPoint.cs new file mode 100644 index 0000000..83dd56c --- /dev/null +++ b/ClumsyPilot/Trajectory/TrajectoryPoint.cs @@ -0,0 +1,102 @@ +using System; +using MyParking.Shared; + +namespace MultiWheelC.Trajectory +{ + /// + /// 描述按弧长参数化的车体中心参考轨迹点,统一使用SI单位。 + /// + public readonly struct TrajectoryPoint + { + /// + /// 创建包含中心位姿、曲率和速度信息的参考轨迹点。 + /// + public TrajectoryPoint( + double arcLengthMeters, + Pose2D poseInWorld, + double curvaturePerMeter, + double referenceSpeedMetersPerSecond) + { + EnsureFiniteNonNegative( + arcLengthMeters, + nameof(arcLengthMeters)); + EnsureFinite( + poseInWorld.XMeters, + nameof(poseInWorld)); + EnsureFinite( + poseInWorld.YMeters, + nameof(poseInWorld)); + EnsureFinite( + poseInWorld.YawRadians, + nameof(poseInWorld)); + EnsureFinite( + curvaturePerMeter, + nameof(curvaturePerMeter)); + EnsureFinite( + referenceSpeedMetersPerSecond, + nameof(referenceSpeedMetersPerSecond)); + ArcLengthMeters = arcLengthMeters; + PoseInWorld = new Pose2D( + poseInWorld.XMeters, + poseInWorld.YMeters, + AngleMath.NormalizeRadians( + poseInWorld.YawRadians)); + CurvaturePerMeter = curvaturePerMeter; + ReferenceSpeedMetersPerSecond = + referenceSpeedMetersPerSecond; + } + + /// + /// 获取从轨迹起点累计到当前点的弧长,单位为m。 + /// + public double ArcLengthMeters { get; } + + /// + /// 获取车体中心参考坐标系在世界坐标系中的位姿。 + /// + public Pose2D PoseInWorld { get; } + + /// + /// 获取车体中心参考轨迹曲率,单位为1/m,左转为正。 + /// + public double CurvaturePerMeter { get; } + + /// + /// 获取沿轨迹切线方向的有符号参考速度,单位为m/s。 + /// + public double ReferenceSpeedMetersPerSecond { get; } + + /// + /// 检查数值是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹点参数必须是有限值。"); + } + } + + /// + /// 检查数值是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹累计弧长不能为负数。"); + } + } + } +} diff --git a/ClumsyPilot/Trajectory/TrajectoryProjection.cs b/ClumsyPilot/Trajectory/TrajectoryProjection.cs new file mode 100644 index 0000000..798ae17 --- /dev/null +++ b/ClumsyPilot/Trajectory/TrajectoryProjection.cs @@ -0,0 +1,123 @@ +using System; +using MyParking.Shared; + +namespace MultiWheelC.Trajectory +{ + /// + /// 保存车体中心投影到二维参考轨迹后得到的只读结果。 + /// + public readonly struct TrajectoryProjection + { + /// + /// 创建包含轨迹进度、参考状态和跟踪误差的投影结果。 + /// + public TrajectoryProjection( + int segmentStartIndex, + TrajectoryPoint referencePoint, + double lateralErrorMeters, + double headingErrorRadians, + double distanceToTrajectoryMeters, + double remainingDistanceMeters) + { + if (segmentStartIndex < 0) + { + throw new ArgumentOutOfRangeException( + nameof(segmentStartIndex), + "投影线段起点索引不能为负数。"); + } + + EnsureFinite( + lateralErrorMeters, + nameof(lateralErrorMeters)); + EnsureFinite( + headingErrorRadians, + nameof(headingErrorRadians)); + EnsureFiniteNonNegative( + distanceToTrajectoryMeters, + nameof(distanceToTrajectoryMeters)); + EnsureFiniteNonNegative( + remainingDistanceMeters, + nameof(remainingDistanceMeters)); + + SegmentStartIndex = segmentStartIndex; + ReferencePoint = referencePoint; + LateralErrorMeters = lateralErrorMeters; + HeadingErrorRadians = + AngleMath.NormalizeRadians( + headingErrorRadians); + DistanceToTrajectoryMeters = + distanceToTrajectoryMeters; + RemainingDistanceMeters = + remainingDistanceMeters; + } + + /// + /// 获取投影所在轨迹线段的起点索引,线段终点索引为该值加1。 + /// + public int SegmentStartIndex { get; } + + /// + /// 获取投影位置插值得到的车体中心参考轨迹点。 + /// + public TrajectoryPoint ReferencePoint { get; } + + /// + /// 获取有符号横向误差,单位为m,参考轨迹位于车辆左侧时为正。 + /// + public double LateralErrorMeters { get; } + + /// + /// 获取参考航向减实际车体航向的最短角差,单位为rad,逆时针为正。 + /// + public double HeadingErrorRadians { get; } + + /// + /// 获取车体中心到投影点的欧氏距离,单位为m。 + /// + public double DistanceToTrajectoryMeters { get; } + + /// + /// 获取投影位置沿轨迹到终点的剩余弧长,单位为m。 + /// + public double RemainingDistanceMeters { get; } + + /// + /// 获取投影位置从轨迹起点累计的弧长,单位为m。 + /// + public double ArcLengthMeters => + ReferencePoint.ArcLengthMeters; + + /// + /// 检查数值是否为有限值。 + /// + private static void EnsureFinite( + double value, + string parameterName) + { + if (double.IsNaN(value) || + double.IsInfinity(value)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹投影参数必须是有限值。"); + } + } + + /// + /// 检查数值是否为非负有限值。 + /// + private static void EnsureFiniteNonNegative( + double value, + string parameterName) + { + EnsureFinite(value, parameterName); + + if (value < 0.0) + { + throw new ArgumentOutOfRangeException( + parameterName, + "轨迹投影距离不能为负数。"); + } + } + } +} diff --git a/ClumsyPilot/Trajectory/TrajectoryProjector.cs b/ClumsyPilot/Trajectory/TrajectoryProjector.cs new file mode 100644 index 0000000..43cc3c5 --- /dev/null +++ b/ClumsyPilot/Trajectory/TrajectoryProjector.cs @@ -0,0 +1,219 @@ +using System; +using MyParking.Shared; + +namespace MultiWheelC.Trajectory +{ + /// + /// 将Detour给出的实际车体中心位姿投影到二维离散参考轨迹。 + /// + public static class TrajectoryProjector + { + /// + /// 在整条轨迹上查找距离实际车体中心最近的线段投影结果。 + /// + public static TrajectoryProjection Project( + Trajectory2D trajectory, + Pose2D vehiclePoseInWorld) + { + if (trajectory == null) + { + throw new ArgumentNullException( + nameof(trajectory)); + } + + EnsureFinitePose( + vehiclePoseInWorld, + nameof(vehiclePoseInWorld)); + + var bestSegmentStartIndex = 0; + var bestInterpolationRatio = 0.0; + var bestProjectedX = 0.0; + var bestProjectedY = 0.0; + var bestDistanceSquared = + double.PositiveInfinity; + + for (var segmentStartIndex = 0; + segmentStartIndex < trajectory.Count - 1; + segmentStartIndex++) + { + var segmentStart = + trajectory[segmentStartIndex]; + var segmentEnd = + trajectory[segmentStartIndex + 1]; + + var segmentX = + segmentEnd.PoseInWorld.XMeters - + segmentStart.PoseInWorld.XMeters; + var segmentY = + segmentEnd.PoseInWorld.YMeters - + segmentStart.PoseInWorld.YMeters; + var segmentLengthSquared = + segmentX * segmentX + + segmentY * segmentY; + + var vehicleFromSegmentStartX = + vehiclePoseInWorld.XMeters - + segmentStart.PoseInWorld.XMeters; + var vehicleFromSegmentStartY = + vehiclePoseInWorld.YMeters - + segmentStart.PoseInWorld.YMeters; + + var interpolationRatio = + InterpolationMath.Clamp01( + (vehicleFromSegmentStartX * segmentX + + vehicleFromSegmentStartY * segmentY) / + segmentLengthSquared); + + var projectedX = + InterpolationMath.Lerp( + segmentStart.PoseInWorld.XMeters, + segmentEnd.PoseInWorld.XMeters, + interpolationRatio); + var projectedY = + InterpolationMath.Lerp( + segmentStart.PoseInWorld.YMeters, + segmentEnd.PoseInWorld.YMeters, + interpolationRatio); + + var projectionErrorX = + projectedX - + vehiclePoseInWorld.XMeters; + var projectionErrorY = + projectedY - + vehiclePoseInWorld.YMeters; + var distanceSquared = + projectionErrorX * projectionErrorX + + projectionErrorY * projectionErrorY; + + if (distanceSquared >= bestDistanceSquared) + { + continue; + } + + bestSegmentStartIndex = + segmentStartIndex; + bestInterpolationRatio = + interpolationRatio; + bestProjectedX = projectedX; + bestProjectedY = projectedY; + bestDistanceSquared = distanceSquared; + } + + return BuildProjection( + trajectory, + vehiclePoseInWorld, + bestSegmentStartIndex, + bestInterpolationRatio, + bestProjectedX, + bestProjectedY, + bestDistanceSquared); + } + + /// + /// 根据最近线段和插值比例生成控制器使用的完整投影结果。 + /// + private static TrajectoryProjection BuildProjection( + Trajectory2D trajectory, + Pose2D vehiclePoseInWorld, + int segmentStartIndex, + double interpolationRatio, + double projectedX, + double projectedY, + double distanceSquared) + { + var segmentStart = + trajectory[segmentStartIndex]; + var segmentEnd = + trajectory[segmentStartIndex + 1]; + + var referenceYawRadians = + AngleMath.LerpRadians( + segmentStart.PoseInWorld.YawRadians, + segmentEnd.PoseInWorld.YawRadians, + interpolationRatio); + var referenceArcLengthMeters = + InterpolationMath.Lerp( + segmentStart.ArcLengthMeters, + segmentEnd.ArcLengthMeters, + interpolationRatio); + var referenceCurvaturePerMeter = + InterpolationMath.Lerp( + segmentStart.CurvaturePerMeter, + segmentEnd.CurvaturePerMeter, + interpolationRatio); + var referenceSpeedMetersPerSecond = + InterpolationMath.Lerp( + segmentStart.ReferenceSpeedMetersPerSecond, + segmentEnd.ReferenceSpeedMetersPerSecond, + interpolationRatio); + + var referencePoint = + new TrajectoryPoint( + referenceArcLengthMeters, + new Pose2D( + projectedX, + projectedY, + referenceYawRadians), + referenceCurvaturePerMeter, + referenceSpeedMetersPerSecond); + + var segmentX = + segmentEnd.PoseInWorld.XMeters - + segmentStart.PoseInWorld.XMeters; + var segmentY = + segmentEnd.PoseInWorld.YMeters - + segmentStart.PoseInWorld.YMeters; + var segmentLength = + Math.Sqrt( + segmentX * segmentX + + segmentY * segmentY); + + // 以轨迹线段的前进方向判断左右: + // 从车辆指向参考轨迹的向量位于轨迹左侧时为正。 + var vehicleToProjectionX = + projectedX - + vehiclePoseInWorld.XMeters; + var vehicleToProjectionY = + projectedY - + vehiclePoseInWorld.YMeters; + var lateralErrorMeters = + (segmentX * vehicleToProjectionY - + segmentY * vehicleToProjectionX) / + segmentLength; + + var headingErrorRadians = + AngleMath.ShortestDifferenceRadians( + referenceYawRadians, + vehiclePoseInWorld.YawRadians); + + return new TrajectoryProjection( + segmentStartIndex, + referencePoint, + lateralErrorMeters, + headingErrorRadians, + Math.Sqrt(distanceSquared), + trajectory.GetRemainingDistanceMeters( + referenceArcLengthMeters)); + } + + /// + /// 检查用于投影的实际车体中心位姿是否包含有限数值。 + /// + private static void EnsureFinitePose( + Pose2D pose, + string parameterName) + { + if (double.IsNaN(pose.XMeters) || + double.IsInfinity(pose.XMeters) || + double.IsNaN(pose.YMeters) || + double.IsInfinity(pose.YMeters) || + double.IsNaN(pose.YawRadians) || + double.IsInfinity(pose.YawRadians)) + { + throw new ArgumentOutOfRangeException( + parameterName, + "用于轨迹投影的车体位姿必须是有限值。"); + } + } + } +} diff --git a/ClumsyPilot/ref/CommonUsage.dll b/ClumsyPilot/ref/CommonUsage.dll index effb81ec918baad01b420552b70e9dd5766da42c..82a8d1d5586f0ac0393a53084b6c846b2fe31a56 100644 GIT binary patch delta 79489 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

c70(w4#?@op?+81 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 K3Igu|kuV44`u52H delta 69780 zcmb@v33yb+@;BVwvt}|0b28cYge*fA2qA$$ z2Sf#TR8U;O3n~gKB3^M{QB+*6SFc`0ML|V;e^sZmx%}V%_dRbOt<%4*>gww1I=!89 zhDBev7v12#=89pv2ma%Aq5lqw?9516B7|KD;j}V8$woR8={j76@NpVRJ0bkFU&W$p zU+*1#HKa#THl&x@uFdj$Co16u-n?CkIpHi-6do`0J;J7y+QV1Mo)M&)^f<%+kQosu zA@HB@gg=zoNl#P#_&W*zE8!4lyeP!U{}n>6%_>#{ww$a+Nb>@2*9F3(OE9jK?OJKf+M-$i=*TBTplHRH&IQw$M1b#D&Jv zWdX5ZBviu4)3%)8@GIZrTvUaoTk+f{JdcXlLbJmkyNl({Ej=Q7N6Of6ZET|X3OKpN33&@&M&h^= zq2Zi+1Kgauh&X|GsQ0=SzS><15!xcv zWyv{Q3*YD>2QgQt*c;)NHJ zc`#faxDfRIKwBIMv_4G+x47LcDPsqI5c8$>9@A_um- zU9e8Yk#+~0F+S44Tu1Pu@Ur4OYD96*-lS=~3-w#B2a__~EzJ?9``s}SR#C(4YH6NH z1&dsP|HbnTmk$;HLgBBydlDgNtx3IE2gbxI_%CxWm6A zR`&0|n`Y=+BvlZ@t{w50MzQoRoEb>NeHlmxiG@J_&DNh_r9*v;@w2b~w_W~)MMia-&Mh5~^%~x9< zudSEzt1ZU{eGqBpL6AIp6jJc$&$z&&xU)MwdW=|>M}I+51;4aBnoo87*`p-h>Cr>v z(Zk4MkN$wfaVI9mc8|)#;l*Y7DO6v(7pXkF$XUT-a?84=@$Qr#yKCr48Z7_)816AP zZrD%V`8l0@;#mIriToR6gN}3;&+RnmNO<Tllg0YOl%@eZ6{ElR9HrqcZ)$~gHyDvs)=IDGkpdaqU7(iDDkLcZJ-J~m;h|88hSj8MJ`CQvZAek72s>mnabq|2V1M%q4}f zp*-tSK$nFciJPF1l5^82Swu;z_2ry=Y2Ut@&;XrVR~M?FWPGTSE`bxsQc+L~YpI~E zH61$iVkHMFnI=pd0`~_o7xs-(I;6Zv9CsE|4`!eMZc=2>d|nv9lBduz0;R%flaoy< z+nNQjtLI>}2;`DL3Ds_?{UfW4SP5Ntf!?II4;AbG_r*&8jul=s*_F%<;F=PT+da;r z0@1d?CFYQcJ0|y(mxd2cPBF=^xy81;!pI;NRQ6Vbe4&E9kR5oA2C-~ppzu&+cE}D4 z^h9pkC!}owiSh)vf|{x^&3=pnCnKfxd3ebjA6c;x%R|fi;Y+4uxG1C}f+vJ;o08dS zPki7tl#B=x({RMcCj=s>co!@BvP7HRE#^eD-h)o%3f--@wI{i5OGAL`w$c%;w?hKq zvbCW(BA5dG96EEaM}6TzQ={WM+SxaN=<^s(!-+ECV^i~GUihl1#g@Uy**P`8t1!xW z442{R6wW!PEWJZpWgs5{Zf=pLme$Tj_q7~LO>%c_5*gg-POGrhq(AkQ-)*>y0$u%- z90+vOYxP8jH-&H|G)LN96cd@}mdT><@j1Oy8{1tHcor@XQ4c^u;u1O`eCxTH2%TN$ zW}55JiR1ZR3Hrc`KQ0j`E)`e;W6`~80h)`kAewlsaWEk`oRWbni4Qic((Hszc7kTx zRM09;QZe#)+(02xT-;Hbv*UgXtLw3n1kI zo23#uq{99geRAp6w3N3VMijt%{r;3w6h3!Gc?R9su2u{a9|f=_6vJwrbjPkK6#nh6*Dt>Tcc1l>Ir$tMhtc~@da8lft+b;y%R+OAHwr9J+{^j z;Tvb2@2r7bpg8P1ucwIxI~Jz)5EkUPxkRRX2?GBQu#K!$o6(k??ZRrYlG%mq0W6_m148w)rec^=e7#D4iq+t(Zp>ge9Ha_9mp= zY$c1hgB{XVGqrRcDGwkhnnFI!u0Pk8p6tokyKm=i$Idqmqb zB<+DyP`GV9{xZ3WXDDphxn7M3ogJy~3}~DgIxM4CbYz2}-1o&LVn> zljqVNmvp+sDz^}4SvsQ-DD54aq}twuF>U_=iM9u@OLqDrDkei%alZs+kf+4inYI9?Z_msYe^i?}5v8r(by%vSEY;M% zZP$0Gwqxt#Qx2jw8pIY2^4KFC-h`;RxP=gRxTRv8)=q`f?u36FaeklS@cRvaWrGbxVq#S=3q1B;fF`+^I4fsrXO_ z+T9*q&HfytXD1x*&oy$4Tq_9jjJz&EKrw&-$T#vU?;_V2sYVX$`-{#E7-hCMoNF#8|9? z#T_Ux8y#c~g)LC4heBS=1~!A`H{$&<@&4k96R;ydbYZHjw;RPgS^9g!z5R@SmAIME zsNP-nWY%h!<7-wsgTl+QE&R&BEYt91v`_?;8D&QAAk^EfPV56jOD(-vewmEuuY$BS zFp7=-hLOGbx%>gmr&}u&Q8-MvjUHA5RSgEWD|KbpwroMHM36UtO4tCR7`Cqnce1 zFlovVMl}qoHmbWBWcq82n&Q_)VWBpoZ8rM9>zK{+K&NTg>Y$3+!ZJhwtqP4=?b2GV z%hsY@Qp*dUnJv11j9OG)OTwKN+x>$Kb5QX%6~s1+C%`(Ru4tTS`x+*+`Jy}G(As-q zrDmOGSXW&;}b0E9B5*Yqe@L!1M+x*VXjF#^B;d!jont#3bm|BxZHD_zu(3#PD}Z;`|d~ zKhNTN5;rRLgp-!Gl=A8#!EaQY4bd(-d25Vt?Wp8xEP!PsGQxK)-IPh$hTpp3VvShq zCZl_4mOqZ*mf4q;O0)C+$xw#h$Y>)goQ8iDZAM+VlFpdSj)2CfSQMvN%E;_6^r#}s zX!&aRQE0Ul+rsd}5tvB3*ocS@7=fx1M0C8rS7A8g@(3SCo6!rhy^LODe#PZkX5kd! zGzxF9iW-I7j@DbySA4hgaE$>p+9(QOQ1bYrjVPl?(eSD#>L)O%1`>${25#Ql>M*Y3 z?kaL%5m1uvNdzFb8FX0S~W#nz^#P#IDF9`XJ@g*}zg$8Gev!RXQA zGWx8Z*^|D7D`uE=zM z%HR>5nFe(ohYEM_bkWLY3!@BP!sy-IOXWs+RlnacQ1xrAN9)=GzVJuOF?)qwSJo6y z!eFGE$%jR})LyA`tR0xW^;R0RRIoIB{*?(?XHyAlFKFXQqM5gZv?GKVvcj9LoGcr{ zQCH=i+d$rR7e>cp0CddNNEmM3?lmH`CtKBHJ7u@o+jqE(^o&9KUmK^#^I{e~-O1KV zi<0n7S7qgpWR%Tsk1*&zF5;ZoRaym0!XI6AGByk)D~eKSLJROz7J-&^)Z(XOknsE! zy`o6KpatjFhVkxj$<$h<>Ns;sXa6ni(S20zL!j04^c@&80UZ9i;z>QE& zoIp7#w@xI(Eh|%oZ-R$faV+Z*I8UaHfVJ%B;cm6oXm)3_!_~~zW8>?l(-S8@`aXyI$YvcU2QI^%H+@wp z%@3Z)Lg9jGF7YJpg>$X0j6&luUPZc!60yxRy6-=|6>A`=f^<>?p{R(WSPW-;uGQt3 zS>VWt&vNvcl6CTA|8235PqfWzX&7w{ddLwQ#B4EAVy3wu`%|U*owN%sXH!$wdAOY)XCCelEU(Sg$ z*$Hh;u%bP_ED!2vN-!dH3R?iPi^}$BGxXSRMcM{xjq{I@CJZMAdTdC$5A;66S9K## z|3+bvG7PI?r(xXC63 zhTy{h57S7d-uS2roSwFKa)i~#2ruqo)O$%|z)hX%uoWj;u8d$$f>k#7^z8S046SP^ z^kdwng-B;+5)MtAw&G}Ofol~hbU|!m*uSQ49ZhG(ov33S&B zwO(5>HYvPr?$GOj-uu@SH!SC197+cF$PEsotB=lzvH5ZuJ*-`Y%kWtVw_&vJGa}$j z90J0b;neF>`ge6H7Hd^!p>wQ+rR*51tP{AI)>uV(ykfAP zQaQw9h=Eyb3q~hTinH<%jKiHzg8cXci)bqyUVLMM92maw#yq$6I50Rc{M(JG*A2GE zx6HI0gD}uf4KY6ux2KcJE*KKdyQwgbUF4|1-b=JUQM6QVMCZ-Y)!Y)^aZ@sM=KrNm zw&aiESzRhozss<1U2G}|a&UE-mU4N0I(JjJZr!L+9Rh)q;l?gf*5nlz!ZrvmDq&|g zHxdG9Pe+MA2`7ZA!ym5e6@v{Gw%jC#)`Q})WzzZ+V@cCYZ0%@PY=3$Y-g#!6TpPY} z{aDb0>oc>@>+GM7LDwfGf5&Ge@mVDX9MARbT%(%Xllu7+l%qbI?A-v@+imK*uZFvqj=Vdsft>pAATa* z>9BYzbt?X7UYS!LqH}LbZha<%g|3xs2%Ux1Z2QAiEOYG2MvrR)caaf7*)35=h^3;P zA#4fkD%usoir=n!NM2@#uq4G}4yx29T5xS;zu8CP)u@}p)wh<(o5SbdnjeeR7Oj%0 za%-`xg7x9ew=QXa{L%lmY$xe$9UY!kXO8NmA`-cRLvLS(G7H5G9{2h#1g zREAXtG5MCF2hDz%r1NR+Q$gZ3TgTylC`T<}M!NE_p#|xv`nuZ_PT-OOxX%N(C>spG zn#}GD(Ze*j2~)0JIYO9n$rI%L#LiloNkxW+C)}QVCPu_|<0L%NVH_219gPOD1r4gb zHd_c|r@bIsXV8*JQG>+!)L46<0*g1ytQgVYkW<5N-JTKGW^pPl4n`?(#)Kp8$Z*XC zM+HZP``l4#qAq*rVw|gnPDM%uPhm>ww$vCLR^SOiVe3dVIWk5D#=@}TIu(y*V34yb zS7;1fyvlm3P{ybLp5YaW4=1W1aU#_Mx*U(oqGfoTE(S%T$cGQ|SV? zo`Y))xfNUULh->dbhru<6P~5Z@i^T4DePG;31!$oc|y}bu;8MLNiR%#@OlO31qrmy z0I?Rm*`n=AZaL@AME(i{gR~aoJg|rllbrE=@PL6@SLC&>tx42@2L#k|b7R>PK&0lw zUVghW?4emmi#C_jPoA9>_TH7A!$xwe47={+Sn9RTeDvs`uF3uc2_ zy<1duA$oOgKkSd%Uww_m1_|#V#nZ(W!kdEhs-~YV3y6jBs0O|S2-n`7Ty+r@FYi(u zZ&Oimyz@lG@vamV$7@?CPAn9!<>E6IDQMBWhPQie2D`FDY$DS3^Nk zq+NRn(!r=s*AkCi8_%w_oVtJt1X)ENdu~Uf& zr_!a1Q>kR9Q>jAj)J`)kr*>c!B#X6E^P*G(9L4p=TFs0NvGHs~yEn7Rn|~)n!|%OC zW{};8KDS2GPB)^QHSk%!MB^}6T44$gdvt_S_MhAcqY)3V*AukdNEv_cMyeccg#A2L z!d2E-i9W7rY{Mvm9 z=3?aYlwx`IQmR9Hb_vqjvy}ZO&n_j|;3CkV53$^O8B!_7?RZo50<^UwUj<39)8kQi zi2@UV&nP3lDstr`a*^(@GKZkO)!lr|=V!+*h%ywFj0{HCKXkt^-@I zqP!K9XA50T7kg+WF6c%Q^u$LN7CK$4y}%^oo)CZBhLFa)z3$K|63>VXU4v_3dZhJ6 zlXKBg8@&frGGX!zVphZC(e#TonrA(z*2fL?wk*vnCI@7%>RHdP4AJ z7c)cfB$;+2T^ymCaKW0GmSPuRiZiK_x$BS#dn^8nYVo@2ap!tu%!Ri-fkwoLI&L~| zr5yO#6BvS=zzHDKQc_ND_<6iMdSgC<&plCi9EP`j>E$XMj}%tKI073eNjoXB1JB{_ z!X<)U=$JC;Bz@H5 z;T9B}qO&QMI!g4OtFVw+^m6#whl6r^c+evWQQN^U=sou_ToyeZp8QCE=MG9f8NTI_ zyz)ipMEbA=W0ftm6IT`71&X)XpT=eGGa!LsFugN!8dPv+_?JgAOFltxw^*maIA8db zP_qhthHGJ+KWw1 z!8ZB-Vks5xV(Bk{-7Ni;DuJcP`05J%(rIZFm2J25=Wz8u@U}8pM-Q1hyQ9>G=VV&0 zAlqi2`vTlmu*_y_KmA)Y2sxgfnO=w`Lx#c7_y#Pz5ntUxPN>Rk2*{hlyUAmuUB(c=zD$ zx@qam#?15FwnEctEo~O4v9x#S+F06B1ZR@Ny|kRnhZT{o%9Ai0V!XAQ-&v%y`S?r) zwsjQN&F_@#Y`*!YDygj1eynd9RMJ?2&sXei82sQ6O17>+0I$I8gC}K-*8+DrTB5}< z=iiF_*4L>|(ow8%2ww1|MBoj|%1|Z_=GyLHwHc%qPneu-k9T6!?F_t0y=6`Na5Bkh zC2xUI{J@6^*lfmFS;ymXT9l%tYQZ3tisOcjm*> z!=K1Y!T4jX$~b|Q5%m^^3|pu;yyA((!gpYw3cL#%+7D8YM>ig2VpyjI(zEdOQtqvH z!mm7$Xc9}{Rqyw3EnG+;N+}f#g9cZD;9!^2*C36_h10Ymnh<&)6!M(2lG0X^7gIrE zwjLnz0Z8aW;wP{n2?Z*S6Nr!ZXbp-NzOntVs_jxaRYFVas%cma#M8=ugS7G(IsDj4pE1h31Ss333< z2s6;fxGbVIMCVY9v6|LAG-lruPJA*@yKlM>J+a8Em?6X&SXA{qbza-_+2`U&P>9tJ z3b8L1d1Fr%6L-^R&Pbm)b|_Y3Vh-@ZKzQMkc{La*=-5%<*hDnp|55lq1{l=~oUI5< z5`soG9@%h3<3tMnN6lhK_~R#Y&8e6K>CY+7&so^tDGo5*ko{{}4-tun+fFgR=OcMe z@odhRBD@X2bW=}C?_qi^=lm<1a>|SLS2)GjJ&Cqu*AH=uaoMEv-ke{{oZ`9aZ+kh# zwLK|)D2LL^tG+dHS&>cP_Xwh|_WM51Bi?eo0ijgRsV$=XZ!?J=V|AyqilG68YOZrI zH-oBKT1vE}H|3P}rSwJz(brSA;Gy;A+ASr~;@R30Yh%RR%8J@(F{|>#S~FfG^=|6} ztNUCU86z47RMa}f+m0hq+r%imG7ux;(xPgd;+ZV6`k(L$o_-g6+bdr9`#n(^FY3j&tGLTWs@HK_@8z8HQ^=Hp45CAOQrVN4+YIB7!|8MTPHKo3{dglT-6dAoaJv#NH>KHM!FLmOLmE>s;>#0TqDD2G)%p!Du(2e2~lWjr_=|DrW zzZx(C$ickr2tGnMX@F!cdU%9T!i^lCSl;jP)FHwqA{Z@fya-yHc&(p!a|$gKEp7+8 zvls0z-6DZ`mzvCUi*$x%r6g)#=%<1siY8YPDq^%hi%^{C&*)$yp=43d=&E8uX?QV% z%4KB}$`wNyc~c1$h+&K#s3X){oXTiTnmM^@spu=lFx-wRmWnET)<9|wrjTfjIE|5$ zl@1c8GpZ;dUZXgN(T6@lC*eU8mAj#W&`7a>(Rx(BRE!o&7>#2?$71>*$p^9~n_xC$ zSR%Nwk_w(KmN9y-oX}KpIit6#EOZ5<-fYZNv7FH-y@+?NSjp%f_RMs#iqV;c#G56q zVI+qTI$vDNs6MTCxj9746W1}E2slJsBEpQ$V`PfejEo}UEf;GToyxpb;tocirxNc5 zaUY{1<~<}HV03Bzr)5LL!{R|k`GYLypl#Nvf{ zi^^XRPl}_=tEwm7Ga7BnAhcWj%)D1}7nN@p|HK-fe7HH9(0d{ZN_^sd=6xyh7`+;x za=(fiN!7nUVBUZs!u(yFz|1QE4+y_JjnRU@ya9(qik!k|J@YczpZfFv7kF<<65&F&Y{{s8yP~7{0>r zBKa(%PgvN}1Fb%Nf2WeTvX+jNZqi z1wuFdnMlg|3n>$76DP~4erChs*Ox)TYRIq94Q&eGoZL@jcsH#Vr_Dd_8jFD#^q@JM+(y`wB>dkKMk5(}L4U zOnWhXt6-`Lp-Ki%u&ciSm12LwO`sd&$?bc3+zOe6+>5uTl4CC_BX12Hcn4@qG&y-l z717Ens&a4nJ)ql}j$zH?xYz&XC2ot40(Me~@H)k5g;mmYiV3AuSo9_OudL2=1>O*H zii$o=hY+=j{ZU1C4QCeKhiW!fJ_Pz!)mG5=%D00aY}|n+O^Ur0^eOI|Rn!rth)$&5 zJ;YgWCGLV~i;r47sf4N|8@pB@MUZ|}Xnlq&@w7!TDMXiZ`nO~%d#o-F^i?jks-Du{Clh^(jeeG?#l0t=YJ9DT zhNj2L%%nsqHut4F`wr zaGU^xb5l@1uI0^ySx@0J94y>xXC(RSiVniKd+YaDh)b z|7EmEimd}DCX&@jXc@Jj-E_<7eQfB3Wn^4!260z&Gj`jB4X_$RMq?S$X?6ED zS?zwrMHRoobOjswT6(R+jzbxu>zR(?7O%y~Zx?0RQxol?g~yDTeCoc+6ms|grvEW0 zJvEBxT6hBczg%WCX9%xd#QDF8vx}YsXF$b6xs=|(v=IR)#jU7ViYCl30$I3Z^5|scSMh zUCi`rvvMV}zHOjlo4EQ+?)#l+w-omTt_Qb;?WLAs)XSsR{F7Ug+jkwfhcVfpgP1Z?=b7PYiP04m~o-&A=8KX z-W_d7ac2b4e@7&NUTXi|*Ih+)es>k5XsyDDLCkKhlG2vZR=(AYziPl*EUT;r3~^!n zm*Q^sqg>_RwT23mv%O1Pe<#`1xc^V`Cb#1+D$wIyQDj5HX^YC3<}f|tp`3p+Jp?Mn z<#8{fJ6bVYO0kWDcN#BDu4nok)1pM0R}b^#{t-|2fh6hz^CrFwNqUu3@FZmAT*~>o z{qG^ag_jH#w~?0{H)EmEJ|x#iE{8!O(Ds}YBX2^^D5eoi?Vwp=L;(%DpLmH5jUsxB zS)LFh#rYLIW3t4^s-hUDIMG8B%y6b7nEuSmhoPMIJBfS5MKpoazjAsX-|JH_9AU!c zrQ4@Ik}-JJ-^8>4_l{G{ETN!&hZnBPl1&1e(iWrQM^dkb5np+G!k|wo&yOh=DV`4! zaXzL#0e#;_^gm9b1KmWYMG$>neVs^>x7gyvKg4yQQ=Be4CN^`Ct}ttX{Fq00N5zk;5@X2saah%DEybh|c0CDDD6 z+Cdf1gwOG)@LUPq>WTgu87~&3WF?Wd;v}5^S?X3xQna00`8+D%I~9ulS_STA3|UTb z7O#45rnC;&RfDHuYQ8s*s3Sl$XUJ4&qoExuYz$3K(UG`5;NkD;=D#Aqn3b&y>H zlH$cASq*x%^F+`!;&jl9Y_mZJJBd2oCXDNPf5+rq$L(I5WZg}F&0Vj#QcQE*j=fyoJ z+GY$GjR-r%PWh*rt{M!yDW2Hac=4m{H-9{S@8yKpld&ZeVp(DfPB*1!X0%5nRXhm; zF4CwdH3vWQafwFz^0whHZZRXPoK0NWfu2dSi90*c^FUiPGF!?d&h4JHpm;IkB``nM z=(2|Qk|M;Fa_xd!+D`qjWD(oRH=s;g3^$^!E`dkjH{w~Q72Q=zY zdL$`^x3xliE`K!ps${nEE@sq}OUP;xcpCkP=H`o{Jc|cI&7^>suOV65 zOYGK&EG-b3cs5H)|C{c^spwKhpGz{WNW7~Nnbuq2SD5g(O#G0VpIjnJ_;h!%_$a-1 zazC+`k=0H8#cLLF=lHe>v%lD{VPBsQhpV4xbX(>&Q7-j)2hW1jmWebag|17T8&t(QJ}gexmK*#sIq!6ka?GeM^SK)xL>2_ zK0cs-XjD=;EV)iRrO}Yeu|UsjG}ALFxnAI{C2nufGYjZljkb8^CpU z0S_n08tnydh?uRBJ8Gx*1aV;@&VQ(%Vxm?hpCGy#bE3Gy5{0G1+$V~AIuOn@#1q}n zZmsmO!P~&wr_nzL`+(lks5W<-7%o20XmqX*=<`B){YtmKNqnoBpY~s!+$4@@bfo_+ zKzInihQ5owA0gZteU82#AqjXiuoPis^N z(@qmFXjB2yP7|+d)W3L}7$@G-=!9Y)(5H;dW$tfNUP^8jUumX0^>v`b8ZEAPH+j4` zrqSw(4}csv^Chp%uG#5r5m6dlT(c^9UzL2iIKe`u z`$jO&5F<77PB709<1}hU6%)i+8cjwO6T~zNaTRBZ^ShzTx}lZb(AsY3HjOap9AbXjI8IF-h#!XhI2&t;I_k zolt0Q6K9LJG(4ry2lSyvCzWo)r~IF5G`P0%oHE}O75LQ!W-OG`gVILn#ZyQM`aayloNBri6qWZ`}}jwq&@& z6!SF7$$L3vk=W3|t8pz6-bza{C-1G4rJ_QkvpfeM>P zq+TuR23iH31qV`Ai8nQBLBVUqifW5@mZwK*SY*=c6)@&3PeJM$F~0-#PhBhaYUC{F zk-APyskJ0qQ0``tGpHT)PrX&lGc{~zs7t+5tk>wV`Wn|g;*dt)xlT&mC}QiZg4=;M ziwcc4rk}@kyvk)`yykw<596!qm+i0)PIPg2Ge4uG)zg| zAr3cK=8>HOj3zFLke|YqWT|RZCJ|6-ztNRjIFwH#OQ@wU&~`Mi&&fq#h8*@ca?n>*16KQ$G=_hXTm!MXhgl>StmfA@DvgdNK9i zV&gCi4XgPu^(%2$qul)Aj&DTHaEq6lU*q~#G%>Q)dWS?aqp&zCzeqhK_BD~D#rr|L z4TSm$9!>p09O__-v>%0MM7yXb?TEmmeqAso?N>3)LgFBXZYfu2bgzfdLmIu9PRKiw z%Zc-{Qq!dDV@so`Rlk()dmNJS-yX;NbyaEY$lT-boGF$yDPW{P@h!v;=v0G@)hUNZplb z$@0xpm}d&ICRN@xh9O+~k|SMi*NBWsmxsn$1+Va}P0Ns%p2o->J@A&aOnIe7jeT-l znesY~?&wSCCJTux>NZ015sl^pWy^nRWUi}|B1e9v;c0c*X}L0S996McZ0YM1xpLPT z7Mhp;Ab9o(7RvW-PwOS;Y4j*ik^D@f>F%9redIM~k|cTUKw4jUl#%6&0rJpU%oAB= zD)#9zFp;7AgVOA@Dp{h@7p3_?Cu#KAz?U2Y<;fa-JMhc2fpVHgXVkyssFrgynpyv4 zTD81Fqo_qQG1^02Z<12XofIb2+x2py zMidDRaz3NQ;`V_!9g#bxlH_8MkxA%^a|w}yze#J5WAGym?ZI|}q<8DugKenXJcH0; zL0vdp9%97Rrio9aB#Uo=*mA5fk=BDsERc_RX0`XM& zkVd)))rg{Wob%~0(Efakkzf7B< zMRz+66@HmEQ^sAwRfwDMO4+OqHA}jo%!;P;PTmDkXQZ7c%XB#-dvyA2IZmUNiV5kh zatR|E1@OM5yy6m`|Mm#7?RUuWktv;M~2Rmvw&#+6DJ!Nq+cNSF>|q)3N&B-xR8zE zxClzDx9~?%vLrny%e2=LVcKP~NuvrzQ#5+U_%iJ>d4)!=0WFi8E#yv&86hs0TM3zB zvAcInAMfSzNzGi*XPdY}KF4UW`(YeJTp?ezcx=pa*}9005px5Z)0fLdj4aPwDIZ!) zJj=mX%A-pyM1Hzb-gX%w%QIKXla>)O_Xu*$RdNHvJ%XILLe9CIn9IaHC0o*0$U}@4 zi=|mR)327FT|vCXVoLfy)A2_0RTf%X{7(Ayatb3HLSZ1kQ7+KvR-7Q*D05bjMXO$|~F$_ex_O@fM5p{2uA|$_kC{D)MJ+ zmiF})Z>o`%@qiq2i-qzAIK>vZ;8qL$m__J*M$1G*F`_QUAUvi|(~})P--#d5o;yep7y-dDPi&$-4VUav8R-<^}0*%YzIp=kJ%X zn~6#H!Rn0n+<|V-I3TCpuhkUo%=lEk%4o5;qh@c$=kkgNEM7has4wJ3 zjmBeu`a*uDQGNPLrsGR_Ov48Tyabf`pjGhh6hdVdav!WARISkyj2bP3yW*9MFXd8Q z?gHFuU(1aeMdGIXuiT~44?aR4bd<|aGyf~U>3~l0jXcU|i72og%=kw3e29(V8}E=T zVzdOGdL7I-B*%5(eJ>|6`dBvOHQevztcR%_-h;#W?e}svBg=L=Ve@TRN2Uf+kHzAV(jR5X)=tSEWho;|@+Vo%Xo>h-{X63) zXf045pS)#wn`I<+fzsk=Z>y-Ree$L2}JSGom9!VaP6QAgmJSNX( zw9I`t^V^Kyb|yM1b*f@sF@oI`go7ar5c@v(fW6}LZcZNtbdnl zH2MNZvqIgh(N8#<73yw{7UB3+ss}V$iQ`weLZeh1zbdu!37-F!xqIXIRjIw2Iiqx^ z*QQ?8=%Uh9$u_lLqx-Stva63Z+J@6IyZVntKh^H^I@I?XMGab&>`=dI^i9<^eAjB* zL7rLWK3?Sm@&cj#mJ?knM>FGUwh5Oi(kQLQ2h?Aqvj-l_aH|@P<_&aZy4499TM!YE!F5&{B~lbx>}>3a2n`U zYc%@Z>(BJ6TQqtShqzJdZjGM9A#RjrO(8#aOr9AFp1}=+cPO{0ZuFjZRW_f1>)8 z(Y1*a%g)M7Qm$RBg=(FtR+FsUFgjJH}vtO|2gIin?FtGgmDSDn~}m#0qFyh9Ol zGxOA?jL2;Btjv71vWsZGTB}93IxhzAHjU1K+X~c1jZ*ECGYi#28Wp-O&MZ=oF|v&5 zqn^>c!M4jY`=~E8%649tS)yKjhRj|fUUxreW|k_~vjiXGs{)6M$e!N2dMrUy#~n&Rm;fIGEkkUd1TB$HA^Eh#;jIjc6V5lS*xyMWEC8w zRx`3p8>DXQ#@nRJ&6H1M4pRTnXsz>^%sRDGqg%!6nS<5G8ZEYcoH;~&snH1M$C)RX z>N^d6?r$oICZhoOtbeMX85hja;_ZHT*IXQq7I-JX*5LEm#X(Sq*azwqPvm#Xti)NVO{}S;oNy+U8o@bR>nVrQ zN?7T$t10K@7^06dJ-~Ef8e~ko7DkL{s8%}dW1g`uAnOX4AKD+21++0jI|fM*O$mRR6QpvpKlB-h_WA|JFbses+6m1{vK|-f}*L zq~egYi4h*Gf9*nX#NNz9FZHxdkfWqHp5IAN>P5v?K+-0zWqB)zY+VA%O8>QQy7zhp zd}-&dG2spL4Zcd{9{TgJLpjzTUQXw+y`FE#of$s-V?s@wb9XTr`T#fJc~DDI@oXZ+ z8kWyWA=3}STsvkHij24WVNNmiq%x45b}kRbf2^Qu^2`xY z&n2VpFQYqaOBt0iWq^v=K;c++=AU!k;CuR8cmh9^%dO1f;V6if*~NYgN)ACY1-^mK zB$+=p;D49}X#D@HHVwpRrNm2BWY$G&?;sASKdSCtRi$gqB zO8uAAmxi4ek#>tbq$9+7q&?z((C#L7y;J_#r2!wKda0}b3h|`GUbNoE@qw=dcoN^j zIbS#8XD3WJhyE4y1Nbcpe0b0C|1}mDxOtFnx4P4rHa#kkx0E;sBZN(yluz+I1UIBZ z{DS)k--f_XE-~Hf^SMPak0NB2Vi_BA?aXxu(_)jW@sTRpp~M6fXm`8SGAn-_T4NI_ zc@*;!_pU>?lGcDqj6ut6V%!ko{ss*W*4D1)Z)Hq&9Tiqw*oMOGt!a;p6A>~>oB(PQ zXMx(qnH)rkb(HT!_u2UF$Lq96WBx?GO)N#aeL<@b;Fjm?dc66DZ}oo$%)bsO%e{3` zoq=O{XdTwUP*$|++4%%CQz5)*xni zg$6M?f5M;S30HB%O7Rd+zC^9R_Q_);uL_&E0&7Mn;2>HJL)#^-6%O4m|4Th4Pc5B_ z@SPTvwyU!S5K`AQM~XT&r7Qh&1a$4Ds%$c--Is2WgCQ+PG~i5W?b$Y2%P)Z&f*ye; zfqFzQP#=C4foLVudZt5}j%GTZ>2%OI{GtGry_o4@&cBw^n?O^<$Dmo_OU{veK#q^H zv4>wSTgrNi+hvOh_R|0-il0HV@Z(Ptu~&{zv`0=AuDWKV8?hyhP_DWOFubX38h*>G z30vRMiYsQioGZAIX_0s%?J7|QdY4$PruEwf>KXC`==|!Z#kG+4i6nV**2|!)>diNC z=@<9}?-93@9T7LG-F<%(x3khlR@cZMK{evl{ZiS8&0;wyea_U#Um!HGOcTpAu}l;8 zv*oghWtv!Klf12AvE0NuH>sfSaybU2)*xqizjY=qd!jCvkEwgB??d`Uus5ks3myVp zl)Ozo&BZ_~pG10W`5w^s2fqZmDEV#q8guunw`&i`O1U=r3;CV6ru2}Ekg0eReYv{2 z`X~9Fx*x~g)cOSVvucV?Q)h5fD`gBw!**VSsSLR%m0t8C zCr{z_O+owCswwP)Dcrs(+`d^XGmB+rvCJ%%nZ+`*SZ2AJp7Jv5e^Vvl5lVu}vVAb{ zJ(XqJ9%=kYJ*FP7{v7n)>aSSn2Q?nifB>}ZA2?vJ2-1If5fv{ z&|&3nE!5n2qJ1BK(6bLJ9!8qZC*RcEehr80Pm3W%>5iv`$jb*kslGR8Q)4-3QZeP1 z;_IZR#V*hTD0P<@ZM&>;xMQ^KBeP&Q7%gQdJBHdWAAEu1YpwL&)N8?hte%8+mJN3t z?#O?@@tS-*@+Htyyf2|tS^c+;W8iw6cKGawI3^ybPj?;@ulDK%y1!Q+=TO_rY30uG zwxek+POr#~m=F4+qgi;xPL~;SPO{ycb*b}de*b8+?FH8(NH;ZZcaFB5kTTp6%LQV& zKr9M`ockrUX1}D??3eF?dL>onl}%-SSFEI)HWv5#2z(pqticvp!kDB}vs zxPodfyB{>oyFr5PS5xn+b1<1bYx83t}dfedXh@rMq8u~{pkUu-holZE z-2p*42L$CD5WONUj97~N%OY|lbwQ3?S5I_$H9oYp-H|uku>xE?Za2Bzs~O~2S!~d1 zra5fmYNjig4z>MO-ycSp)g>M;s~&25zu;QOdM>+xJ+zrUv|e74d$VT)m)gMG^^l+8 z+RUC?ZhNQnQO|Ol>8Rb|S<3C$%5B=pX)oJ5)HbZ796z>cdE$hUm61bjSp|xdOzb1`kE{Inp<`lX}XUW z=0!(*&AxnE#1D~CkKv1?aqh2K^J8L514Teyfas+KM1Rj2=f2SvY%oprH9PGXJM9=d zXS8k5;B{WFqUiN1ifubuwno|&8H9gzBDWcX>-_2~qDI@c6?_xrWrLm;3Ep>H)$GM; z%ZrNo-phu@LcDEWZ{h%^;7bd(aQyW%$0+jyYM?4RG^G$ zHPc2=#I2%0Yg80yO`LAhX$rcbwtG__k8FbEU1AJ#$1wM4vAQtdafW&$ty$1$Tk4yF zG~$02Un1mPc+{CBSH`sZl4No3C7^kjLgp#@h;5#t*>9erd2gPg4|w}>i{~kP#_OA> zXja^0b9=V?9=5fVJ>}~w?}&chw^UtI`jKyiBF*y@eb+lr(FesVR4O`jo}#aQ=b4H= z0q!Lyrj8K3WJ9Xon5XDN;MH95YA)~^lwxCJ+YvR+c+IxcNA$hGnV>~^=YlRuo^4dh zE2HNb>$%i=HfX(~FL&3o(d!j`zq=mISgY27ztc{*2m+$Rb+&P_d<4H} zcd^{!yESf;?XA2Gar0o@8o7Y=m`hm^-Oan=ma@9E5*j<(N6{&@Rb4C&^SZHS3(i02-a6Ku>ax0c~=Q2R+$& zCJKZgllZieznRpxLZ{YlmoWB|QnIZ@$=i^%yq77mg z{@uVx@px8m{JT5Pf~JU1KzoX>Knq2`?B1fEFdK0hC`N$}7H5DCXU3_Zb*yf%m)P3Tiyn0U8b9$n>N_?64p7X*k6G40;~Z% zT)QBEhijM%j&Q+R6V{rrc7wGWtm9!F4{KNaG{jdPSA5*cp&%{Fi-I*4(owuvSR)A1 z5dFLe0$d(V0$gL71h}@`VC@EL6bMJ+&xuPef_3pZaSjI7SeOVxTt%EPHDJ2J#KK%4 zltQ%gBZ%NUFg0Mh!bHI=f?0e{5N-gD^qv#vU}3#*PMos{YXmW_YT%qW9}m`?FuBf& z^Lb#c0aNpwIA0sqt}xw*Z-P*DZp63*ci2L~j6WyNPlR<5%;Iz6{8CtBVD_F9=f8pV z!Z~ryBCG)tc-2X84j!zzAcD}L2U|R_r2$j(oVcvouy%#%c21n{4r>(5_;cd?L|7NW zEIudBuOh(%3>h)7(|b5bCI;pkm>?Do7GWaDah2l1MA3n$5I|7ED+iMkCJ#&vn65BAC_50H0w`D) z!NkDC!bDKP4a3xciJ}5=RuLS*z{J8tP{UdOLmHgsIw$UZXuw+YoH)l7)^6v-IVf1i zpA+X4!@8ITr+Z=DOH&O(k$75M1M#%Dgd$iMpA+X`VBLF8oP&k+!Z~pcf(~9?m>Mu$ zVWMCb!NkDC!bH&H@@qhZj(=A;f`VBD69W?q6Ttvi4bv4S3TE*+am~A3=s@)F7hmjv zPTzZBB3N;yxY%$$z=89*U_RsQK#U5YxNxaDJdh9aCqcoefEzE)a^uBS7Z2-rSTFIx zl?sqSAD#u6O9Ht33xYTY5W^5i7ht^rYd{*82av|);es_6tTkb+32Qf4yTLjh*72|| zhIKKldtu!R!jT0yvH)v925vwGZUEL?u-1gNCam3H?FQ?3SjWS<7}mwG?uB(PtQTOt z0Bb-Nu3r{cf08WNfuImX$l*>LTw!8h;>&j+WCc0pVP8HC!N=z+kNX`ff;5`12-a9g z6YyhUt)YO+Sp*ZI2$zD1Qp7b@1QP=j3lpJ)^Es8^?|)vU4g^9#1GZdYqF@%m#K6SD zL@2|>U~0g0g^7Y$1QP=j3zJg?mtO;>D@+v3BA6J6(DA>Z(t()cN2tP8!_~^VE`o`HiG_(!gUiBng^7Y0uZF9+2-X;w3y`zQuc7`1;meOvhf0BrSX{t} z(7>@*6SpN6AW{kdS~zmS)P(sKB8$KhOnhxv>);xQ*Tt2M*M;{ebUC-mUkp3Nu(Jd? zJ^c8Wai0+#h;#fNFpDnZb`w|(MOXxSVIuT!7QP<*zG3YF=^lQR9s0)l%9GAyHkqFWtDKys-qK}jfq7=CxL=4O+m`W5l=acpc2o7f(j0B7!T66~N^j zV8`(pOetDhNH@c~X-O65gFSEnaFs!k%1gLpe>l@28E&KHzNj5O*v zja0^Mq06c`MkrT9jsjfuf1FYkoR3G*0{Q*$PW(^m3vHY)4)1hp25nqLcDgvKtK*uF zQ*YsFeZc`3;(?_=G1R~J3@C*-2$UfJ#1KRzH0N&xq=z9Q5ubr7NRL27B1Qq+6xCx8 zksp zf1bR9^aeyEViV|t^cF-UVh8Al^k0ZjUo$WO>3wORY|AMO{hVOg%xpLd{N7Lo-kFmxhX#h1P)9f%Y2hCM_dfC|wI(H{B@R z0^Kg11br0!7y3a5ndWJTJU))IO);~T9*B|h{l1p#%z{wGvC50%eDv-8mWJiK!rrk;nn=i#IC zu<|^tKM$ME!$9e^5X8B&qD3~5TukMO3hK}JE#F|{p1gvJ)304+K+HOLrf$$^$0Xj!IF z#nYt&05lyFpiAe6*FhhSN5jAb&@iY%VG6X=AZHlh?WJ-h^qQ}C@C`J7GeC(Z69N)1 z%1F3KAXK#7eBDER-9kJyBRqrEuDQ5CHaZ2mkBlx70a(zxLV(2uMHd%I8C+%qfA0`) zH(&24Ph3t(8MM|VUe;jMrW z%Siv$OY(pOeyg7{kcey*Py>vB)I=TQ)YpO1AjMrLd0Nv|vl`{sy z@X?qnKr2m%XK;u~uouD)T68?Gg?hElm;-Ns);UW+ouF0G9$+9wn>PaNtsmWhaiI03 zJHSkV{t^iAwVnh5HN>rA(NH^p6#98A@Tdg^6d(aEw8CA0vsLykz=k9?4};D&0uUkS zC+Pr(h!BDk`fdn-Zo4TV1QF5DjW;F4WoTGJEHoQbG<4tYzcFZ(mpXbN9bo0PfgYIP z4n5Hz0C5YN{>T&hJ_>;1-jEXsm5_i+LPrFY^P(=oHXJIC^Ki9DK`ESYJJC@6I7b?G zLZA~JT(19VnG@j#w-5@2zG$vQXvTE}58OBb+(7GoIzWg=O-2q@+c_|p+|%x`xZL5te_k3P>@giD2A z#Q${&8E8?>05LSmijs}eg-8ci18k8aY1}BGycFc*=;eGs08LW>a6>co0>tEK%L2dx zPlrezy-@&2F(Hw}xHcte;}waa7LkOU03<2VQiae|kjzAMZ2xH$mxfzGVqC4nXpJJk z3Y}jB%&~tV=EMUiY2&|8QgT9V$A1H8Q4gW`0x>Rveq0Qw0zc4`#eg==3MDbF@)er+ zc(|1nv~USf3~aRyl>iNR#6$>uO56(_R}R#-DpUY1c=!aINTBu4bHE%QPmq%otx*Sk zIKAqCVk$~b=t;HYsTcx;?RXxDmxr8TA=I)Vk<RUgyF?7+*8zuXB4uS*gL zR&*Oi(-H)Wraa>bY~tU2Bhb7j=DN|v^rs6wRocKS`TnB&cx7b+OLY3T@K3WHf$dL@ z=a0J4;ysTKh8|1*FfMdLgLfX7(vZ6mpL&t}y2wy0_-}8klt#xeIZ>JPbK6ZD!g~qN zlJ&7Xm`v^^6zglDsG3)tdMV$-?k4F=#O~445-z-9&RApTxcyoz$ybDcJ-ecJ(0_#~ zNd|0|b+gsYnHk}TQ)0`X*x}_j>S%a9F!+Q~w;LcQOsY@fg&r*O4+91y)T{0bTo7YJ z>UhXt2#N?<5ifGJVd&cqMgb(TO zx!sP;D+|@2Sl$7)7B92~K@CZBb`L`&7N*+0xJ2 zpYru%D-zWSj&-qxz}dz9-0S2)a_$Da``<7eq^Am-dx_u~-M%e161cimWS-92o1`!#PTGjb-!5tTXj{%^~( z;^DK8jcTCxx@95xW{uf(WH+A5W=(eUX0;jZvCqB*mgbZ>jDPhwzuyMaiqb|cqAXEl zC}Ej(TE}=-_3=5`KQm+z{-U}9Y$KE~S2XN3@T4du7$k`5nd@omfqK;dyoc%vX@iB+Ph^$R82dG5^xc#hUG>z zfSPK&wKf;Q9G!O66*{XFa>`qr?uoy(3PQPtUwy8{{dq7X$s@y{6Kk>k$=lt}jWRU$ z^`39&f5KPHnMX__KqNchjF>~}*Yji(C}$_5_z`uk;|<(ni8cC@)WTBK^~8aUfo%^N zs>Y^MBM@qM$+sP{olqPIiWxf^7pQa1l?+Rdl`b-I6waYyrmDohim+>K`26FpzwF9JLJpLpH*j{ z?CaUxkI-Or<#h#InXvd+TC5sY6s6-pr>CKR_9>?U}FpWcAB_RKEP4hWo z=)TAYMU*(ePdcrokSP=Yam2;K`kU%WriSf6A3O5`zKy=e<4JIV$XEyTDa>KgQ% zp#8Cr0kXp}rvMcIO^+xHT}BB&!?fboK4n#ZyWQgP;V(P+VU8tM5`ApM5c9 z$$g*WkCMW8V8k#a7(R>(sufjKAkt4bNc4he7XR5lDK6+q&O8W9;33Mdi+3p+V}@!( z8KG8Db6tEM)L33uYYvh~k{jP_wL%c1W?F}MVu-MR=qwi0GU^vVJ1S?XrS_cnEx)7I zEKmbPBMwZirwy|kY}{Xw!KSP7ou zKgBzyY=chJ_sKR%H`y`2FH%dRL@^{-(oF;i?O)_H;x!P2;?m9bJF-G{Di<*!}WvhCCX14NFvjB-146Yc}W!Pt{}g zSt;)rmlUC6^up_Rk0}~BTTlnuNhf?<54CwcD4@@WBPJZhksNpuQm*6|``yNZZqD-c zuFHT$y>bx+XQ$O8@fVZLZTsrNFYCusUkJ+b@V|{v$22yzqm*e>s0N-#x}ap6bOc*z zHZ$=1ZcywYrvNX4S@t%PVNVdHgk;y=oORBdWGD-#4-^?@@Nwm?EX+goa7AP^#47`R z1Hd}whsrQg2IVx&`j8&Oin&MZv?*nOPa1kO+SL`RV+T;08s zslP6qb98s!FxD!NvMR*!-%XMl^1Df;Rr&*m+8H~4b)X!?&*mriuHPq_~e*(y%Fwy8@OxFgH; z3;1%X@z?tPT~8ZbzB9U3q^&pS-lzoWa2G>?@j`Fp$MZ=&w48vRbOUyP#{1_tnjHU5u?0$%!Q@i zyiwpD+l|N0Q$Oj3&5y`vQAc)?4fg@vL>tFT$1GEb-g zbk=d6P+^X?W-n}(q820n2w+blotXkH-hzyq-7028&V_*+pDR$pk%|RXaWsKu7;m8O zRFwzYHknUQ@Ge-TF(0ph=U3ql=8H;S^H~CFac*CDQ&Xj{aYPsB`*%iol_$#uS-YrM z%K-M^al8&zNpm7pd$4?)pS2Ll>c=uXP=N$L1|E}cddEbm^tdo=j;R!qu})r4Av*

P8b%%bNo)_H(Q>V=+RffF^YkF&7xfL3p%e-%`%tYIU+M+!yMn9 z(Qh)Ml#v-;UptV_RDou1H=qsmkeygz4g}dr6*ACz$c)9U2K`gC{;7@?bhX&GwQI}^ ziskg6=vuSRc(4l2^TY}hdo`%VuYcK|ez+GSjqO5FdH5)--(%>Ekqw+hiy_nyE^jK- ziS~(O*oLX3S?5V-5wmRj0@#^I#ajD9g4wi%eN*hc$Y`E6gb->qBCoOsUT69E>{PK&D^YsP{1bH~@!c*OMD0#~i(f3!>Ounc<73^XfXYW^Ye)RW#n> z5wP}|W!)i410(K0RgW2Bf=;|ev7vmdAk}ON2h|64L|>zZR{=;EcON_VCg^p2edfZ~ z)J1+wv}97ZB$qrvT;{@DW|mRBxNG|^K|6U{7fEY(5)v(aHFndl>>f&ev7O<~Zcong z{(<7U=YmQrkg?aU}xPwh98rl z8D>jm#sw1;LxEWT6dWUL#1yE~BS^ry*Gi5xN&gpr`U!viO)!fFr(P1;aYl!JW_;D1`}I=*ysoi8Mm_{>6+95dpi5 zqwO7czTuPj#V*!imc)3Xt;2mrwR>ANx znL@YIJ)rycas)pIS&Z;7)l1fS!eg3+>9iQ-O*CdPvYRO)AUC?_0udO)QJ49K_COP> z08U7;s^iZ-&K=q+#cVmPQq+Ou3UfWf$oF{a@0P>ymJypfDpYvJ^nsnjX0(B`!~A$X zm|zt;W4CM7igV}+QiV^g(1G^vi5NEe@x=`9AoW7vm{z>+B(fZ}7(F7A9n*qZ+=tEu z!wVL@7=j66lw8?2!GVM=X%|cnkq9F0;c{Dnn1_;e@F19tT8#CO+`OsMw^zxBsm5On zszQ}^T_Rwe=`_YPn4!~_gz;JT5+8DRry9KYx&L*_2k1Mweh=jycawm1wbc|8bnhF+ z&MH_Q1ND9r#E@+|s`Ty5g#iJiQ+(KY&9ily{ypud*^5WukS5KU>Dx>Rxi2w7A2LWH z6gD~VStsl7Bj+ggg*4-C)VV+njg4_1T*y9tp&w_;t1h17LfsEC^oyr1_1s-T>uk^K zZVzxDFk~M;*FR0sk2B%z5zCpU?yr7y_=R=yD6iyitIkS5+7g($6t1(~!F`~r8L7ar zo;IpZT?T@JIqJ0i98l`rB{QAv3ho2x?BjO*Qwv^@FXv1&r-!kBUP83`fEwr<4hrh- zB#6sHN0YQ|?VN>U&gfs*LMyw`%0bY1XJMZWYsVxsyn*a%d}pP0qKfUOW*^u&!aR(L zD@3VdciwDhfQ*4L0errG?RTNC6G<9*hQOQPi~Dy5bgqhF+tloc6^!z4M__-|dU999U1p(_YPc1S|AJgC`9dQc%k`4)M z9!myBF)bd@b@yy`5c^{aw+|MU_3llzd|@335W(8@@N=(o8O~c_8oGz@kF!uLt7*sJ zJ7Uj1>f{K*qG<6n6RNpaP0AH)@!7#e!maXZW;w1z2tnbvn~xcyI`-*1v0 zoaWKHL0NNQuvj&5x9{cBIOX-XU+IIZ&1xhQKB@T|-GPTj{S+qkao6||{%QN*TO;bP5f9g>S9Ip(|G;Os+^GDW0_G?>&-?dhi{IP3t^d&+5z(UiPpz-9fUm}l;e7;wV?6>`nAc-ZC(b^AY2Y)`t``xj-={z1RYxWe)7DNuqbRpysqd~OV zmXswYb9ZV>dS96P41P_Vczf?>OkTOSX>=nh!%?(mnAP&dFbD0Q-evx�dF%Zby@P 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�N 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