Files
ParkingRobot/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs
T

261 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade;
using MultiWheelC.TrajectoryPlanning.CoarsePath.Test;
using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle;
using MultiWheelC.TrajectoryPlanning.Mapping;
using Newtonsoft.Json;
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test;
/// <summary>显式开发入口:从已成功的 Hybrid A* 粗路径结果生成八个稳定的快速比较夹具。</summary>
public static class SmoothingFixtureGenerator
{
// 仅用于开发时固化真实规划输出;不会改变业务场景或运行时服务的预算。
private static readonly TimeSpan FixtureGenerationSearchTimeout = TimeSpan.FromSeconds(120d);
/// <summary>生成夹具 JSON;目标已存在且未明确允许覆盖时拒绝写入。</summary>
public static void Generate(string outputPath, bool overwrite)
{
if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("输出路径不能为空。", nameof(outputPath));
if (File.Exists(outputPath) && !overwrite)
throw new IOException("夹具目标已存在;必须显式允许覆盖。");
SmoothingFixtureDocument document = CreateDocument();
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath)));
var settings = new JsonSerializerSettings { Culture = CultureInfo.InvariantCulture, Formatting = Formatting.Indented };
File.WriteAllText(outputPath, JsonConvert.SerializeObject(document, settings), new System.Text.UTF8Encoding(false));
}
private static SmoothingFixtureDocument CreateDocument()
{
var document = new SmoothingFixtureDocument { SchemaVersion = 1 };
var planner = new CoarsePathPlanningService();
IReadOnlyList<FixturePlan> plans = CreatePlans();
for (int index = 0; index < plans.Count; index++)
{
FixturePlan plan = plans[index];
CoarsePathPlanningJob job = plan.CreateJob();
job.Configuration.SearchTimeout = FixtureGenerationSearchTimeout;
CoarsePathPlanningJobResult result = planner.Plan(job, CancellationToken.None);
document.Scenarios.Add(CreateRecord(plan.Id, job, result));
}
for (int index = 0; index < document.Scenarios.Count; index++)
document.Scenarios[index].ConfigurationFingerprint = SmoothingScenarioFixtureLoader.ComputeFingerprint(document.Scenarios[index]);
return document;
}
private static IReadOnlyList<FixturePlan> CreatePlans()
{
return new List<FixturePlan>
{
new FixturePlan("straight", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ExplicitEmpty)),
new FixturePlan("single-turn", () => CoarsePathScenarioFactory.CreateManualGoalDemo(1000d, 1000d, 0d, 3000d, 2000d, 45d)),
new FixturePlan("s-bend", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ManualAndTwoLeg, 1000d, 1000d, 45d)),
new FixturePlan("large-heading-change", () => CoarsePathScenarioFactory.CreateManualGoalDemo(1000d, 1000d, 0d, 3000d, 3000d, 90d)),
new FixturePlan("rectangle-detour", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.RectangleDetour)),
new FixturePlan("multi-obstacle-detour", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ManualAndTwoLeg)),
new FixturePlan("narrow-corridor", CreateNarrowCorridor),
new FixturePlan("forward-reverse-switch", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ReverseGearSwitch)),
};
}
private static CoarsePathPlanningJob CreateNarrowCorridor()
{
return CreateFixedObstacleJob("narrow-corridor", new IMapObstacle[]
{
new AxisAlignedRectangleObstacle(1500f, 4500f, 500f, 1200f),
new AxisAlignedRectangleObstacle(1500f, 4500f, 2800f, 3500f),
});
}
private static CoarsePathPlanningJob CreateFixedObstacleJob(string id, IReadOnlyList<IMapObstacle> obstacles)
{
var job = new CoarsePathPlanningJob
{
MapRequest = new PlanningMapRequest
{
Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f),
ResolutionMm = 50f,
AllowExplicitEmptyMap = false,
ObstacleSources = new IMapObstacleSource[]
{
new ManualObstacleSource("fixture-" + id, 1L, true, obstacles),
},
},
Start = new Pose2D(1d, 2d, 0d),
Goal = new Pose2D(5d, 2d, 0d),
Vehicle = new VehicleParameters
{
LengthMeters = 0.80d,
WidthMeters = 0.60d,
SafetyMarginMeters = 0.05d,
MaximumCurvaturePerMeter = 1d / 1.20d,
},
Configuration = new HybridAStarConfiguration(),
GoalDirection = GoalDirectionConstraint.Forward,
};
job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d);
return job;
}
private static SmoothingFixtureRecord CreateRecord(string id, CoarsePathPlanningJob job, CoarsePathPlanningJobResult result)
{
if (job == null || result == null || result.MapResult == null || !result.MapResult.Succeeded ||
result.MapResult.Map == null || result.PlanningResult == null || result.PlanningResult.Status != PlanningStatus.Success)
{
string status = result == null || result.PlanningResult == null ? "no-result" : result.PlanningResult.Status.ToString();
string reason = result == null || result.PlanningResult == null || result.PlanningResult.Diagnostics == null
? string.Empty
: result.PlanningResult.Diagnostics.TerminationReason;
throw new InvalidOperationException("夹具场景未产生成功的粗路径:" + id + ";状态=" + status + ";原因=" + reason);
}
PlanningGridMap map = result.MapResult.Map;
IReadOnlyList<CoarsePathPoint> smoothingPath = SmoothingScenarioFactory.CopyWithFiniteClearance(result.PlanningResult.Path, map);
var record = new SmoothingFixtureRecord
{
Id = id,
FixtureVersion = 1,
Map = CreateMapRecord(map, result.MapResult.SourceResults),
Vehicle = CreateVehicleRecord(job.Vehicle),
PlanningConfiguration = CreatePlanningConfigurationRecord(job.Configuration),
};
for (int index = 0; index < smoothingPath.Count; index++)
record.Path.Add(CreatePathPointRecord(smoothingPath[index]));
for (int index = 0; index < result.PlanningResult.Segments.Count; index++)
record.Segments.Add(CreateSegmentRecord(result.PlanningResult.Segments[index]));
return record;
}
private static SmoothingFixtureMap CreateMapRecord(PlanningGridMap map, IReadOnlyList<ObstacleProjectionResult> sourceResults)
{
var record = new SmoothingFixtureMap
{
XMinMm = map.Bounds.XMin,
XMaxMm = map.Bounds.XMax,
YMinMm = map.Bounds.YMin,
YMaxMm = map.Bounds.YMax,
ResolutionMm = map.ResolutionMm,
};
for (int sourceIndex = 0; sourceIndex < sourceResults.Count; sourceIndex++)
{
IReadOnlyList<IMapObstacle> obstacles = sourceResults[sourceIndex].Obstacles;
for (int obstacleIndex = 0; obstacleIndex < obstacles.Count; obstacleIndex++)
record.Obstacles.Add(CreateObstacleRecord(obstacles[obstacleIndex]));
}
return record;
}
private static SmoothingFixtureObstacle CreateObstacleRecord(IMapObstacle obstacle)
{
if (obstacle is CircleObstacle circle)
{
return new SmoothingFixtureObstacle
{
Kind = "circle",
CenterXMm = circle.CenterX,
CenterYMm = circle.CenterY,
RadiusMm = circle.RadiusMm,
};
}
if (obstacle is AxisAlignedRectangleObstacle rectangle)
{
return new SmoothingFixtureObstacle
{
Kind = "axis-aligned-rectangle",
XMinMm = rectangle.XMin,
XMaxMm = rectangle.XMax,
YMinMm = rectangle.YMin,
YMaxMm = rectangle.YMax,
};
}
throw new InvalidOperationException("夹具仅支持圆形和轴对齐矩形障碍物。" + obstacle?.GetType().FullName);
}
private static SmoothingFixtureVehicle CreateVehicleRecord(VehicleParameters vehicle)
{
if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter))
throw new InvalidOperationException("夹具车辆缺少有效最大曲率限制。");
return new SmoothingFixtureVehicle
{
LengthMeters = vehicle.LengthMeters,
WidthMeters = vehicle.WidthMeters,
SafetyMarginMeters = vehicle.SafetyMarginMeters,
MaximumCurvaturePerMeter = maximumCurvaturePerMeter,
};
}
private static SmoothingFixturePlanningConfiguration CreatePlanningConfigurationRecord(HybridAStarConfiguration configuration)
{
return new SmoothingFixturePlanningConfiguration
{
PrimitiveLengthMeters = configuration.PrimitiveLengthMeters,
IntegrationStepMeters = configuration.IntegrationStepMeters,
MaximumCollisionCheckStepMeters = configuration.MaximumCollisionCheckStepMeters,
HeadingResolutionRadians = configuration.HeadingResolutionRadians,
CurvatureLevelCount = configuration.CurvatureLevelCount,
GoalPositionToleranceMeters = configuration.GoalPositionToleranceMeters,
GoalHeadingToleranceRadians = configuration.GoalHeadingToleranceRadians,
MaximumExpandedNodes = configuration.MaximumExpandedNodes,
SearchTimeoutSeconds = configuration.SearchTimeout.TotalSeconds,
HeuristicWeight = configuration.HeuristicWeight,
ReverseCostMultiplier = configuration.ReverseCostMultiplier,
GearSwitchPenaltyMeters = configuration.GearSwitchPenaltyMeters,
CurvatureMagnitudeWeight = configuration.CurvatureMagnitudeWeight,
CurvatureChangePenaltyMetersPerLevel = configuration.CurvatureChangePenaltyMetersPerLevel,
ClearanceCostWeight = configuration.ClearanceCostWeight,
ClearanceCostDistanceMeters = configuration.ClearanceCostDistanceMeters,
AllowReverse = configuration.AllowReverse,
};
}
private static SmoothingFixturePathPoint CreatePathPointRecord(CoarsePathPoint point)
{
return new SmoothingFixturePathPoint
{
XMeters = point.X,
YMeters = point.Y,
HeadingRadians = point.Heading,
UnwrappedHeadingRadians = point.UnwrappedHeading,
ArcLengthMeters = point.ArcLength,
Direction = point.Direction,
VehicleCurvaturePerMeter = point.VehicleCurvature,
BodyClearanceMeters = point.BodyClearance,
IsGearSwitchPoint = point.IsGearSwitchPoint,
Source = point.Source,
};
}
private static SmoothingFixtureSegment CreateSegmentRecord(PathSegment segment)
{
return new SmoothingFixtureSegment
{
SegmentIndex = segment.SegmentIndex,
Direction = segment.Direction,
StartIndex = segment.StartIndex,
EndIndex = segment.EndIndex,
StartsAtGearSwitch = segment.StartsAtGearSwitch,
EndsAtGearSwitch = segment.EndsAtGearSwitch,
};
}
private sealed class FixturePlan
{
public FixturePlan(string id, Func<CoarsePathPlanningJob> createJob)
{
Id = id;
CreateJob = createJob;
}
public string Id { get; }
public Func<CoarsePathPlanningJob> CreateJob { get; }
}
}