From 1903e71fc1afba71761a39659e5c7cb982dbc78f Mon Sep 17 00:00:00 2001 From: "jing.zhang" Date: Tue, 11 Aug 2026 20:32:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=20EM=20=E8=BD=A8?= =?UTF-8?q?=E8=BF=B9=E8=A7=84=E5=88=92=E9=A6=96=E4=B8=AA=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 12 +- .superpowers/brainstorm/.last-port | 1 - .superpowers/brainstorm/.last-token | 1 - .../content/dashboard-layout-options.html | 63 - .../content/speed-profile-strategies.html | 56 - .../waiting-full-segment-semantics.html | 6 - .../brainstorm/1212-1786007808/state/events | 2 - .../1212-1786007808/state/server-info | 1 - .../1212-1786007808/state/server-instance-id | 1 - .../1212-1786007808/state/server.pid | 1 - .../1515-1785381825/state/server-info | 1 - .../1515-1785381825/state/server-instance-id | 1 - .../1515-1785381825/state/server.pid | 1 - .../content/six-figure-layout.html | 37 - .../1528-1785381873/content/waiting.html | 3 - .../1528-1785381873/state/server-instance-id | 1 - .../1528-1785381873/state/server-stopped | 1 - .../1528-1785381873/state/server.pid | 1 - .../content/dashboard-layout.html | 73 - .../content/dashboard-scientific.html | 70 - .../content/dashboard-style.html | 74 - .../content/waiting-architecture.html | 6 - .../1612-1785939150/state/server-stopped | 1 - .../content/diagram-strategy.html | 54 - .../content/domain-planning-visual-v2.html | 89 - .../content/interaction-design-v3.html | 72 - .../content/interaction-layout.html | 69 - .../content/waiting-architecture.html | 6 - .../content/waiting-evidence-state.html | 6 - .../content/waiting-granularity.html | 6 - .../content/waiting-validation.html | 6 - .../1923-1785734653/state/server-stopped | 1 - .../content/dashboard-layout-options.html | 10 - .../content/dashboard-paper-thin-v2.html | 23 - .../content/waiting-targeted-repair.html | 6 - .../985-1786008552/state/server-instance-id | 1 - .../985-1786008552/state/server-stopped | 1 - .../985-1786008552/state/server.pid | 1 - .task8-sweep/AGV.cs | 534 - .task8-sweep/ChassisController.cs | 100 - .task8-sweep/ClumsyPilot.csproj | 53 - .task8-sweep/FleetCrabWalk.cs | 526 - .task8-sweep/FleetCurveWalk.cs | 411 - .task8-sweep/MovementTests.TireFollowing.cs | 606 - .task8-sweep/MovementTests.TwoLegDetect.cs | 136 - .task8-sweep/MovementTests.cs | 608 - .task8-sweep/Movements.cs | 326 - .../CoarsePath/Contracts/CoarsePathPoint.cs | 63 - .../Contracts/CoarsePathPointSource.cs | 14 - .../Contracts/GoalDirectionConstraint.cs | 14 - .../Contracts/HybridAStarConfiguration.cs | 83 - .../CoarsePath/Contracts/PathSegment.cs | 37 - .../Contracts/PlanningDiagnostics.cs | 65 - .../CoarsePath/Contracts/PlanningRequest.cs | 41 - .../CoarsePath/Contracts/PlanningResult.cs | 67 - .../CoarsePath/Contracts/PlanningStatus.cs | 56 - .../CoarsePath/Contracts/Pose2D.cs | 28 - .../CoarsePath/Contracts/TravelDirection.cs | 11 - .../CoarsePath/Contracts/VehicleParameters.cs | 28 - .../Facade/CoarsePathPlanningJob.cs | 45 - .../Facade/CoarsePathPlanningJobResult.cs | 45 - .../Facade/CoarsePathPlanningService.cs | 108 - .../CoarsePath/Facade/IPlanningDebugSink.cs | 29 - .../CoarsePath/Facade/PlanningDebugOptions.cs | 23 - .../CoarsePath/HybridAStarPlanner.cs | 264 - .../CoarsePath/Output/CoarsePathAssembler.cs | 216 - .../CoarsePath/Output/CoarsePathValidator.cs | 245 - .../CoarsePath/Output/PathBacktracker.cs | 167 - .../ParkrobTrajplanner/CoarsePath/README.md | 387 - .../CoarsePath/Search/BinaryMinHeap.cs | 146 - .../CoarsePath/Search/GoalToleranceChecker.cs | 61 - .../Search/GridDijkstraHeuristic.cs | 123 - .../CoarsePath/Search/HybridAStarNode.cs | 99 - .../CoarsePath/Search/HybridAStarNodeKey.cs | 67 - .../CoarsePath/Search/HybridAStarSearch.cs | 542 - .../CoarsePath/Search/MotionPrimitive.cs | 69 - .../Search/MotionPrimitiveGenerator.cs | 173 - .../CoarsePath/Search/SearchCostCalculator.cs | 98 - .../Test/CoarsePathScenarioFactory.cs | 507 - .../Test/MovementTest.CoarsePathTest.cs | 628 - .../Vehicle/FootprintCollisionChecker.cs | 160 - .../OrientedRectangleCellIntersection.cs | 33 - .../CoarsePath/Vehicle/VehicleFootprint.cs | 90 - .../CoarsePath/Vehicle/VehicleKinematics.cs | 40 - ...MR_HybridAStar_粗路径规划总体技术方案.html | 812 - .../AMR_HybridAStar_粗路径实施计划.md | 726 - .../AMR_轨迹规划技术路线_Agent版.md | 1194 - .../Map/Core/EnvironmentGridMap.cs | 92 - .../Map/Core/EnvironmentMapBuildResult.cs | 38 - .../Map/Core/EnvironmentMapBuilder.cs | 67 - .../Map/Core/MapBoundsMm.cs | 86 - .../Map/Core/MapBuildRequest.cs | 18 - .../Obstacles/AxisAlignedRectangleObstacle.cs | 31 - .../Map/Obstacles/CircleObstacle.cs | 29 - .../Map/Obstacles/IMapObstacle.cs | 12 - .../Map/Obstacles/MapObstacleRasterizer.cs | 98 - .../Planning/EuclideanDistanceTransform.cs | 123 - .../Map/Planning/ObstacleDistanceField.cs | 70 - .../Map/Planning/PlanningGridMap.cs | 88 - .../Map/Planning/PlanningMapAdapter.cs | 45 - .../Map/Planning/PlanningMapCache.cs | 65 - .../Map/PlanningMapBuildResult.cs | 66 - .../Map/PlanningMapFactory.cs | 184 - .../Map/PlanningMapRequest.cs | 35 - .task8-sweep/ParkrobTrajplanner/Map/README.md | 367 - .../Map/Sources/IMapObstacleSource.cs | 22 - .../Map/Sources/ManualObstacleSource.cs | 39 - .../Map/Sources/ObstacleProjectionResult.cs | 30 - .../Map/Sources/ObstacleSourceStatus.cs | 14 - .../Map/Sources/TwoLegObstacleProjector.cs | 32 - .../Map/Sources/TwoLegObstacleSource.cs | 24 - .../Map/Sources/TwoLegProjectionInput.cs | 51 - .../Map/Test/MovementTest.MapTest.cs | 133 - .../PlanningMapImageExportRequest.cs | 10 - .../PlanningMapImageExportResult.cs | 20 - .../Visualization/PlanningMapImageExporter.cs | 67 - .../Visualization/PlanningMapImageRenderer.cs | 37 - .../Test/Visualization/ValidatedPngWriter.cs | 98 - .../Algorithms/CubicBSplineSmoother.cs | 412 - .../PathSmoothing/Algorithms/IPathSmoother.cs | 14 - .../Algorithms/LocalCubicBezierSmoother.cs | 427 - .../Algorithms/PiecewiseQuinticSmoother.cs | 557 - .../Algorithms/SmoothingAlgorithmInput.cs | 44 - .../Algorithms/SmoothingAlgorithmRunner.cs | 443 - .../Algorithms/SmoothingCandidate.cs | 83 - .../Algorithms/SmoothingOptionsSnapshot.cs | 50 - .../PathSmoothingComparisonEntry.cs | 132 - .../PathSmoothingComparisonRequest.cs | 60 - .../PathSmoothingComparisonResult.cs | 49 - .../Comparison/SmoothingMethodRanker.cs | 60 - .../Comparison/SmoothingTimingSummary.cs | 90 - .../Comparison/StableGeometryDigest.cs | 111 - .../Contracts/CubicBSplineOptions.cs | 8 - .../Contracts/LocalCubicBezierOptions.cs | 16 - .../Contracts/LocalG2QuinticOptions.cs | 15 - .../Contracts/PathQualityMetrics.cs | 108 - .../Contracts/PathSmoothingConfiguration.cs | 53 - .../Contracts/PathSmoothingDiagnostics.cs | 80 - .../PathSmoothingRegionFailureReason.cs | 17 - .../Contracts/PathSmoothingRegionReport.cs | 83 - .../Contracts/PathSmoothingRegionStatus.cs | 8 - .../Contracts/PathSmoothingRequest.cs | 97 - .../Contracts/PathSmoothingResult.cs | 154 - .../Contracts/PathSmoothingStatus.cs | 16 - .../Contracts/PiecewiseQuinticOptions.cs | 11 - .../Contracts/SmoothedPathPoint.cs | 100 - .../Contracts/SmoothedPathPointSource.cs | 11 - .../Contracts/SmoothedPathSegment.cs | 41 - .../Contracts/SmoothingMethod.cs | 10 - .../Facade/PathSmoothingComparisonService.cs | 231 - .../Facade/PathSmoothingService.cs | 305 - .../LocalG2/CurvatureTransition.cs | 55 - .../LocalG2/CurvatureTransitionDetector.cs | 260 - .../LocalG2/LocalG2CandidateBuilder.cs | 1242 - .../LocalG2/LocalG2CandidateEvaluator.cs | 559 - .../LocalG2/LocalG2CandidateGeometry.cs | 76 - .../LocalG2/LocalG2OptionsSnapshot.cs | 57 - .../LocalG2/LocalG2PathSplicer.cs | 134 - .../LocalG2/LocalG2PreSmoothingPipeline.cs | 234 - .../LocalG2/LocalG2RegionWorkOrder.cs | 107 - .../LocalG2/LocalG2SmoothingRegion.cs | 71 - .../LocalG2/LocalG2WindowPlanner.cs | 378 - .../LocalG2/QuinticHermiteCurve2D.cs | 124 - .../Processing/ArcLengthResampler.cs | 145 - .../Processing/PathGeometryAnalysis.cs | 72 - .../Processing/PathGeometryAnalyzer.cs | 490 - .../Processing/PathReferenceInterpolator.cs | 102 - .../Processing/PathSmoothingPreprocessor.cs | 201 - .../Processing/PreparedDirectionSegment.cs | 70 - .../PathSmoothing/Processing/PreparedPath.cs | 49 - .../Processing/RawPathBaselineBuilder.cs | 264 - .../Processing/SmoothingPoint2D.cs | 53 - .../PathSmoothing/README.md | 56 - .../Fixtures/path-smoothing-fixtures.json | 19451 ---------------- .../Test/PathSmoothingComparisonDemo.cs | 161 - .../Test/SmoothingFixtureGenerator.cs | 260 - .../Test/SmoothingScenarioFactory.cs | 69 - .../Test/SmoothingScenarioFixture.cs | 156 - .../Test/SmoothingScenarioFixtureLoader.cs | 258 - .../Validation/SmoothedPathValidator.cs | 245 - .../Visualization/IeeeFigureStyle.cs | 26 - .../Visualization/SmoothingCsvWriter.cs | 43 - .../SmoothingFigureDefinition.cs | 109 - .../Visualization/SmoothingFigureKind.cs | 12 - .../Visualization/SmoothingFigureModel.cs | 185 - .../SmoothingFigureModelBuilder.cs | 170 - .../SmoothingFigureSetBuilder.cs | 171 - .../Visualization/SmoothingFontResolver.cs | 140 - .../Visualization/SmoothingPngRenderer.cs | 287 - .../SmoothingReportExportRequest.cs | 11 - .../SmoothingReportExportResult.cs | 49 - .../Visualization/SmoothingReportExporter.cs | 130 - .../Visualization/SmoothingSvgRenderer.cs | 189 - .../ParkrobTrajplanner/Utils/AngleMath.cs | 34 - .../Utils/CoordinateTransform.cs | 18 - .../ParkrobTrajplanner/Utils/GridIndex.cs | 15 - .../ParkrobTrajplanner/Utils/NumericGuard.cs | 14 - .../Utils/PlanningOperationBudget.cs | 81 - .../ParkrobTrajplanner/Utils/UnitConverter.cs | 11 - .task8-sweep/PilotConfig.cs | 334 - .task8-sweep/PilotDefinition.cs | 2128 -- .task8-sweep/PlaygroundWebApi.cs | 81 - .task8-sweep/TireFollowing.cs | 511 - .task8-sweep/VehicleSyncBinaryCodec.cs | 277 - .task8-sweep/VehicleSyncModels.cs | 75 - .task8-sweep/ref/CommonUsage.dll | Bin 134144 -> 0 bytes .task8-sweep/ref/LessokajiWeaverUtilities.dll | Bin 10240 -> 0 bytes .task8-sweep/ref/MDCSToolBox.dll | Bin 3580416 -> 0 bytes .task8-sweep/ref/RefClumsyCore.dll | Bin 95744 -> 0 bytes .task8-sweep/ref/RefClumsyDance.dll | Bin 55296 -> 0 bytes .task8-sweep/ref/RefFundamentalLib.dll | Bin 36352 -> 0 bytes .../PathSmoothingPngVerificationHost.csproj | 13 - .../Program.cs | 360 - .../generate_path_smoothing_fixtures.ps1 | 15 - .../tests/run_path_smoothing_comparison.ps1 | 32 - .../tests/verify_coarse_path_collision.ps1 | 130 - .../tests/verify_coarse_path_integration.ps1 | 460 - .../tests/verify_coarse_path_search.ps1 | 411 - .task8-sweep/tests/verify_coarse_path_ui.ps1 | 122 - .../verify_path_smoothing_algorithm_input.ps1 | 121 - .../tests/verify_path_smoothing_bezier.ps1 | 319 - .../tests/verify_path_smoothing_bspline.ps1 | 286 - .../verify_path_smoothing_comparison.ps1 | 230 - .../tests/verify_path_smoothing_contracts.ps1 | 381 - .../verify_path_smoothing_documentation.ps1 | 65 - .../tests/verify_path_smoothing_fixtures.ps1 | 119 - .../tests/verify_path_smoothing_geometry.ps1 | 642 - .../verify_path_smoothing_integration.ps1 | 140 - ...ify_path_smoothing_local_g2_candidates.ps1 | 252 - .../verify_path_smoothing_local_g2_curve.ps1 | 118 - ...rify_path_smoothing_local_g2_detection.ps1 | 129 - ...fy_path_smoothing_local_g2_integration.ps1 | 136 - .../tests/verify_path_smoothing_png.ps1 | 18 - .../tests/verify_path_smoothing_quintic.ps1 | 338 - .../tests/verify_path_smoothing_runner.ps1 | 81 - .../tests/verify_path_smoothing_service.ps1 | 259 - .../tests/verify_path_smoothing_svg_csv.ps1 | 188 - .../verify_path_smoothing_validation.ps1 | 175 - .../tests/verify_planning_map_adapter.ps1 | 47 - .../verify_planning_map_documentation.ps1 | 58 - .../tests/verify_planning_map_factory.ps1 | 88 - .../tests/verify_planning_map_image.ps1 | 31 - .../tests/verify_planning_map_test_config.ps1 | 23 - ClumsyPilot/ClumsyPilot.csproj | 6 + ...MR_HybridAStar_粗路径规划总体技术方案.html | 812 - .../Configuration/CorridorConfiguration.cs | 21 + .../Configuration/EmPlannerConfiguration.cs | 34 +- .../Configuration/FrenetConfiguration.cs | 15 + .../Configuration/LateralConfiguration.cs | 21 + .../EMPlanner/Configuration/LateralWeights.cs | 30 + .../LongitudinalConfiguration.cs | 42 + .../Configuration/LongitudinalWeights.cs | 21 + .../Configuration/SchedulingConfiguration.cs | 39 + .../Configuration/SolverConfiguration.cs | 30 + .../Configuration/ValidationConfiguration.cs | 18 + .../EMPlanner/Contracts/ContractNumeric.cs | 6 + .../EMPlanner/Contracts/EmBoundaryType.cs | 18 + .../EMPlanner/Contracts/EmLongitudinalMode.cs | 12 + .../EMPlanner/Contracts/EmMotionModel.cs | 12 + .../EMPlanner/Contracts/EmPlanningRequest.cs | 272 +- .../EMPlanner/Contracts/EmPlanningResult.cs | 15 + .../EMPlanner/Contracts/EmPlanningScope.cs | 9 + .../EMPlanner/Contracts/EmPlanningStatus.cs | 70 + .../EMPlanner/Contracts/EmTerminalType.cs | 12 + .../EMPlanner/Contracts/EmTrajectory.cs | 12 + .../Contracts/EmTrajectoryMetadata.cs | 53 + .../EMPlanner/Contracts/EmTrajectoryPoint.cs | 71 + .../EMPlanner/Contracts/VehicleMotionState.cs | 21 + .../EMPlanner/Corridor/LateralInterval.cs | 29 +- .../EMPlanner/Corridor/StaticCorridor.cs | 12 +- .../Corridor/StaticCorridorBuilder.cs | 86 +- .../EMPlanner/Facade/EmPlanningService.cs | 173 +- .../EMPlanner/Frenet/FrenetProjection.cs | 33 +- .../EMPlanner/Frenet/FrenetProjector.cs | 59 +- .../EMPlanner/Frenet/FrenetReferencePoint.cs | 58 +- .../EMPlanner/Frenet/FrenetTransform.cs | 18 +- .../Frenet/ReferencePathInterpolator.cs | 25 +- .../LongitudinalConstraintBuilder.cs | 103 +- .../LongitudinalContinuousProfileValidator.cs | 136 + .../LongitudinalEnvelopeTrustRegion.cs | 274 + .../LongitudinalQpFeasibilityAudit.cs | 217 + .../LongitudinalSolutionValidator.cs | 46 +- .../Longitudinal/LongitudinalSolveTrace.cs | 47 + .../SequentialLongitudinalOptimizer.cs | 704 +- .../EMPlanner/Optimization/IQpSolver.cs | 3 + .../EMPlanner/Optimization/QpSolveResult.cs | 3 + .../Optimization/QuadraticProgram.cs | 3 + .../Segmentation/DirectionSegmentView.cs | 29 + .../Segmentation/PlanningHorizonSelector.cs | 58 +- .../Segmentation/ReferenceBoundary.cs | 32 + .../Segmentation/ReferenceHorizonSlicer.cs | 40 + .../Segmentation/ReferencePathSegmenter.cs | 12 + .../AMR_HybridAStar_粗路径实施计划.md | 726 - .../AMR_轨迹规划技术路线_Agent版.md | 1194 - .../LocalG2/LocalG2WindowPlanner.cs | 5 + .../EmPlanningCoordinator.cs | 66 +- .../trajectory-planning-flow-demo.html | 0 .../ControlModuleTrajectoryAdapter.cs | 68 - .../Trajplanner_output/Program.cs | 45 - .../Trajplanner_output/README.md | 164 - .../Trajplanner_output/Tests/Program.cs | 45 - .../Tests/TrajectoryOutputDemo.Tests.csproj | 11 - .../TrajectoryOutputDemo.csproj | 15 - .../TrajectoryOutputDemoConfiguration.cs | 63 - .../TrajectoryOutputDemoRunner.cs | 99 - .../TrajectorySequenceExporter.cs | 46 - .../EmControlTrajectoryAdapter.cs | 60 + .../MovementTest.EmClosedLoopTest.cs | 697 + .../tarjplanner_movementtest/README.md | 294 +- .../TrajectoryObservationDiagnostics.cs | 2 + ...ectoryObservationDynamicSnapshotBuilder.cs | 1 + .../TrajectoryObservationHandoffAnalyzer.cs | 2 + ...jectoryObservationKinematicChartBuilder.cs | 1 + .../TrajectoryObservationPipeline.cs | 159 +- .../TrajectoryObservationPlanningDeadline.cs | 226 + .../TrajectoryObservationPresentation.cs | 2 + .../TrajectoryObservationSegmentTracker.cs | 2 + ...jectoryObservationStaticSnapshotBuilder.cs | 1 + .../CoordinatorChecks.cs | 512 - .../CorridorChecks.cs | 182 - .../EMPlannerVerificationHost.csproj | 12 - .../EmFixtureFactory.cs | 44 - .../EmPlanningServiceChecks.cs | 1092 - .../ExecutorChecks.cs | 296 - .../EMPlannerVerificationHost/FakeQpSolver.cs | 88 - .../FoundationChecks.cs | 345 - .../EMPlannerVerificationHost/FrenetChecks.cs | 136 - .../LateralIntegrationChecks.cs | 472 - .../LateralModelChecks.cs | 646 - .../LongitudinalIntegrationChecks.cs | 829 - .../LongitudinalModelChecks.cs | 988 - .../OptimizationChecks.cs | 142 - .../EMPlannerVerificationHost/OsqpChecks.cs | 305 - .../PluginPackagingChecks.cs | 152 - .../EMPlannerVerificationHost/Program.cs | 147 - .../SegmentationChecks.cs | 46 - .../TrajectoryChecks.cs | 551 - .../TrajectoryObservationChecks.cs | 1444 -- .../TrajectoryObservationSegmentChecks.cs | 228 - .../TrajectoryObservationSettingsChecks.cs | 150 - ...rajectoryObservationVisualizationChecks.cs | 648 - .../EMPlannerVerificationHost/Verification.cs | 24 - .../PathSmoothingPngVerificationHost.csproj | 13 - .../Program.cs | 590 - .../ContractChecks.cs | 139 - .../Program.cs | 65 - .../RuntimeChecks.cs | 101 - .../SampleSnapshotFactory.cs | 120 - .../ServerChecks.cs | 262 - ...anningVisualizationVerificationHost.csproj | 10 - .../Verification.cs | 45 - .../WebAssetChecks.cs | 150 - .../dashboard.dom.test.mjs | 468 - .../package-lock.json | 558 - .../package.json | 14 - .../generate_path_smoothing_fixtures.ps1 | 15 - .../run_local_g2_diagnostic_visualization.ps1 | 28 - .../tests/run_path_smoothing_comparison.ps1 | 32 - .../tests/verify_coarse_path_collision.ps1 | 130 - .../tests/verify_coarse_path_integration.ps1 | 460 - .../tests/verify_coarse_path_search.ps1 | 411 - ClumsyPilot/tests/verify_coarse_path_ui.ps1 | 122 - .../verify_path_smoothing_documentation.ps1 | 68 - .../tests/verify_path_smoothing_fixtures.ps1 | 119 - .../tests/verify_path_smoothing_geometry.ps1 | 642 - ...ify_path_smoothing_local_g2_candidates.ps1 | 246 - .../verify_path_smoothing_local_g2_curve.ps1 | 118 - ...rify_path_smoothing_local_g2_detection.ps1 | 137 - ...smoothing_local_g2_diagnostic_contract.ps1 | 20 - ...smoothing_local_g2_diagnostic_evidence.ps1 | 63 - ...hing_local_g2_diagnostic_visualization.ps1 | 11 - ...fy_path_smoothing_local_g2_integration.ps1 | 98 - ...fy_path_smoothing_local_g2_only_layout.ps1 | 75 - .../tests/verify_path_smoothing_png.ps1 | 30 - .../verify_path_smoothing_validation.ps1 | 175 - .../tests/verify_planning_map_adapter.ps1 | 47 - .../verify_planning_map_documentation.ps1 | 58 - .../tests/verify_planning_map_factory.ps1 | 88 - .../tests/verify_planning_map_image.ps1 | 31 - .../tests/verify_planning_map_test_config.ps1 | 23 - .../verify_trajectory_planning_flow_demo.ps1 | 56 - .../Chassis/MultiWheelChassisAdapter.cs | 0 Shared/Fleet/FleetKinematics.cs | 1 + .../Mathematics/AngleMath.cs | 0 .../Mathematics/FrameTransform2D.cs | 0 .../Mathematics/InterpolationMath.cs | 0 .../Models/ChassisCommand.cs | 2 +- ...026-08-05-em-longitudinal-rolling-final.md | 95 - ...6-08-05-em-longitudinal-rolling-phase-1.md | 72 - ...6-08-05-em-longitudinal-rolling-phase-2.md | 60 - ...6-08-05-em-longitudinal-rolling-phase-3.md | 110 - .../phase-01.md | 84 - .../phase-02.md | 34 - .../phase-03.md | 153 - .../phase-04.md | 102 - .../phase-05.md | 142 - .../phase-06.md | 144 - .../phase-07.md | 105 - .../phase-08.md | 82 - .../phase-09-vehicle.md | 51 - .../phase-09.md | 83 - .../handoffs/em-observation-web/phase-01.md | 58 - .../handoffs/em-observation-web/phase-02.md | 42 - .../handoffs/em-observation-web/phase-03.md | 51 - .../handoffs/em-observation-web/phase-04.md | 44 - .../handoffs/em-observation-web/phase-05.md | 43 - .../handoffs/em-observation-web/phase-06.md | 46 - .../handoffs/em-observation-web/phase-07.md | 52 - .../handoffs/em-observation-web/phase-08.md | 41 - .../2026-07-22-trap-map-image-and-console.md | 266 - ...2026-07-22-workstation-bounded-trap-map.md | 279 - .../plans/2026-07-23-trap-map-pure-png.md | 125 - ...26-07-27-hybrid-astar-p0-implementation.md | 427 - .../plans/2026-07-27-map-documentation.md | 242 - ...026-07-27-p1-coarse-path-ui-integration.md | 534 - .../2026-07-27-planning-operation-budget.md | 369 - ...26-07-28-coarse-path-live-amr-scenarios.md | 436 - .../2026-07-28-coarse-path-search-elapsed.md | 256 - ...2026-07-28-coarse-path-test-diagnostics.md | 837 - ...026-07-28-coarsepath-readme-restructure.md | 185 - .../2026-07-28-p1-manual-obstacle-input.md | 365 - .../2026-07-28-path-smoothing-comparison.md | 1177 - .../2026-07-30-local-g2-path-presmoothing.md | 1574 -- ...-07-30-path-smoothing-six-figure-report.md | 73 - .../2026-07-31-local-g2-dailywork-reports.md | 249 - ...l-g2-interactive-visualization-redesign.md | 404 - ...-07-31-local-g2-pre-task8-stabilization.md | 1669 -- ...local-g2-soft-anchor-candidate-recovery.md | 971 - ...minimum-curvature-excursion-feasibility.md | 1584 -- ...ocal-g2-split-derivative-scale-recovery.md | 1618 -- ...08-02-local-g2-diagnostic-visualization.md | 558 - ...ummary-job-domain-visualization-upgrade.md | 915 - .../2026-08-03-daily-summary-job-skill.md | 432 - ...03-em-planner-foundation-implementation.md | 557 - ...03-em-planner-lateral-ls-implementation.md | 334 - ...-planner-longitudinal-st-implementation.md | 394 - ...-em-planner-osqp-backend-implementation.md | 382 - ...lanner-rolling-execution-implementation.md | 375 - ...anner-windowed-execution-implementation.md | 247 - ...l-g2-only-path-smoothing-reorganization.md | 106 - .../2026-08-04-em-observation-movementtest.md | 457 - ...-08-04-em-planner-readme-implementation.md | 262 - ...planner-comment-overhaul-implementation.md | 295 - ...longitudinal-rolling-four-phase-roadmap.md | 213 - ...-08-05-em-longitudinal-rolling-planning.md | 1435 -- .../2026-08-05-em-observation-diagnostics.md | 334 - ...-05-em-observation-planning-diagnostics.md | 296 - ...26-08-05-em-observation-web-integration.md | 694 - ...08-05-movementtest-osqp-iteration-limit.md | 110 - .../2026-08-05-movementtest-solver-timeout.md | 102 - ...026-08-05-movementtest-st-time-controls.md | 115 - ...ajectory-planning-visualization-library.md | 660 - ...full-direction-segment-staged-execution.md | 588 - ...-direction-segment-visualization-repair.md | 1107 - ...-06-em-observation-web-staged-execution.md | 632 - ...-09-em-full-direction-correctness-fixes.md | 227 - ...-09-movementtest-web-path-visualization.md | 694 - .../2026-08-09-trajplanner-output-demo.md | 136 - ...2026-08-10-em-closed-loop-movement-test.md | 508 - ...08-10-em-paths-monotonic-resampling-fix.md | 166 - ...026-08-10-trajectory-planning-flow-demo.md | 179 - ...-11-em-planner-controller-handoff-guide.md | 499 - .../progress/em-planner-progress.md | 266 - ...6-08-05-em-longitudinal-rolling-phase-1.md | 235 - ...6-08-05-em-longitudinal-rolling-phase-2.md | 72 - ...6-08-05-em-longitudinal-rolling-phase-3.md | 86 - ...6-08-05-em-longitudinal-rolling-phase-4.md | 153 - ...-curvature-excursion-feasibility-report.md | 5664 ----- ...07-22-trap-map-image-and-console-design.md | 111 - ...22-trap-map-layered-construction-design.md | 110 - .../2026-07-23-trap-map-pure-png-design.md | 52 - ...6-07-26-hybrid-astar-coarse-path-design.md | 544 - ...7-hybrid-astar-p0-implementation-design.md | 94 - .../2026-07-27-map-documentation-design.md | 56 - ...27-p1-coarse-path-ui-integration-design.md | 114 - ...-07-27-planning-operation-budget-design.md | 59 - ...8-coarse-path-live-amr-scenarios-design.md | 83 - ...07-28-coarse-path-search-elapsed-design.md | 66 - ...-28-coarse-path-test-diagnostics-design.md | 160 - ...28-coarsepath-readme-restructure-design.md | 46 - ...6-07-28-p1-manual-obstacle-input-design.md | 71 - ...-07-28-path-smoothing-comparison-design.md | 704 - ...07-30-local-g2-path-presmoothing-design.md | 573 - ...path-smoothing-six-figure-report-design.md | 38 - ...07-31-local-g2-dailywork-reports-design.md | 62 - ...l-g2-interactive-visualization-redesign.md | 98 - ...local-g2-pre-task8-stabilization-design.md | 255 - ...2-soft-anchor-candidate-recovery-design.md | 212 - ...-curvature-excursion-feasibility-design.md | 579 - ...-split-derivative-scale-recovery-design.md | 283 - ...ocal-g2-diagnostic-visualization-design.md | 104 - ...job-domain-visualization-upgrade-design.md | 295 - ...26-08-03-daily-summary-job-skill-design.md | 188 - .../2026-08-03-em-planner-ls-st-design.md | 1058 - ...03-em-planner-windowed-execution-design.md | 366 - ...8-04-em-observation-movementtest-design.md | 122 - .../2026-08-04-em-planner-readme-design.md | 53 - ...ing-trajectory-execution-comment-design.md | 55 - ...em-longitudinal-rolling-planning-design.md | 370 - ...08-05-em-observation-diagnostics-design.md | 58 - ...observation-planning-diagnostics-design.md | 130 - ...em-observation-web-visualization-design.md | 294 - ...ementtest-longitudinal-time-step-design.md | 25 - ...ovementtest-osqp-iteration-limit-design.md | 25 - ...8-05-movementtest-solver-timeout-design.md | 22 - ...rection-segment-staged-execution-design.md | 181 - ...ion-segment-visualization-repair-design.md | 282 - ...observation-web-staged-execution-design.md | 172 - ...b-adaptive-inquiry-visualization-design.md | 360 - ...ementtest-web-path-visualization-design.md | 209 - ...planner-readme-comment-alignment-design.md | 63 - ...26-08-09-trajplanner-output-demo-design.md | 67 - ...-10-em-closed-loop-movement-test-design.md | 172 - ...op-offline-scenario-verification-design.md | 111 - ...nal-curved-envelope-trust-region-design.md | 244 - ...m-paths-monotonic-resampling-fix-design.md | 32 - ...10-trajectory-planning-flow-demo-design.md | 93 - ...-5p5m-speed-diagnosis-and-repair-design.md | 302 - ...eadline-and-publication-contract-design.md | 117 - ...tudinal-envelope-contract-repair-design.md | 201 - ...planner-controller-handoff-guide-design.md | 188 - emplanner-daily-report.json | 99 - 522 files changed, 4188 insertions(+), 119188 deletions(-) delete mode 100644 .superpowers/brainstorm/.last-port delete mode 100644 .superpowers/brainstorm/.last-token delete mode 100644 .superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html delete mode 100644 .superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html delete mode 100644 .superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html delete mode 100644 .superpowers/brainstorm/1212-1786007808/state/events delete mode 100644 .superpowers/brainstorm/1212-1786007808/state/server-info delete mode 100644 .superpowers/brainstorm/1212-1786007808/state/server-instance-id delete mode 100644 .superpowers/brainstorm/1212-1786007808/state/server.pid delete mode 100644 .superpowers/brainstorm/1515-1785381825/state/server-info delete mode 100644 .superpowers/brainstorm/1515-1785381825/state/server-instance-id delete mode 100644 .superpowers/brainstorm/1515-1785381825/state/server.pid delete mode 100644 .superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html delete mode 100644 .superpowers/brainstorm/1528-1785381873/content/waiting.html delete mode 100644 .superpowers/brainstorm/1528-1785381873/state/server-instance-id delete mode 100644 .superpowers/brainstorm/1528-1785381873/state/server-stopped delete mode 100644 .superpowers/brainstorm/1528-1785381873/state/server.pid delete mode 100644 .superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html delete mode 100644 .superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html delete mode 100644 .superpowers/brainstorm/1612-1785939150/content/dashboard-style.html delete mode 100644 .superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html delete mode 100644 .superpowers/brainstorm/1612-1785939150/state/server-stopped delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/interaction-layout.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-validation.html delete mode 100644 .superpowers/brainstorm/1923-1785734653/state/server-stopped delete mode 100644 .superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html delete mode 100644 .superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html delete mode 100644 .superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html delete mode 100644 .superpowers/brainstorm/985-1786008552/state/server-instance-id delete mode 100644 .superpowers/brainstorm/985-1786008552/state/server-stopped delete mode 100644 .superpowers/brainstorm/985-1786008552/state/server.pid delete mode 100644 .task8-sweep/AGV.cs delete mode 100644 .task8-sweep/ChassisController.cs delete mode 100644 .task8-sweep/ClumsyPilot.csproj delete mode 100644 .task8-sweep/FleetCrabWalk.cs delete mode 100644 .task8-sweep/FleetCurveWalk.cs delete mode 100644 .task8-sweep/MovementTests.TireFollowing.cs delete mode 100644 .task8-sweep/MovementTests.TwoLegDetect.cs delete mode 100644 .task8-sweep/MovementTests.cs delete mode 100644 .task8-sweep/Movements.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/README.md delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html delete mode 100644 .task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md delete mode 100644 .task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/README.md delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs delete mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs delete mode 100644 .task8-sweep/PilotConfig.cs delete mode 100644 .task8-sweep/PilotDefinition.cs delete mode 100644 .task8-sweep/PlaygroundWebApi.cs delete mode 100644 .task8-sweep/TireFollowing.cs delete mode 100644 .task8-sweep/VehicleSyncBinaryCodec.cs delete mode 100644 .task8-sweep/VehicleSyncModels.cs delete mode 100644 .task8-sweep/ref/CommonUsage.dll delete mode 100644 .task8-sweep/ref/LessokajiWeaverUtilities.dll delete mode 100644 .task8-sweep/ref/MDCSToolBox.dll delete mode 100644 .task8-sweep/ref/RefClumsyCore.dll delete mode 100644 .task8-sweep/ref/RefClumsyDance.dll delete mode 100644 .task8-sweep/ref/RefFundamentalLib.dll delete mode 100644 .task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj delete mode 100644 .task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs delete mode 100644 .task8-sweep/tests/generate_path_smoothing_fixtures.ps1 delete mode 100644 .task8-sweep/tests/run_path_smoothing_comparison.ps1 delete mode 100644 .task8-sweep/tests/verify_coarse_path_collision.ps1 delete mode 100644 .task8-sweep/tests/verify_coarse_path_integration.ps1 delete mode 100644 .task8-sweep/tests/verify_coarse_path_search.ps1 delete mode 100644 .task8-sweep/tests/verify_coarse_path_ui.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_algorithm_input.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_bezier.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_bspline.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_comparison.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_contracts.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_documentation.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_fixtures.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_geometry.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_integration.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_png.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_quintic.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_runner.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_service.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 delete mode 100644 .task8-sweep/tests/verify_path_smoothing_validation.ps1 delete mode 100644 .task8-sweep/tests/verify_planning_map_adapter.ps1 delete mode 100644 .task8-sweep/tests/verify_planning_map_documentation.ps1 delete mode 100644 .task8-sweep/tests/verify_planning_map_factory.ps1 delete mode 100644 .task8-sweep/tests/verify_planning_map_image.ps1 delete mode 100644 .task8-sweep/tests/verify_planning_map_test_config.ps1 delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html create mode 100644 ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalContinuousProfileValidator.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalEnvelopeTrustRegion.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalQpFeasibilityAudit.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolveTrace.cs delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md rename ClumsyPilot/ParkrobTrajplanner/{Trajplanner_output => Trajplanner_guide}/trajectory-planning-flow-demo.html (100%) delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/ControlModuleTrajectoryAdapter.cs delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Program.cs delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/Program.cs delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/TrajectoryOutputDemo.Tests.csproj delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoConfiguration.cs delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoRunner.cs delete mode 100644 ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectorySequenceExporter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPlanningDeadline.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/CorridorChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/FrenetChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/OptimizationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/SegmentationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs delete mode 100644 ClumsyPilot/tests/EMPlannerVerificationHost/Verification.cs delete mode 100644 ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj delete mode 100644 ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json delete mode 100644 ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json delete mode 100644 ClumsyPilot/tests/generate_path_smoothing_fixtures.ps1 delete mode 100644 ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 delete mode 100644 ClumsyPilot/tests/run_path_smoothing_comparison.ps1 delete mode 100644 ClumsyPilot/tests/verify_coarse_path_collision.ps1 delete mode 100644 ClumsyPilot/tests/verify_coarse_path_integration.ps1 delete mode 100644 ClumsyPilot/tests/verify_coarse_path_search.ps1 delete mode 100644 ClumsyPilot/tests/verify_coarse_path_ui.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_png.ps1 delete mode 100644 ClumsyPilot/tests/verify_path_smoothing_validation.ps1 delete mode 100644 ClumsyPilot/tests/verify_planning_map_adapter.ps1 delete mode 100644 ClumsyPilot/tests/verify_planning_map_documentation.ps1 delete mode 100644 ClumsyPilot/tests/verify_planning_map_factory.ps1 delete mode 100644 ClumsyPilot/tests/verify_planning_map_image.ps1 delete mode 100644 ClumsyPilot/tests/verify_planning_map_test_config.ps1 delete mode 100644 ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1 rename {ClumsyPilot/Shared => Shared}/Chassis/MultiWheelChassisAdapter.cs (100%) create mode 100644 Shared/Fleet/FleetKinematics.cs rename {ClumsyPilot/Shared => Shared}/Mathematics/AngleMath.cs (100%) rename {ClumsyPilot/Shared => Shared}/Mathematics/FrameTransform2D.cs (100%) rename {ClumsyPilot/Shared => Shared}/Mathematics/InterpolationMath.cs (100%) rename {ClumsyPilot/Shared => Shared}/Models/ChassisCommand.cs (99%) delete mode 100644 docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-final.md delete mode 100644 docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md delete mode 100644 docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-2.md delete mode 100644 docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-3.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-01.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-02.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-03.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-04.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-05.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-07.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-08.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md delete mode 100644 docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-01.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-02.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-03.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-04.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-05.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-06.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-07.md delete mode 100644 docs/superpowers/handoffs/em-observation-web/phase-08.md delete mode 100644 docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md delete mode 100644 docs/superpowers/plans/2026-07-22-workstation-bounded-trap-map.md delete mode 100644 docs/superpowers/plans/2026-07-23-trap-map-pure-png.md delete mode 100644 docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md delete mode 100644 docs/superpowers/plans/2026-07-27-map-documentation.md delete mode 100644 docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md delete mode 100644 docs/superpowers/plans/2026-07-27-planning-operation-budget.md delete mode 100644 docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md delete mode 100644 docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md delete mode 100644 docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md delete mode 100644 docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md delete mode 100644 docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md delete mode 100644 docs/superpowers/plans/2026-07-28-path-smoothing-comparison.md delete mode 100644 docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md delete mode 100644 docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md delete mode 100644 docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md delete mode 100644 docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md delete mode 100644 docs/superpowers/plans/2026-07-31-local-g2-pre-task8-stabilization.md delete mode 100644 docs/superpowers/plans/2026-07-31-local-g2-soft-anchor-candidate-recovery.md delete mode 100644 docs/superpowers/plans/2026-08-01-local-g2-minimum-curvature-excursion-feasibility.md delete mode 100644 docs/superpowers/plans/2026-08-01-local-g2-split-derivative-scale-recovery.md delete mode 100644 docs/superpowers/plans/2026-08-02-local-g2-diagnostic-visualization.md delete mode 100644 docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md delete mode 100644 docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md delete mode 100644 docs/superpowers/plans/2026-08-03-em-planner-foundation-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-03-em-planner-lateral-ls-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-03-em-planner-osqp-backend-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-03-em-planner-windowed-execution-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md delete mode 100644 docs/superpowers/plans/2026-08-04-em-observation-movementtest.md delete mode 100644 docs/superpowers/plans/2026-08-04-em-planner-readme-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-04-planner-comment-overhaul-implementation.md delete mode 100644 docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md delete mode 100644 docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md delete mode 100644 docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md delete mode 100644 docs/superpowers/plans/2026-08-05-em-observation-planning-diagnostics.md delete mode 100644 docs/superpowers/plans/2026-08-05-em-observation-web-integration.md delete mode 100644 docs/superpowers/plans/2026-08-05-movementtest-osqp-iteration-limit.md delete mode 100644 docs/superpowers/plans/2026-08-05-movementtest-solver-timeout.md delete mode 100644 docs/superpowers/plans/2026-08-05-movementtest-st-time-controls.md delete mode 100644 docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md delete mode 100644 docs/superpowers/plans/2026-08-06-em-full-direction-segment-staged-execution.md delete mode 100644 docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md delete mode 100644 docs/superpowers/plans/2026-08-06-em-observation-web-staged-execution.md delete mode 100644 docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md delete mode 100644 docs/superpowers/plans/2026-08-09-movementtest-web-path-visualization.md delete mode 100644 docs/superpowers/plans/2026-08-09-trajplanner-output-demo.md delete mode 100644 docs/superpowers/plans/2026-08-10-em-closed-loop-movement-test.md delete mode 100644 docs/superpowers/plans/2026-08-10-em-paths-monotonic-resampling-fix.md delete mode 100644 docs/superpowers/plans/2026-08-10-trajectory-planning-flow-demo.md delete mode 100644 docs/superpowers/plans/2026-08-11-em-planner-controller-handoff-guide.md delete mode 100644 docs/superpowers/progress/em-planner-progress.md delete mode 100644 docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-1.md delete mode 100644 docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-2.md delete mode 100644 docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-3.md delete mode 100644 docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-4.md delete mode 100644 docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md delete mode 100644 docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md delete mode 100644 docs/superpowers/specs/2026-07-22-trap-map-layered-construction-design.md delete mode 100644 docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md delete mode 100644 docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md delete mode 100644 docs/superpowers/specs/2026-07-27-hybrid-astar-p0-implementation-design.md delete mode 100644 docs/superpowers/specs/2026-07-27-map-documentation-design.md delete mode 100644 docs/superpowers/specs/2026-07-27-p1-coarse-path-ui-integration-design.md delete mode 100644 docs/superpowers/specs/2026-07-27-planning-operation-budget-design.md delete mode 100644 docs/superpowers/specs/2026-07-28-coarse-path-live-amr-scenarios-design.md delete mode 100644 docs/superpowers/specs/2026-07-28-coarse-path-search-elapsed-design.md delete mode 100644 docs/superpowers/specs/2026-07-28-coarse-path-test-diagnostics-design.md delete mode 100644 docs/superpowers/specs/2026-07-28-coarsepath-readme-restructure-design.md delete mode 100644 docs/superpowers/specs/2026-07-28-p1-manual-obstacle-input-design.md delete mode 100644 docs/superpowers/specs/2026-07-28-path-smoothing-comparison-design.md delete mode 100644 docs/superpowers/specs/2026-07-30-local-g2-path-presmoothing-design.md delete mode 100644 docs/superpowers/specs/2026-07-30-path-smoothing-six-figure-report-design.md delete mode 100644 docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md delete mode 100644 docs/superpowers/specs/2026-07-31-local-g2-interactive-visualization-redesign.md delete mode 100644 docs/superpowers/specs/2026-07-31-local-g2-pre-task8-stabilization-design.md delete mode 100644 docs/superpowers/specs/2026-07-31-local-g2-soft-anchor-candidate-recovery-design.md delete mode 100644 docs/superpowers/specs/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-design.md delete mode 100644 docs/superpowers/specs/2026-08-01-local-g2-split-derivative-scale-recovery-design.md delete mode 100644 docs/superpowers/specs/2026-08-02-local-g2-diagnostic-visualization-design.md delete mode 100644 docs/superpowers/specs/2026-08-03-daily-summary-job-domain-visualization-upgrade-design.md delete mode 100644 docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md delete mode 100644 docs/superpowers/specs/2026-08-03-em-planner-ls-st-design.md delete mode 100644 docs/superpowers/specs/2026-08-03-em-planner-windowed-execution-design.md delete mode 100644 docs/superpowers/specs/2026-08-04-em-observation-movementtest-design.md delete mode 100644 docs/superpowers/specs/2026-08-04-em-planner-readme-design.md delete mode 100644 docs/superpowers/specs/2026-08-04-path-smoothing-trajectory-execution-comment-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-em-observation-diagnostics-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-em-observation-planning-diagnostics-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-movementtest-longitudinal-time-step-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-movementtest-osqp-iteration-limit-design.md delete mode 100644 docs/superpowers/specs/2026-08-05-movementtest-solver-timeout-design.md delete mode 100644 docs/superpowers/specs/2026-08-06-em-full-direction-segment-staged-execution-design.md delete mode 100644 docs/superpowers/specs/2026-08-06-em-full-direction-segment-visualization-repair-design.md delete mode 100644 docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md delete mode 100644 docs/superpowers/specs/2026-08-09-daily-summary-job-adaptive-inquiry-visualization-design.md delete mode 100644 docs/superpowers/specs/2026-08-09-movementtest-web-path-visualization-design.md delete mode 100644 docs/superpowers/specs/2026-08-09-planner-readme-comment-alignment-design.md delete mode 100644 docs/superpowers/specs/2026-08-09-trajplanner-output-demo-design.md delete mode 100644 docs/superpowers/specs/2026-08-10-em-closed-loop-movement-test-design.md delete mode 100644 docs/superpowers/specs/2026-08-10-em-closed-loop-offline-scenario-verification-design.md delete mode 100644 docs/superpowers/specs/2026-08-10-em-longitudinal-curved-envelope-trust-region-design.md delete mode 100644 docs/superpowers/specs/2026-08-10-em-paths-monotonic-resampling-fix-design.md delete mode 100644 docs/superpowers/specs/2026-08-10-trajectory-planning-flow-demo-design.md delete mode 100644 docs/superpowers/specs/2026-08-11-em-5p5m-speed-diagnosis-and-repair-design.md delete mode 100644 docs/superpowers/specs/2026-08-11-em-cycle-deadline-and-publication-contract-design.md delete mode 100644 docs/superpowers/specs/2026-08-11-em-longitudinal-envelope-contract-repair-design.md delete mode 100644 docs/superpowers/specs/2026-08-11-em-planner-controller-handoff-guide-design.md delete mode 100644 emplanner-daily-report.json diff --git a/.gitignore b/.gitignore index a3d7c53..e2ba3c6 100644 --- a/.gitignore +++ b/.gitignore @@ -55,5 +55,13 @@ appsettings.Development.json Thumbs.db .DS_Store -#Unfinished Traplanner -TrajPlanner \ No newline at end of file + +# Local / unfinished workspace artifacts +/TrajPlanner/ +/.agents/ +/.sdd-worktrees/ +/.superpowers/ +/.task8-sweep/ +/dailywork_report/ +/docs/ +/ClumsyPilot/tests/ \ No newline at end of file diff --git a/.superpowers/brainstorm/.last-port b/.superpowers/brainstorm/.last-port deleted file mode 100644 index d4ad060..0000000 --- a/.superpowers/brainstorm/.last-port +++ /dev/null @@ -1 +0,0 @@ -52380 \ No newline at end of file diff --git a/.superpowers/brainstorm/.last-token b/.superpowers/brainstorm/.last-token deleted file mode 100644 index 2b306ca..0000000 --- a/.superpowers/brainstorm/.last-token +++ /dev/null @@ -1 +0,0 @@ -c619697a72212b481e509ebd1c9023d987d76f98e8d335c1a428bd2f2e641248 \ No newline at end of file diff --git a/.superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html b/.superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html deleted file mode 100644 index 069c0be..0000000 --- a/.superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html +++ /dev/null @@ -1,63 +0,0 @@ - - -

网页主界面采用哪种观察布局?

-

所有方案都修复页签串页、增加 X/Y 数值刻度与单位、图例、图层显隐、框选缩放、滚轮缩放、复位和全屏查看。

- -
-
-

A · 地图主视图

路径图最大,状态在右侧;图表作为底部工作区。适合先看全局几何。

-
-
FullDirectionSegment · OBSERVE_ONLY图层 ▣ 重置视图
-
+ − ⛶ 图层
-
终点误差 0.8 cm / 1.2°
v=0 · a=0
制动起点 S=6.4 m
-
框选放大 复位 全屏ST:t (s) / PathS (m)
-
路径总览 LS/ST 运动学 诊断与配置
-
-
- -
-
推荐

B · 地图与分析并列

左侧完整路径,右侧同时看当前选中的 ST/速度图与关键状态,减少来回切页。

-
-
方向段 0 · Forward · 完整规划成功导出快照 图层
-
+ − ⛶
-
ST:完整起步—巡航—停车
巡航目标 1.0 m/s
终点 3 cm / 5° 内
无零进度异常
-
路径总览 LS/ST 曲率 速度/加速度/jerk 周期诊断
-
-
- -
-

C · 多图分析墙

地图在上方,LS/ST/速度三图并排;适合桌面大屏同时比较数据。

-
-
完整方向段分析同步缩放 复位全部
-
+ − ⛶ 图层
-
LS
ST
速度
-
基础图组 运动学图组 终点验证 配置
-
-
-
diff --git a/.superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html b/.superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html deleted file mode 100644 index 0ed43dc..0000000 --- a/.superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html +++ /dev/null @@ -1,56 +0,0 @@ - - -

每个方向段应如何规划“起步—巡航—停车”?

-

三种实现方式都保持 OBSERVE_ONLY。点击选择;推荐 C,它既表达完整方向段,又保留实时修正能力。

- -
-
-

A · 固定距离开始减速

- - - - 起步巡航固定点制动 - -

实现简单,但速度、载荷或当前加速度变化后,固定制动点可能过早或来不及停车。

-
- -
-

B · 整段一次性联合优化

- - - - 整段时间与速度共同求解 - -

完整性最好,但长方向段会产生很大的优化问题;现场参数变化时,整段重新求解可能很慢。

-
- -
- 推荐 -

C · 全局速度包络 + 滚动精化

- - - - jerk 起步受限巡航动态停车包络 - -

先为完整方向段计算速度上限与停车包络,再按短周期重算当前轨迹;能主动起步,也能随实时状态调整制动点。

-
-
- -
-开始减速条件:剩余距离 ≤ jerk 受限停车距离(当前 v, 当前 a, 最大减速度, 最大 jerk) + 重规划安全余量 -
diff --git a/.superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html b/.superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html deleted file mode 100644 index dbf016f..0000000 --- a/.superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

正在确认完整方向段规划语义

-

主产物:一次优化得到完整当前方向段的 LS 与 ST;网页显示完整轨迹,而不是短窗口片段。

-
-
diff --git a/.superpowers/brainstorm/1212-1786007808/state/events b/.superpowers/brainstorm/1212-1786007808/state/events deleted file mode 100644 index 73621dd..0000000 --- a/.superpowers/brainstorm/1212-1786007808/state/events +++ /dev/null @@ -1,2 +0,0 @@ -{"type":"click","text":"推荐\n C · 全局速度包络 + 滚动精化\n \n \n \n jerk 起步受限巡航动态停车包络\n \n 先为完整方向段计算速度上限与停车包络,再按短周期重算当前轨迹;能主动起步,也能随实时状态调整制动点。","choice":"c-hybrid","id":null,"timestamp":1786007937984} -{"type":"click","text":"推荐\n C · 全局速度包络 + 滚动精化\n \n \n \n jerk 起步受限巡航动态停车包络\n \n 先为完整方向段计算速度上限与停车包络,再按短周期重算当前轨迹;能主动起步,也能随实时状态调整制动点。","choice":"c-hybrid","id":null,"timestamp":1786007938998} diff --git a/.superpowers/brainstorm/1212-1786007808/state/server-info b/.superpowers/brainstorm/1212-1786007808/state/server-info deleted file mode 100644 index 3735700..0000000 --- a/.superpowers/brainstorm/1212-1786007808/state/server-info +++ /dev/null @@ -1 +0,0 @@ -{"type":"server-started","port":52380,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:52380/?key=c619697a72212b481e509ebd1c9023d987d76f98e8d335c1a428bd2f2e641248","screen_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1212-1786007808\\content","state_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1212-1786007808\\state","idle_timeout_ms":14400000} diff --git a/.superpowers/brainstorm/1212-1786007808/state/server-instance-id b/.superpowers/brainstorm/1212-1786007808/state/server-instance-id deleted file mode 100644 index 31120e7..0000000 --- a/.superpowers/brainstorm/1212-1786007808/state/server-instance-id +++ /dev/null @@ -1 +0,0 @@ -823e5463adedce4ebfd4c6a032a5fb950de021e730acb9aa diff --git a/.superpowers/brainstorm/1212-1786007808/state/server.pid b/.superpowers/brainstorm/1212-1786007808/state/server.pid deleted file mode 100644 index d6e9296..0000000 --- a/.superpowers/brainstorm/1212-1786007808/state/server.pid +++ /dev/null @@ -1 +0,0 @@ -1224 diff --git a/.superpowers/brainstorm/1515-1785381825/state/server-info b/.superpowers/brainstorm/1515-1785381825/state/server-info deleted file mode 100644 index 363a337..0000000 --- a/.superpowers/brainstorm/1515-1785381825/state/server-info +++ /dev/null @@ -1 +0,0 @@ -{"type":"server-started","port":52380,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:52380/?key=c619697a72212b481e509ebd1c9023d987d76f98e8d335c1a428bd2f2e641248","screen_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1515-1785381825\\content","state_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1515-1785381825\\state","idle_timeout_ms":14400000} diff --git a/.superpowers/brainstorm/1515-1785381825/state/server-instance-id b/.superpowers/brainstorm/1515-1785381825/state/server-instance-id deleted file mode 100644 index 8b7b542..0000000 --- a/.superpowers/brainstorm/1515-1785381825/state/server-instance-id +++ /dev/null @@ -1 +0,0 @@ -ee4120676fb587be5e6f7996276b30b3e88f37f9781d30a6 diff --git a/.superpowers/brainstorm/1515-1785381825/state/server.pid b/.superpowers/brainstorm/1515-1785381825/state/server.pid deleted file mode 100644 index c16dfe7..0000000 --- a/.superpowers/brainstorm/1515-1785381825/state/server.pid +++ /dev/null @@ -1 +0,0 @@ -1527 diff --git a/.superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html b/.superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html deleted file mode 100644 index 315b29d..0000000 --- a/.superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html +++ /dev/null @@ -1,37 +0,0 @@ -

六张点图报告:布局草图

-

所有轨迹均以完整采样点绘制,不使用任何连线。每张俯瞰图按路径包围盒加小边距取景,并保持 X/Y 等比例。

- -
-
1. 原始粗路径 · 俯瞰
- - - X (m)Y (m) -

保留障碍物、起终点与灰色粗路径点。

- -
2. 四方法 · 点集对比
- - - - - - ● 粗路径● B样条● Bézier● 五次 -

仅四组路径点、坐标轴和图例;无障碍物、无起终点。

- -
3. B 样条 · 俯瞰
- X (m)Y (m) -

灰色粗路径点作参照;蓝色为全部 B 样条输出点。

- -
4. Bézier · 俯瞰
- X (m)Y (m) -

灰色粗路径点作参照;橙色为全部 Bézier 输出点。

- -
5. 五次 · 俯瞰
- X (m)Y (m) -

灰色粗路径点作参照;绿色为全部五次输出点。

- -
6. 曲率 · 全方法点图
- s (m)κ (m⁻¹)● 粗路径● B样条● Bézier● 五次 -

四组曲率采样点、统一坐标轴和图例;不绘制曲线。

-
- -

请确认:这正是将被导出的六张独立 PNG + SVG 图的内容划分。看完后直接回复“确认”或指出需要调整的那一张。

diff --git a/.superpowers/brainstorm/1528-1785381873/content/waiting.html b/.superpowers/brainstorm/1528-1785381873/content/waiting.html deleted file mode 100644 index a574018..0000000 --- a/.superpowers/brainstorm/1528-1785381873/content/waiting.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

布局已确认,继续在终端中准备实现。

-
diff --git a/.superpowers/brainstorm/1528-1785381873/state/server-instance-id b/.superpowers/brainstorm/1528-1785381873/state/server-instance-id deleted file mode 100644 index 8527a1f..0000000 --- a/.superpowers/brainstorm/1528-1785381873/state/server-instance-id +++ /dev/null @@ -1 +0,0 @@ -fe7e537ea1c8795202b14034148de7c503a50e4319e76d42 diff --git a/.superpowers/brainstorm/1528-1785381873/state/server-stopped b/.superpowers/brainstorm/1528-1785381873/state/server-stopped deleted file mode 100644 index 8af0225..0000000 --- a/.superpowers/brainstorm/1528-1785381873/state/server-stopped +++ /dev/null @@ -1 +0,0 @@ -{"reason":"idle timeout","timestamp":1785396457111} diff --git a/.superpowers/brainstorm/1528-1785381873/state/server.pid b/.superpowers/brainstorm/1528-1785381873/state/server.pid deleted file mode 100644 index bf9b507..0000000 --- a/.superpowers/brainstorm/1528-1785381873/state/server.pid +++ /dev/null @@ -1 +0,0 @@ -1540 diff --git a/.superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html b/.superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html deleted file mode 100644 index 8f73439..0000000 --- a/.superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html +++ /dev/null @@ -1,73 +0,0 @@ -

实时轨迹规划网页看板:布局选择

-

三种方案都保持 OBSERVE_ONLY。蓝线表示当前规划,灰线表示上一轮规划;红点专门标记滚动交接处。

- - - -
-
-
-
-
A · 总览 + 分析页签● cycle 128 · Rolling
-
-
世界路径 / 当前与上一轮
-
-
关键状态
v
0.18
a
0.04
j
-0.12
-
交接检查
速度连续 ✓
非零滚动终点 ✓
jerk 合规 ✓
-
-
-
LS / ST曲率v-a-j历史
-
L-S
T-S
T-V
-
-
-

A · 分层混合(推荐)

首屏保留路径、状态和异常;曲线按页签切换。信息完整,浏览器与车辆端的内存/重绘压力较平衡。

-
- -
-
-
-
B · 六图同时显示● LIVE 20 Hz
-
-
世界路径
L-S
T-S
-
曲率-S
速度-T
加速度-T
-
加加速度-T
规划耗时
交接误差
-
-
cycle 128OSQP 42 ms0 violations
-
-
-

B · 密集工程屏

一次看到所有图,适合大显示器持续监控;小屏可读性较差,浏览器渲染负载最高。

-
- -
-
-
-
C · 单图聚焦Rolling handoff #128
-
路径LSSTJerk-T
-
加加速度 / 限值带
-
当前轮
max |j| 0.31
交接点
Δa 0.02
上一轮末端
v 0.17
下一轮首点
v 0.18
-
-
-

C · 单图诊断

一次聚焦一条曲线,最轻量、适合笔记本;跨图对照需要切换,实时总览能力较弱。

-
-
- -

请点击一个布局。我的建议是 A:默认总览轻量,发生异常时再切到曲率或 v-a-j 细节。

diff --git a/.superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html b/.superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html deleted file mode 100644 index 17dd441..0000000 --- a/.superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html +++ /dev/null @@ -1,70 +0,0 @@ -

科研绘图风 · A 布局精修

-

以论文图表为基准:细线、白底、单位完整、图例克制;红色只表示真正越界。

- - - -
-
Preview: Scientific trajectory dashboard
-
-
-
-
EM Trajectory Planning Observation
OBSERVE_ONLY · local session · coordinate units: m / s / rad
-
● LIVE   Cycle 128   RollingContinuation   42 ms
-
-
-
-
(a) World path and rolling handoff━ current   ┄ previous   ● handoff
- - - - - - - - World X (m)World Y (m) - -
-
-
Current sample
t0.35 s
v0.182 m/s
a0.041 m/s²
j−0.118 m/s³
κ0.224 m⁻¹
-
Rolling continuity
terminal speed ≠ 0PASS
|Δv| ≤ tol0.003
|Δa| ≤ tol0.018
|j| ≤ limitPASS
trajectory age61 ms
-
-
-
LS / STCurvatureVelocity / Acceleration / JerkCycle history
-
-
(b) Lateral offset
s (m)l (m)
-
(c) Longitudinal progress
t (s)s (m)
-
(d) Signed velocity
t (s)v (m/s)
-
-
Refresh: 10 Hz · latest-snapshot transport · history: 60 cycles (bounded) · browser clients: 1 · no planner backpressure
-
-
-
- -
-

采用这个科研风格

后续页面都沿用细线、白底、论文式多子图与单位标注。

-
diff --git a/.superpowers/brainstorm/1612-1785939150/content/dashboard-style.html b/.superpowers/brainstorm/1612-1785939150/content/dashboard-style.html deleted file mode 100644 index 0e060fd..0000000 --- a/.superpowers/brainstorm/1612-1785939150/content/dashboard-style.html +++ /dev/null @@ -1,74 +0,0 @@ -

A 布局:选择视觉风格

-

图表颜色始终语义一致:当前轨迹为蓝色、上一轮为灰色、交接点为橙色、越界才使用红色。

- - - -
-
-
-
轨迹规划观测台● 实时 · Cycle 128
-
世界路径
-
当前状态
v
0.18
a
0.04
j
-0.12
交接检查
✓ 非零滚动终点
✓ 速度/加速度连续
✓ Jerk 合规
-
LS / ST曲率v-a-j历史
L-S
T-S
T-V
-

1 · 清晰实验室(推荐)

浅色、低饱和、高可读性,适合长时间观察、截图和报告。

-
- -
-
-
TRAJECTORY OBSERVATIONLIVE / 128
-
WORLD PATH
-
STATE
v
0.18
a
0.04
j
-0.12
HANDOFF
PASS / rolling
PASS / continuity
PASS / jerk
-
LS / STCURVATUREV-A-JHISTORY
L-S
T-S
T-V
-

2 · 工程图纸

米白与墨色,接近论文/仪器输出,克制但稍显传统。

-
- -
-
-
轨迹规划观测台● 实时 · Cycle 128
-
世界路径
-
当前状态
v
0.18
a
0.04
j
-0.12
交接检查
✓ 非零滚动终点
✓ 速度/加速度连续
✓ Jerk 合规
-
LS / ST曲率v-a-j历史
L-S
T-S
T-V
-

3 · 石墨夜间

中性深灰、低炫光;比上一版更克制,适合暗光车间。

-
-
- -

请点击一种风格;也可以回复“以 1 为基础,但……”。

diff --git a/.superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html b/.superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html deleted file mode 100644 index 093b38a..0000000 --- a/.superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

科研风格已确认

-

界面说明使用中文;坐标轴、单位和数学符号保留英文规范。继续在终端讨论数据服务架构。

-
-
diff --git a/.superpowers/brainstorm/1612-1785939150/state/server-stopped b/.superpowers/brainstorm/1612-1785939150/state/server-stopped deleted file mode 100644 index 41cca7d..0000000 --- a/.superpowers/brainstorm/1612-1785939150/state/server-stopped +++ /dev/null @@ -1 +0,0 @@ -{"reason":"stop-server.sh","timestamp":1785940779} diff --git a/.superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html b/.superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html deleted file mode 100644 index da7344e..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html +++ /dev/null @@ -1,54 +0,0 @@ -

不同算法应该怎样被可视化?

-

请选择生成策略。重点考虑:既要自动化,也要让不同算法的关键机制真正容易理解。

- -
-
-
-
A · 推荐:自适应图形族
-
-
数据处理函数
输入 → 变换 → 校验 → 输出
-
搜索/规划算法
起点 ⇢ 扩展 ⇢ 代价 ⇢ 目标
-
状态机
Idle ⇄ Running → Error
-
数值/几何算法
曲线、约束区间、异常峰值
-
-
-
-

根据算法特征自动选择主视图

-

所有类型共享同一诊断叠加协议,但主图可以是流程、搜索路径、状态转移或数值曲线。表达力最强,同时保留统一点击体验。

-
-
- -
-
-
B · 通用流程图
-
- 输入 - 步骤 A - 步骤 B ⚠ - 输出 -
-
-
-

所有函数统一画成节点流程

-

生成稳定、实现简单、读法一致;但数值曲线、状态跳转和搜索空间等算法特征会被压扁,可能仍停留在“文字方框”。

-
-
- -
-
-
C · 人工指定视图
-
-
日报输入中指定:visual_type = curve
-
自定义节点、坐标、连线和交互说明
-
-
-
-

由 Agent 或用户提供专用图形描述

-

定制能力最高,但生成成本大、输入要求高,也更依赖当次 Agent 是否完整理解算法,难以保证每份日报都有图。

-
-
-
- -
-

建议 A:使用“统一诊断协议 + 自适应主视图”。不管图形类型如何变化,点击节点后都固定展示源码、当前状态、原因、后果、修正方案和预期结果。

-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html b/.superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html deleted file mode 100644 index 52a45c4..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html +++ /dev/null @@ -1,89 +0,0 @@ -

修订方向:展示算法的真实领域效果

-

下面以泊车路径规划为例。主画面直接呈现规划场景与算法结果,流程和文字只负责解释“为什么”。

- -
-
路径规划诊断 · Parking Scenario #08
-
-
- 正常机制 - ● 当前问题 - ○ 候选修正预演 - ✓ 已验证结果 - 播放搜索 逐步执行 重置视角 -
- -
-
- - - - - - - - - - - - - - - 目标车位 - - - - - - 起始姿态 - - - - - - - - - - 最小间距失效 - 车体包络进入障碍膨胀区 - - - - 当前轨迹 - 修正预演 - - - -
-
环境建模
-
状态扩展
-
碰撞检查 ⚠
-
路径平滑
-
安全验证
-
-
- - -
-
-
- -
-
-

就是这个方向

算法主区域展示真实业务对象和运行结果,诊断信息与源码证据随点击联动。

-
-
-

还需要调整

保留领域可视化方向,但需要改变内容层次或交互方式。

-
-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html b/.superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html deleted file mode 100644 index f87a7ed..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html +++ /dev/null @@ -1,72 +0,0 @@ -

交互设计:从算法效果一路追到验证结果

-

示意的是所有领域共用的操作方式;中央主画面会根据算法自动变成路径、曲线、搜索树、状态机或其他实际效果图。

- -
-
- 算法诊断报告算法:路径规划器 ▾ 问题:最小间距失效 ▾ -
-
-
- - -
-
- 领域效果主画面播放 暂停 单步 缩放 重置 -
-
-
-
输入/起点
-
输出/目标
- - - - - - -
点击异常对象
主画面与右侧诊断联动
-
-
-
输入阶段
核心计算
异常阶段 ⚠
输出验证
-
-
- - -
- -
-
一次完整点击路径
-

选择算法 → 查看正常机制 → 点击异常对象 → 播放影响传播 → 逐步预演纠正方案 → 切换已验证结果

-
-
-
- -
-
-

确认这套交互

领域主画面、四种状态、对象诊断卡和证据下钻共同组成报告页面。

-
-
-

需要调整

说明希望改变的交互顺序、页面区域或信息层次。

-
-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/interaction-layout.html b/.superpowers/brainstorm/1923-1785734653/content/interaction-layout.html deleted file mode 100644 index 052ec8a..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/interaction-layout.html +++ /dev/null @@ -1,69 +0,0 @@ -

选择算法诊断页的交互结构

-

三种方案都能展示“正常功能 → 当前问题 → 原因 → 后果 → 纠正方案 → 预期结果”。请重点比较:第一眼是否容易理解,以及点击后能否逐层深入。

- -
-
-
-
B · 推荐:双层算法地图
-
算法基线 | 问题叠加 | 修正预演  [当前:问题叠加]
-
-
- 输入路径 - 曲率计算 ⚠ - 平滑约束 - 输出轨迹 -
-
-
原因
边界导数不连续
-
影响传播
曲率峰值 → 控制抖动
-
纠正预演
新约束 → 预期稳定
-
-
-
-
-

基线流程 + 故障/修正叠加层

-

先固定展示算法正常机制;点击问题后在同一流程上高亮故障节点、原因和影响,再切换到修正方案预演。上下文不丢失,最适合函数与算法诊断。

-
-
- -
-
-
A · 线性故事链
-
-
1. 原算法功能 输入如何变成输出
-
↓ 点击继续
-
2. 当前问题 异常发生在哪里
-
-
3. 原因与后果 为什么错、会导致什么
-
-
4. 纠正与预期 改什么、预计变成什么
-
-
-
-

从上到下的诊断故事

-

阅读顺序最直观,适合汇报展示;但算法复杂或问题较多时页面会变长,比较多个问题时容易失去原流程位置。

-
-
- -
-
-
C · 自由探索工作台
-
-
函数列表

calculate()

smooth()

validate()
-
-
可缩放算法关系图
节点可自由点击
-
-
-
节点详情
输入
源码位置
问题
根因
影响
验证
-
-
-
-

图谱式诊断工作台

-

探索能力最强,适合大型算法和多个函数;但实现与维护成本最高,日报第一次打开时也更容易让读者迷失。

-
-
-
- -
-

我的建议:选择 B。它保留原算法作为稳定“地图”,问题和解决方案都是同一张地图上的可切换叠加层,最符合你强调的“点击就能理解目前状况和修正后结果”。

-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html deleted file mode 100644 index 4ae751c..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

领域算法可视化方向已确认

-

正在终端中比较可复用的实现架构……

-
-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html deleted file mode 100644 index 4339a62..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

已确定:混合粒度算法图

-

主图展示算法阶段,节点可下钻至函数与源码。正在终端中确认修正预演的证据状态……

-
-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html deleted file mode 100644 index ec9883f..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

已选择:B · 双层算法地图

-

正在终端中确认算法节点的组织粒度……

-
-
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-validation.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-validation.html deleted file mode 100644 index 7e033f6..0000000 --- a/.superpowers/brainstorm/1923-1785734653/content/waiting-validation.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

交互设计已确认

-

正在终端中确认降级规则与验收标准……

-
-
diff --git a/.superpowers/brainstorm/1923-1785734653/state/server-stopped b/.superpowers/brainstorm/1923-1785734653/state/server-stopped deleted file mode 100644 index 60a2089..0000000 --- a/.superpowers/brainstorm/1923-1785734653/state/server-stopped +++ /dev/null @@ -1 +0,0 @@ -{"reason":"stop-server.sh","timestamp":1785737557} diff --git a/.superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html b/.superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html deleted file mode 100644 index dc768c2..0000000 --- a/.superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html +++ /dev/null @@ -1,10 +0,0 @@ - -

网页主界面布局选择

-

三种方案都包含独立页签、X/Y 数值刻度与单位、图例、图层显隐、框选/滚轮缩放、复位及单图全屏。

-
-

A · 地图优先

大地图、右侧状态、底部单张分析图。

完整方向段 · OBSERVE_ONLY图层 复位
+ − ⛶
终点 3 cm / 5°
v=0 · a=0
制动点 S=6.4m
框选 复位 ⛶完整 ST
路径 LS/ST 运动学 诊断
-
推荐

B · 地图与分析并列

左侧完整路径,右侧同时看选中图表和终点状态。

段 0 · Forward · 完整规划成功图层 导出
+ − ⛶
框选 ⛶完整 ST:起步—巡航—停车
目标速度 1.0m/s
终点误差合格
无零进度异常
路径 LS/ST 曲率 速度/加速度/jerk 诊断
-

C · 多图分析墙

地图在上,LS、ST、速度三图始终并排。

完整方向段分析同步缩放 复位全部
+ − ⛶
LS
ST
速度
基础图组 运动学 终点验证 配置
-
diff --git a/.superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html b/.superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html deleted file mode 100644 index 27a05be..0000000 --- a/.superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html +++ /dev/null @@ -1,23 +0,0 @@ - -

论文风格 · 细线版

-

恢复之前的科研绘图语言,并进一步减小所有线宽。蓝色只表示完整规划轨迹,红色只表示真实越界。

-
Preview: Full-direction-segment trajectory observation
-
-
EM Full-Direction-Segment Trajectory Observation
OBSERVE_ONLY · segment 0 / Forward · units: m, s, rad
● VALID FullDirectionSegment solve 186 ms
-
-
(a) World path and complete planned trajectoryLayers Box zoom Reset Full screen
- World X (m)World Y (m)brake start -
-
Terminal boundary
Δ position0.012 m
Δ yaw1.8°
v terminal0.000 m/s
a terminal0.000 m/s²
yaw rate0.000 rad/s
Profile summary
segment length8.42 m
planned duration12.80 s
cruise target1.00 m/s
brake start6.31 m
zero progressNO
-
-
LS / STCurvatureVelocity / Acceleration / JerkTerminal validationConfiguration
-
-
(b) Lateral offsetZoom Reset ⛶
ReferenceS (m)l (m)
-
(c) Complete STZoom Reset ⛶
t (s)PathS (m)
-
(d) Signed velocityZoom Reset ⛶
t (s)v (m/s)
-
-
Thin scientific rendering · current trajectory 0.70 px · reference 0.50 px · grid 0.35 px · all axes include numeric ticks and units · per-plot zoom does not alter source data
-
-

采用论文风格细线版

后续页面沿用这套排版、线宽、颜色和交互密度。

diff --git a/.superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html b/.superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html deleted file mode 100644 index ea56516..0000000 --- a/.superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

保留原页面与论文风格

-

当前设计已收紧为:修复空白、页签、数据、刻度和图层缺陷;补充 s_end、换向点、车辆位置与路径对比,不重做布局。

-
-
diff --git a/.superpowers/brainstorm/985-1786008552/state/server-instance-id b/.superpowers/brainstorm/985-1786008552/state/server-instance-id deleted file mode 100644 index 0a6049c..0000000 --- a/.superpowers/brainstorm/985-1786008552/state/server-instance-id +++ /dev/null @@ -1 +0,0 @@ -df42eac72602791b7f917cb1b2703cca549757ea1ad9eca5 diff --git a/.superpowers/brainstorm/985-1786008552/state/server-stopped b/.superpowers/brainstorm/985-1786008552/state/server-stopped deleted file mode 100644 index ecded91..0000000 --- a/.superpowers/brainstorm/985-1786008552/state/server-stopped +++ /dev/null @@ -1 +0,0 @@ -{"reason":"idle timeout","timestamp":1786050319918} diff --git a/.superpowers/brainstorm/985-1786008552/state/server.pid b/.superpowers/brainstorm/985-1786008552/state/server.pid deleted file mode 100644 index eeb072e..0000000 --- a/.superpowers/brainstorm/985-1786008552/state/server.pid +++ /dev/null @@ -1 +0,0 @@ -997 diff --git a/.task8-sweep/AGV.cs b/.task8-sweep/AGV.cs deleted file mode 100644 index b0c86c0..0000000 --- a/.task8-sweep/AGV.cs +++ /dev/null @@ -1,534 +0,0 @@ -using ClumsyCore; -using ClumsyCore.Interfaces; -using ClumsyCore.Sensors; -using CommonUsage.Chassis; -using CommonUsage.Mathematics; -using FundamentalLib; -using MDCSToolBox.Clumsy.AgvInterfaces; -using MDCSToolBox.Clumsy.Calibration; -using MDCSToolBox.Clumsy.MotionControllers; -using MDCSToolBox.Clumsy.Tracks; -using MDCSToolBox.Commons.Controllers; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Numerics; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using static ClumsyCore.DTools.Painter; - -namespace MultiWheelC -{ - public class SetLocationRes - { - public float x, y, th; - public int l_step; - public long tick; - public string error; - } - public class AGV : MultiWheelInterface - { - public override AbstractGeometricController GetController() - { - return new ChassisController().Get(); - } - - public override MultiWheelMagTracker GetMagController() - { - return new MultiWheelMagTracker(); - } - - public override NaiveMagnetController GetNaiveMagnetController() - { - return new NaiveMagnetController(); - } - - public void Sleep(float s) - { - new DriveTask(new Sleep() { Second = s }.Get()).Wait(); - } - - public void ControlChargePort(bool open) - { - DLog.Log($"call ControlChargePort({open})"); - PilotDefinition.Self.OpenChargeByClumsy = open; - } - - public void SwitchLidarArea(int area) - { - DLog.Log($"call SwitchLidarArea({area})"); - PilotDefinition.Self.AreaChoose = area; - } - - public void SwitchIoArea(int area) - { - if (area != -1) - { - PilotDefinition.Self.IOObstacleArea = area; - } - } - public void RotateToTarget(float target) - { - //if (!needrotate) return; - var dl = new DriveTask(new MultiWheelRotateInPlace() - { - AngleTarget = target, - PidparamsRead = () => new PIDParams() - { - Kp = PilotDefinition.Conf.TireFollowingThkp, - Ki = PilotDefinition.Conf.TireFollowingThki, - Kd = PilotDefinition.Conf.TireFollowingThkd, - DeadZone = PilotDefinition.Conf.TireFollowingThDeadZone, - SpeedAccPerSec = PilotDefinition.Conf.TireFollowingThSpeedAccPerSec, - OutputUpperThreshold = PilotDefinition.Conf.TireFollowingThThresh, - MaxI = PilotDefinition.Conf.TireFollowingThMaxI, - } - }.Get()); - dl.Wait(); - } - - //1tireNum Ҫ̥ - //2frontLidarDetect true:ǰ״ʶ false:״ʶ - public void TireFollowing(int tireNum, bool frontLidarDetect, int srcId, int dstId) - { - while (!TryLock(dstId)) - { - Thread.Sleep(50); - } - DLog.Log($"{dstId}", "TireFollowing"); - var lidarName = frontLidarDetect ? "ǰ״" : "״"; - DLog.Log($"ʼ공ͨ{lidarName}ʶ{tireNum}̥", "TireFollowing"); - if (tireNum != 1 && tireNum != 2) - { - DLog.Log($"TireNum12 (ǰ:{tireNum})", "TireFollowing"); - return; - } - - if (PilotDefinition.Self.GhostMode) - { - while (!TryLock(dstId)) - { - Console.WriteLine("ȴȡ..."); - Thread.Sleep(200); - } - Console.WriteLine($"{dstId}"); - Thread.Sleep(1000); - Console.WriteLine($"ʼ공ͨ{lidarName}ʶ{tireNum}̥"); - Thread.Sleep(1000); - Leave(srcId); - Console.WriteLine($"ʼһäߣʱͷԤȡ{srcId}"); - Thread.Sleep(2000); - //Leave(dstId); - //Console.WriteLine($"һäߣʱͷȡ{dstId}"); - Thread.Sleep(2000); - Console.WriteLine($"공"); - return; - } - - var detectors = new List() - { - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, frontLidarDetect), - StartGuessingX = frontLidarDetect ? PilotDefinition.Conf.TireFollowingStage1GuessX : -PilotDefinition.Conf.TireFollowingStage1GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, - frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0), - LeaveSrcFunction = Leave, - SrcId = srcId, - DstId = dstId, - }, - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, frontLidarDetect), - StartGuessingX = frontLidarDetect ? PilotDefinition.Conf.TireFollowingStage2GuessX : -PilotDefinition.Conf.TireFollowingStage2GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, - frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0) - }, - }; - DLog.Log($"̥Ϊ{tireNum}", "TireFollowing"); - var following = new TireFollowing() - { - GetController = () => new ChassisController().Get(), - GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, - GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, - detectors = detectors, - SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, - MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, - TireNum = tireNum, - CarDirection = frontLidarDetect ? 0f : 180f, - WalkBlindTh = frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarWalkBlindTh : PilotDefinition.Conf.TireFollowingBackLidarWalkBlindTh, - }; - var _dt = new DriveTask(following.Get()); - _dt.Wait(); - DLog.Log("공", "TireFollowing"); - } - - //복һǺ״ʶһ̥ - public void LeaveCar(int srcId, float srcX, float srcY, int dstId, float dstX, float dstY) - { - while (!TryLock(dstId)) - { - Thread.Sleep(50); - } - DLog.Log($"{dstId}", "TireFollowing"); - DLog.Log($"ʼ공ͨ״ʶ1̥", "TireFollowing"); - var chassis = (MultiWheelChassis)PilotDefinition.Chassis; - chassis.SetOriginBias(0, 0, 0); - var following = new TireFollowing() - { - GetController = () => new ChassisController().Get(), - GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, - GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, - detectors = new List() - { - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, false), - StartGuessingX = -PilotDefinition.Conf.TireFollowingStage2GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingLeaveCarWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - PilotDefinition.Conf.TireFollowingLeaveCarBackLidarPathTransformationX, - PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0), - LeaveSrcFunction = Leave, - SrcId = srcId, - DstId = dstId, - }, - }, - CarDirection = 180f, - SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, - MaxSpeed = 0.25f, - EnableHandover = true, - HandoverDistance = 200f, - HandoverSpeed = 0.3f, - WalkBlindTh = 0, - TireNum = 1 - }; - - IEnumerable LeaveThenFollow() - { - foreach (var running in following.Get()) - { - if (!running) break; - yield return true; - } - DLog.Log($"ͷ{srcId}", "TireFollowing"); - DLog.Log("복TireFollowingʼDstTracker", "TireFollowing"); - - foreach (var running in new DstTracker() - { - Src = new Vector2(srcX, srcY), - Dst = new Vector2(dstX, dstY), - CarDirectionBias = 180f, - InitialSendSpeed = 0.3f - }.Get()) - { - if (!running) break; - yield return true; - } - - yield return false; - } - - var _dt = new DriveTask(LeaveThenFollow()); - _dt.Wait(); - DLog.Log("복1", "TireFollowing"); - } - - public void LineTracking(int srcId, float srcX, float srcY, int dstId, float dstX, float dstY) - { - while (!TryLock(dstId)) - { - Thread.Sleep(50); - } - var chassis = (MultiWheelChassis)PilotDefinition.Chassis; - chassis.SetOriginBias(0, 0, 0); - DLog.Log($"{dstId}", "TireFollowing"); - IEnumerable TrackThenFollow() - { - foreach (var running in new LineTracking() - { - Target = PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, - LeaveSrcFunction = Leave, - SrcId = srcId, - EnableHandover = true, - HandoverDistance = 200, - HandoverSpeed = 0.3f, - }.Get()) - { - if (!running) break; - yield return true; - } - DLog.Log($"ͷ{srcId}", "TireFollowing"); - DLog.Log("복LineTrackingʼDstTracker", "TireFollowing"); - while (!TryLock(426)) - { - Thread.Sleep(20); - } - Leave(dstId); - DLog.Log($"ͷ{dstId}", "TireFollowing"); - foreach (var running in new DstTracker() - { - Src = new Vector2(srcX, srcY), - Dst = new Vector2(dstX, dstY), - InitialSendSpeed = 0.3f - }.Get()) - { - if (!running) break; - yield return true; - } - yield return false; - } - var _dt = new DriveTask(TrackThenFollow()); - _dt.Wait(); - DLog.Log("복2", "TireFollowing"); - } - - //ʹ - public void DriverAble() - { - var dl = new DriveTask(new DriverAble() { }.Get()); - dl.Wait(); - DLog.Log("ʹ", "TireFollowing"); - } - - //ʹ - public void DriverDisable() - { - var dl = new DriveTask(new DriverDisable() { }.Get()); - dl.Wait(); - DLog.Log("ʹ", "TireFollowing"); - } - - // бclose Ϊ true ʱرռб򿪼б - public void ClamptoTarget(bool close) - { - if (PilotDefinition.Self.GhostMode) - { - Thread.Sleep(2000); - Console.WriteLine("б"); - return; - } - new DriveTask(new ClampToTarget() - { - LeftClampTarget = close ? PilotDefinition.Self.LeftArmUpperPos : PilotDefinition.Self.LeftArmLowerPos, - RightClampTarget = close ? PilotDefinition.Self.RightArmUpperPos : PilotDefinition.Self.RightArmLowerPos - }.Get()).Wait(); - } - // Fleet crab walk: convert scheduler src/dst into the same relative crab-walk path used by MovementTest. - public void FleetCrabWalk(float srcX, float srcY, int srcId, float dstX, float dstY, int dstId, - float speed) - { - var dx = dstX - srcX; - var dy = dstY - srcY; - var pathLength = (float)Math.Sqrt(dx * dx + dy * dy); - - if (pathLength <= 1f) - { - DLog.Log("FleetCrabWalk abort: path length is too short.", "FleetCrabDbg"); - return; - } - - var self = PilotDefinition.Self; - if (!self.TryGetFleetCenterFromMembers(out var centerX, out var centerY, out var centerTh) && - !self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh)) - { - DLog.Log("FleetCrabWalk abort: failed to read fleet center.", "FleetCrabDbg"); - Hedingben.ToastText("FleetCrab requires master localization", "FleetCrab"); - return; - } - - var pathAngle = (float)CommonMath.RoundTh((float)(Math.Atan2(dy, dx) / Math.PI * 180.0)); - var crabAngle = (float)CommonMath.ThDiff(pathAngle, centerTh); - var targetBodyWorldHeading = (float)CommonMath.RoundTh(PilotDefinition.Conf.FleetCrabBodyWorldHeadingDeg); - var bodyToPathAngle = (float)CommonMath.ThDiff(pathAngle, targetBodyWorldHeading); - DLog.Log( - $"call FleetCrabWalk(src=({srcX:0},{srcY:0},id:{srcId}), dst=({dstX:0},{dstY:0},id:{dstId}), " + - $"len={pathLength:0.0}, speed={speed:0.000}, pathAngle={pathAngle:0.0}, " + - $"center=({centerX:0},{centerY:0},{centerTh:0.0}), crabAngle={crabAngle:0.0}, " + - $"targetBodyWorld={targetBodyWorldHeading:0.0}, bodyToPath={bodyToPathAngle:0.0})", - "FleetCrabDbg"); - - if (dstId != -1) - { - while (!TryLock(dstId)) - { - Thread.Sleep(50); - } - DLog.Log($"{dstId}", "FleetCrabDbg"); - } - - var action = new MultiWheelC.FleetCrabWalk - { - CrabAngleDeg = crabAngle, - BodyToPathAngleDeg = bodyToPathAngle, - CrabLengthMm = pathLength, - CrabSpeed = speed, - FleetCrabAccel = PilotDefinition.Conf.FleetCrabAccel, - FleetCrabStartAccel = PilotDefinition.Conf.FleetCrabStartAccel, - FleetCrabSlowDistance = PilotDefinition.Conf.FleetCrabSlowDistance, - FleetCrabFinishDistance = PilotDefinition.Conf.FleetCrabFinishDistance, - FleetCrabFinishSpeed = PilotDefinition.Conf.FleetCrabFinishSpeed, - FleetCrabSlowingPow = PilotDefinition.Conf.FleetCrabSlowingPow, - GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold - }; - - try - { - new DriveTask(action.Get()).Wait(); - } - finally - { - if (srcId != -1) - { - Leave(srcId); - DLog.Log($"ͷŷų{srcId}", "FleetCrabDbg"); - } - } - } - - public void FleetCurveWalk(float srcX, float srcY, int srcId, float dstX, float dstY, int dstId, - float speed, params float[] trackTypeInfo) - { - if (trackTypeInfo == null || trackTypeInfo.Length < 2) - { - DLog.Log("FleetCurveWalk abort: invalid trackTypeInfo, expected Bezier type info.", "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve invalid trackTypeInfo", "FleetCurve"); - return; - } - - var trackType = (int)trackTypeInfo[0]; - if (trackType != 2) - { - DLog.Log($"FleetCurveWalk abort: unsupported trackType={trackType}, only Bezier(type=2) is supported.", - "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve only supports Bezier trackType=2", "FleetCurve"); - return; - } - - var controlPointNum = (int)trackTypeInfo[1]; - var expectedLength = 2 + controlPointNum * 2; - if (controlPointNum < 3 || trackTypeInfo.Length < expectedLength) - { - DLog.Log( - $"FleetCurveWalk abort: invalid Bezier trackTypeInfo. controlPointNum={controlPointNum}, " + - $"length={trackTypeInfo.Length}, expected>={expectedLength}.", - "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve invalid Bezier trackTypeInfo", "FleetCurve"); - return; - } - - BezierTrack track; - try - { - track = ProcessTrackTypeInfo(srcX, srcY, dstX, dstY, trackTypeInfo) as BezierTrack; - } - catch (Exception ex) - { - DLog.Log($"FleetCurveWalk abort: failed to process trackTypeInfo. {ex.Message}", "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve failed to process track", "FleetCurve"); - return; - } - - if (track == null) - { - DLog.Log("FleetCurveWalk abort: ProcessTrackTypeInfo did not return BezierTrack.", "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve requires BezierTrack", "FleetCurve"); - return; - } - - track.Speed = speed; - track.CarDirectionBias = 0f; - - DLog.Log( - $"call FleetCurveWalk(src=({srcX:0},{srcY:0},id:{srcId}), dst=({dstX:0},{dstY:0},id:{dstId}), " + - $"speed={speed:0.000}, trackType={trackType}, controls={controlPointNum}, track={track.GetType().Name}, " + - $"carDirectionBias=0.0)", - "FleetCurveDbg"); - - if (dstId != -1) - { - while (!TryLock(dstId)) - { - Thread.Sleep(50); - } - DLog.Log($"锁点{dstId}完成", "FleetCurveDbg"); - } - - var action = new MultiWheelC.FleetCurveWalk - { - Track = track, - CurveSpeed = speed, - CarDirectionBias = 0f, - SlowDistance = PilotDefinition.Conf.FleetCurveSlowDistance, - FinishDistance = PilotDefinition.Conf.FleetCurveFinishDistance, - FinishSpeed = PilotDefinition.Conf.FleetCurveFinishSpeed, - SlowingPow = PilotDefinition.Conf.FleetCurveSlowingPow, - GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold, - StartSyncTimeoutSec = PilotDefinition.Conf.FleetCrabStartSyncTimeoutSec - }; - - try - { - new DriveTask(action.Get()).Wait(); - } - finally - { - if (srcId != -1) - { - Leave(srcId); - DLog.Log($"release srcId={srcId}", "FleetCurveDbg"); - } - } - } - - public void ChangeAvoidanceDistance(float stopDistance, float slowDistance) - { - DLog.Log($"call ChangeAvoidanceDistance({stopDistance},{slowDistance})"); - PilotDefinition.Self.SlowDistance = slowDistance; - PilotDefinition.Self.StopDistance = stopDistance; - } - public void ChangeAvoidanceParam(float length = -1, float width = -1) - { - - PilotDefinition.Self.CarLength = length; - PilotDefinition.Self.CarWidth = width; - - } - public void SetLocation(float x, float y, float th) - { - DLog.Log($"call SetLocation({x},{y},{th})"); - Console.WriteLine($"call SetLocation({x},{y},{th})"); - Queue(() => - { - while (true) - { - var str1 = new HttpClient() - .GetStringAsync( - $"http://127.0.0.1:4321/setLocation?x={x}&y={y}&th={th}") - .Result; - Thread.Sleep(500); - Console.WriteLine($"SetLocation str={str1}"); - var setLocationRes = JsonConvert.DeserializeObject(str1); - Console.WriteLine(setLocationRes.l_step); - if (setLocationRes != null && setLocationRes.l_step == 2) break; - } - }); - } - public float baseSpeed = 0; - } -} diff --git a/.task8-sweep/ChassisController.cs b/.task8-sweep/ChassisController.cs deleted file mode 100644 index c041b2a..0000000 --- a/.task8-sweep/ChassisController.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Numerics; -using ClumsyCore; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using FundamentalLib; -using MDCSToolBox.Clumsy.MotionControllers; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; - -namespace MultiWheelC; - -public class ChassisController : MovementDefinition -{ - public float BaseSpeed = Configuration.conf.basicSpeed; - - private DateTime _sendMotionDbgLast = DateTime.MinValue; - - public override MultiWheelGeometricController Get() - { - return new MultiWheelGeometricController - { - Chassis = BasicPilotBase.Chassis, - BaseSpeed = BaseSpeed, - SlowDistance = PilotDefinition.Conf.SlowDistance, - SlowingPow = PilotDefinition.Conf.SlowingPow, - FinishDistance = PilotDefinition.Conf.FinishDistance, - FinishSpeed = PilotDefinition.Conf.FinishSpeed, - FirstThAccuracy = PilotDefinition.Conf.FirstThAccuracy, - FirstRotateSpeedFac = PilotDefinition.Conf.FirstRotateSpeedFac, - FirstRotateMaxSpeed = PilotDefinition.Conf.FirstRotateMaxSpeed, - NotContinuousAngle = PilotDefinition.Conf.NotContinuousAngle, - DebugMode = PilotDefinition.Conf.MotionDebugPrint, - DebugCurvature = PilotDefinition.Conf.DebugCurvature, - PowerSteeringLookAhead = PilotDefinition.Conf.PowerSteeringLookAhead, - SpeedLookAhead = PilotDefinition.Conf.SpeedLookAhead, - SpeedLookAheadCurveDiff = PilotDefinition.Conf.SpeedLookAheadCurveDiff, - SpeedLookBackCurveDiff = PilotDefinition.Conf.SpeedLookBackCurveDiff, - SpeedLimitCurveDiffMin = PilotDefinition.Conf.SpeedLimitCurveDiffMin, - SpeedLimitCurveMin = PilotDefinition.Conf.SpeedLimitCurveMin, - MaxRotateSpeed = PilotDefinition.Conf.MaxRotateSpeedCurveLimit, - MaxRotateAcc = PilotDefinition.Conf.MaxRotateAccCurveLimit, - GcpThetaThreshold = PilotDefinition.Conf.GcpThetaThreshold, - DthLinearFac = PilotDefinition.Conf.DthLinearFac, - DthLinearThreshold = PilotDefinition.Conf.DthLinearThreshold, - BiasFac = PilotDefinition.Conf.BiasFac, - BiasThreshold = PilotDefinition.Conf.BiasThreshold, - - MultiVehicleSendMotion = (speed, frontTh, rearTh, idealPos, idealAngle) => - { - var self = PilotDefinition.Self; - self.MultiVehicleAutoEnabled = true; - // A: 用固定锁对象(不再锁会被替换的字段引用)。 - int fleetCnt; - lock (self.FleetLock) - fleetCnt = self.MultiVehicleFleet.Count; - - // 诊断(节流 ~300ms):确认回调被调用、编队是否就绪、是否因数量不符提前 return(导致不下发速度)。 - if ((DateTime.Now - _sendMotionDbgLast).TotalMilliseconds >= 300) - { - _sendMotionDbgLast = DateTime.Now; - DLog.Log( - $"SENDMOTION speed={speed:0.000} fTh={frontTh:0.0} rTh={rearTh:0.0} " + - $"ideal=({idealPos.X:0},{idealPos.Y:0},{idealAngle:0.0}) " + - $"editCnt={fleetCnt}/{PilotDefinition.Conf.MultiVehicleFleetNum} " + - $"earlyReturn={fleetCnt != PilotDefinition.Conf.MultiVehicleFleetNum}", - "FleetCrabDbg"); - } - - if (fleetCnt != PilotDefinition.Conf.MultiVehicleFleetNum) - return; - - self.MultiVehicleAutoVx = speed; - self.MultiVehicleAutoFrontTh = frontTh; - self.MultiVehicleAutoRearTh = rearTh; - // D: 透传路径控制器算出的理想车队中心位姿(此前被丢弃),供各车按 layout 做前馈。 - self.MultiVehicleAutoIdealX = idealPos.X; - self.MultiVehicleAutoIdealY = idealPos.Y; - self.MultiVehicleAutoIdealTh = idealAngle; - self.MultiVehicleAutoHasIdeal = true; - // B: 标记命令新鲜度。路径结束/早退/卡顿不再刷新此时刻 → 主车超时后清零速度,避免滑行。 - self.MultiVehicleAutoCmdTime = DateTime.Now; - }, - - // G: 读取车队中心原子快照,避免跨线程读到撕裂的 x/y/th 组合。 - MultiVehicleGetFleetPos = () => - { - var snap = PilotDefinition.Self.GetFleetCenterSnapshot(); - return new Location - { - x = snap.X, - y = snap.Y, - th = snap.Th, - l_step = 1, - tick = DateTime.Now.Ticks - }; - } - }; - } -} diff --git a/.task8-sweep/ClumsyPilot.csproj b/.task8-sweep/ClumsyPilot.csproj deleted file mode 100644 index 29039db..0000000 --- a/.task8-sweep/ClumsyPilot.csproj +++ /dev/null @@ -1,53 +0,0 @@ - - - - netstandard2.0 - 10 - true - - - - - - - - - - - - - - - - ref\CommonUsage.dll - - - ref\LessokajiWeaverUtilities.dll - - - ref\MDCSToolBox.dll - - - ref\RefClumsyCore.dll - - - ref\RefClumsyDance.dll - - - ref\RefFundamentalLib.dll - - - - - - - - - - - - diff --git a/.task8-sweep/FleetCrabWalk.cs b/.task8-sweep/FleetCrabWalk.cs deleted file mode 100644 index c75333d..0000000 --- a/.task8-sweep/FleetCrabWalk.cs +++ /dev/null @@ -1,526 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using ClumsyCore; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using FundamentalLib; -using CommonUsage.Chassis; -using CommonUsage.Mathematics; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; - -namespace MultiWheelC; - -// ===== 车队联动-自动蟹行动作 ===== -// 以当前车队中心为起点,构造指定方向和长度的直线路径; -// 执行侧直接写 MultiVehicleAuto...,由 TickMultiVehicle 自动分支统一下发。 -// -// 控制思路参考 MDCSToolbox 几何控制器,但实现收在 MultiWheelC 内: -// 1) 读取主车 Detour 反推车队中心,计算沿直线的进度、横向偏差和车身目标朝向偏差; -// 2) 根据横向偏差给前后 GCP 同向修正,根据车身目标朝向偏差给前后 GCP 反向修正; -// 3) 根据终点距离减速,并发布 ideal fleet center 给从车做前馈。 -// -// 前提:在主车(MultiVehicleMasterEndpoint=="/")运行,且主车有 Detour 定位。 -public class FleetCrabWalk : MovementDefinition -{ - /// 路径方向相对启动时车队朝向的夹角(deg,逆时针为正)。 - public float CrabAngleDeg = 45f; - - /// 路径方向相对车身目标朝向的夹角(deg,逆时针为正)。MovementTest 会设为 CrabAngleDeg,以保持启动时车身朝向。 - public float BodyToPathAngleDeg = 45f; - - /// 路径长度(mm)。 - public float CrabLengthMm = 2000f; - - /// 行驶速度(m/s)。 - public float CrabSpeed = 0.2f; - - /// 速度命令加速度限制(m/s^2),小于等于 0 表示不限制。 - public float FleetCrabAccel = 0.2f; - - /// 预对齐后正式下发速度前 5 秒加速度限制(m/s^2),小于等于 0 表示不限制。 - public float FleetCrabStartAccel = 0.01f; - - /// 末端开始减速距离(mm)。 - public float FleetCrabSlowDistance = 2000f; - - /// 完成距离(mm),低于该剩余距离结束动作。 - public float FleetCrabFinishDistance = 20f; - - /// 末端最低速度(m/s)。 - public float FleetCrabFinishSpeed = 0.02f; - - /// 末端减速曲线指数。 - public float FleetCrabSlowingPow = 0.8f; - - /// 前后 GCP 舵角修正上限(deg)。 - public float GcpThetaThreshold = 95f; - - private bool _stopping; - - private void Cleanup() - { - var self = PilotDefinition.Self; - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = 0; - self.MultiVehicleAutoRearTh = 0; - self.MultiVehicleAutoHasIdeal = false; - self.MultiVehicleAutoEnabled = false; - } - - public void Stop() - { - _stopping = true; - Cleanup(); - } - - private static float Clamp(float value, float min, float max) - { - if (value < min) return min; - if (value > max) return max; - return value; - } - - private static float ClampAbs(float value, float limit) - { - var absLimit = Math.Abs(limit); - if (absLimit <= 0) return value; - if (value > absLimit) return absLimit; - if (value < -absLimit) return -absLimit; - return value; - } - - private static float Slew(float current, float target, float maxDelta) - { - if (maxDelta <= 0) return target; - if (target > current + maxDelta) return current + maxDelta; - if (target < current - maxDelta) return current - maxDelta; - return target; - } - - private static float AverageAngle(float frontTh, float rearTh) - { - var diff = (float)CommonMath.ThDiff(frontTh, rearTh); - return (float)CommonMath.RoundTh(rearTh + diff / 2f); - } - - private static void ResolveCrabDriveEquivalent(float speed, float rawFrontTh, float rawRearTh, float steerLimit, - out float driveSpeed, out float frontTh, out float rearTh, out bool reverseEquivalent, out float rawBaseTh) - { - var limit = Math.Min(179f, Math.Max(1f, Math.Abs(steerLimit))); - rawBaseTh = AverageAngle(rawFrontTh, rawRearTh); - driveSpeed = speed; - frontTh = rawFrontTh; - rearTh = rawRearTh; - reverseEquivalent = false; - - if (rawBaseTh > limit) - { - frontTh = (float)CommonMath.RoundTh(frontTh - 180f); - rearTh = (float)CommonMath.RoundTh(rearTh - 180f); - driveSpeed = -driveSpeed; - reverseEquivalent = true; - } - else if (rawBaseTh < -limit) - { - frontTh = (float)CommonMath.RoundTh(frontTh + 180f); - rearTh = (float)CommonMath.RoundTh(rearTh + 180f); - driveSpeed = -driveSpeed; - reverseEquivalent = true; - } - - frontTh = ClampAbs(frontTh, limit); - rearTh = ClampAbs(rearTh, limit); - } - - private static float ProbeSpeed(float speed) - { - return Math.Abs(speed) > 1e-4f ? speed : 1f; - } - - private static bool TryGetMotionYawSign(float frontTh, float rearTh, float driveSpeed, float controlRadius, - out float yawSign) - { - yawSign = 0f; - if (Math.Abs(CommonMath.ThDiff(frontTh, rearTh)) <= 1e-3f) - return false; - - var radius = Math.Max(1f, Math.Abs(controlRadius)); - Vector2 pFront = new(radius, 0), pRear = new(-radius, 0), - normFront = CommonMath.Transform2D(pFront, frontTh + 90f, Vector2.UnitX), - normRear = CommonMath.Transform2D(pRear, rearTh + 90f, Vector2.UnitX); - var (intersect, center) = CommonMath.TwoLinesIntersection(pFront, normFront, pRear, normRear); - if (!intersect) - return false; - - // Match MultiWheelChassis.SendMotion: the tangent side is selected by - // rotCenter.Y > 1, and reverse-equivalent motion flips the yaw direction. - var tangentSign = center.Y > 1f ? 1f : -1f; - var speedSign = driveSpeed >= 0f ? 1f : -1f; - yawSign = speedSign * tangentSign; - return true; - } - - private static float GetYawSplitSign(float baseTh, float speed, float steerLimit, float controlRadius) - { - const float probeDth = 1f; - ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + probeDth, baseTh - probeDth, steerLimit, - out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _); - return TryGetMotionYawSign(probeFrontTh, probeRearTh, probeSpeed, controlRadius, out var yawSign) - ? yawSign - : 1f; - } - - private static float EstimateLateralVelocity(float bodyTh, float frontTh, float rearTh, float driveSpeed, - Vector2 pathLeft) - { - var motionTh = (float)CommonMath.RoundTh(bodyTh + AverageAngle(frontTh, rearTh)); - var rad = motionTh / 180f * Math.PI; - var dir = new Vector2((float)Math.Cos(rad), (float)Math.Sin(rad)); - if (driveSpeed < 0f) - dir = -dir; - return Vector2.Dot(dir, pathLeft); - } - - private static float ScoreBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft, - float lateral, float biasProbe) - { - ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + biasProbe, baseTh + biasProbe, steerLimit, - out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _); - var lateralVelocity = EstimateLateralVelocity(bodyTh, probeFrontTh, probeRearTh, probeSpeed, pathLeft); - return -Math.Sign(lateral) * lateralVelocity; - } - - private static float GetLateralBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft, - float lateral) - { - if (Math.Abs(lateral) <= 1e-3f) - return 1f; - - const float probeBias = 1f; - var positiveScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, probeBias); - var negativeScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, -probeBias); - return positiveScore >= negativeScore ? 1f : -1f; - } - - private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY, - out float centerTh, out string source) - { - if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh)) - { - source = "fleet"; - return true; - } - - if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh)) - { - source = "slam"; - return true; - } - - source = "none"; - return false; - } - - public override IEnumerable Get() - { - var self = PilotDefinition.Self; - var conf = PilotDefinition.Conf; - var chassis = BasicPilotBase.Chassis as MultiWheelChassis; - if (chassis == null) - { - DLog.Log("ABORT: FleetCrabWalk requires MultiWheelChassis.", "FleetCrabDbg"); - yield break; - } - _stopping = false; - - DLog.Log( - $"ENTER master?={conf.MultiVehicleMasterEndpoint == "/"} endpoint={conf.MultiVehicleMasterEndpoint} " + - $"fleetNum={conf.MultiVehicleFleetNum} useDetect={conf.MultiVehicleUseDetect} " + - $"syncUseDetour={conf.MultiVehicleSyncUseDetour} useIdealCenter={conf.MultiVehicleAutoUseIdealCenter} " + - $"autoFields=true pathMode=relative pathAngle={CrabAngleDeg:0.0} " + - $"bodyToPath={BodyToPathAngleDeg:0.0} gcpLimit={GcpThetaThreshold:0.0} " + - $"biasFac={conf.BiasFac:0.00} fleetCrabDthFac={conf.FleetCrabDthLinearFac:0.00}", - "FleetCrabDbg"); - - if (conf.MultiVehicleMasterEndpoint != "/") - { - DLog.Log($"ABORT: 非主车 (endpoint={conf.MultiVehicleMasterEndpoint})", "FleetCrabDbg"); - Hedingben.ToastText("车队蟹行需在主车(主车端点=\"/\")运行", "FleetCrab"); - yield break; - } - - // 注意:getCartLocation() 在无有效 Detour 定位时会阻塞——若卡在这里且后面看不到 CENTER 日志,即定位未就绪。 - DLog.Log("主车校验通过,开始读取车队中心 (getCartLocation 无定位会阻塞)…", "FleetCrabDbg"); - if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource)) - { - DLog.Log("ABORT: TryGetFleetCenterFromSlam 返回 false (无定位)", "FleetCrabDbg"); - Hedingben.ToastText("车队蟹行需要主车 Detour 定位", "FleetCrab"); - yield break; - } - DLog.Log($"CENTER 车队中心=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg"); - - DLog.Log($"CENTER_SOURCE source={initialCenterSource} center=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg"); - - var pathStart = new Vector2(x0, y0); - var pathLengthMm = CrabLengthMm; - var phi = CommonMath.RoundTh(theta + CrabAngleDeg); - var dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0)); - var targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg); - var phiRad = phi / 180.0 * Math.PI; - var pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad)); - var pathLeft = new Vector2(-pathDir.Y, pathDir.X); - - DLog.Log( - $"START center=({x0:0},{y0:0},{theta:0.0}) pathMode=relative " + - $"src=({pathStart.X:0},{pathStart.Y:0}) pathAngle={CrabAngleDeg:0.0} bodyToPath={BodyToPathAngleDeg:0.0} " + - $"phi={phi:0.0} targetBody={targetBodyTh:0.0} " + - $"len={pathLengthMm:0} dst=({dst.X:0},{dst.Y:0}) speed={CrabSpeed:0.000} startAccel={FleetCrabStartAccel:0.000} accel={FleetCrabAccel:0.000} " + - $"slow={FleetCrabSlowDistance:0} finishDist={FleetCrabFinishDistance:0} " + - $"finishSpeed={FleetCrabFinishSpeed:0.000} slowingPow={FleetCrabSlowingPow:0.00}", - "FleetCrabDbg"); - - var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold)); - var controlRadius = Math.Max(1f, Math.Abs(conf.TestCarSyncDistance) / 2f); - ResolveCrabDriveEquivalent(0f, (float)CommonMath.ThDiff(phi, theta), - (float)CommonMath.ThDiff(phi, theta), gcpLimit, out _, out var holdFrontTh, out var holdRearTh, - out _, out _); - var warmStart = DateTime.Now; - var warmSeqBaseline = self.BeginFleetMotionWarmup(); - - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleAutoEnabled = true; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = holdFrontTh; - self.MultiVehicleAutoRearTh = holdRearTh; - self.MultiVehicleAutoIdealX = pathStart.X; - self.MultiVehicleAutoIdealY = pathStart.Y; - self.MultiVehicleAutoIdealTh = targetBodyTh; - self.MultiVehicleAutoHasIdeal = true; - self.MultiVehicleAutoCmdTime = DateTime.Now; - self.PrimeMasterAutoFromSlam(); - DLog.Log( - $"WARMUP auto fields enabled, waiting for fleet startup sync seqBase={warmSeqBaseline} " + - $"hold=({holdFrontTh:0.00},{holdRearTh:0.00})", - "FleetCrabDbg"); - - var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, conf.FleetCrabStartSyncTimeoutSec)); - var warmIter = 0; - var warmReady = false; - var warmDetail = ""; - while (!_stopping && DateTime.Now < warmEnd) - { - warmIter++; - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleAutoEnabled = true; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = holdFrontTh; - self.MultiVehicleAutoRearTh = holdRearTh; - self.MultiVehicleAutoIdealX = pathStart.X; - self.MultiVehicleAutoIdealY = pathStart.Y; - self.MultiVehicleAutoIdealTh = targetBodyTh; - self.MultiVehicleAutoHasIdeal = true; - self.MultiVehicleAutoCmdTime = DateTime.Now; - self.PrimeMasterAutoFromSlam(); - var snap = self.GetFleetCenterSnapshot(); - int cnt; - lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count; - if (warmIter % 5 == 0) - DLog.Log( - $"WARMUP#{warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) tick={snap.Tick} " + - $"autoEn={self.MultiVehicleAutoEnabled} scriptEn={self.MultiVehicleScriptEnabled} cnt={cnt}/{conf.MultiVehicleFleetNum} " + - $"detail={warmDetail}", - "FleetCrabDbg"); - if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline, - conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail)) - { - warmReady = true; - DLog.Log( - $"WARMUP done iter={warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) cnt={cnt} detail={warmDetail}", - "FleetCrabDbg"); - break; - } - yield return true; - } - if (!warmReady) - { - DLog.Log($"WARMUP timeout: fleet startup sync failed, abort action. detail={warmDetail}", - "FleetCrabDbg"); - Hedingben.ToastText("车队蟹行启动同步超时,已取消", "FleetCrab"); - Cleanup(); - yield break; - } - - Hedingben.ToastText($"车队蟹行 路径{phi:0.0}° 车身夹角{BodyToPathAngleDeg:0.0}° 长度{pathLengthMm:0}mm", "FleetCrab"); - - if (warmReady && self.TryGetFleetCenterFromMembers(out var warmX, out var warmY, out var warmTh)) - { - x0 = warmX; - y0 = warmY; - theta = warmTh; - pathStart = new Vector2(x0, y0); - phi = CommonMath.RoundTh(theta + CrabAngleDeg); - dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0)); - targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg); - phiRad = phi / 180.0 * Math.PI; - pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad)); - pathLeft = new Vector2(-pathDir.Y, pathDir.X); - self.MultiVehicleAutoIdealX = pathStart.X; - self.MultiVehicleAutoIdealY = pathStart.Y; - self.MultiVehicleAutoIdealTh = targetBodyTh; - self.MultiVehicleAutoCmdTime = DateTime.Now; - DLog.Log( - $"WARMUP_REBASE source=fleet center=({x0:0},{y0:0},{theta:0.0}) phi={phi:0.0} targetBody={targetBodyTh:0.0} dst=({dst.X:0},{dst.Y:0})", - "FleetCrabDbg"); - } - - var iter = 0; - var lastLog = DateTime.MinValue; - var finishDistance = Math.Max(0f, FleetCrabFinishDistance); - var slowDistance = Math.Max(finishDistance + 1f, FleetCrabSlowDistance); - var baseSpeed = Math.Abs(CrabSpeed); - var finishSpeed = Math.Min(baseSpeed, Math.Abs(FleetCrabFinishSpeed)); - var slowingPow = Math.Max(0.01f, FleetCrabSlowingPow); - var accel = Math.Abs(FleetCrabAccel); - var startAccel = Math.Abs(FleetCrabStartAccel); - var cmdSpeed = 0f; - var lastTick = DateTime.Now; - var speedRampStart = DateTime.Now; - var stopReason = "done"; - - while (!_stopping) - { - iter++; - - if (!TryGetControlFleetCenter(self, out var cx, out var cy, out var cth, out var centerSource)) - { - stopReason = "fleet center invalid"; - DLog.Log("ABORT: TryGetControlFleetCenter returned false during auto crab.", "FleetCrabDbg"); - break; - } - var delta = new Vector2(cx - pathStart.X, cy - pathStart.Y); - var along = Vector2.Dot(delta, pathDir); - var lateral = Vector2.Dot(delta, pathLeft); - var remain = pathLengthMm - along; - if (remain <= finishDistance) - break; - - var targetSpeed = baseSpeed; - var slowRatio = 1f; - if (remain < slowDistance) - { - slowRatio = (float)Math.Pow(Clamp(Math.Max(0, remain) / slowDistance, 0f, 1f), slowingPow); - targetSpeed = slowRatio * (baseSpeed - finishSpeed) + finishSpeed; - } - var now = DateTime.Now; - var dt = Math.Max(0.001f, (float)(now - lastTick).TotalSeconds); - lastTick = now; - var rampElapsed = (now - speedRampStart).TotalSeconds; - var activeAccel = rampElapsed < 5.0 ? startAccel : accel; - var speed = activeAccel > 0 ? Slew(cmdSpeed, targetSpeed, activeAccel * dt) : targetSpeed; - cmdSpeed = speed; - - var baseCrabTh = (float)CommonMath.ThDiff(phi, cth); - var headingErr = (float)CommonMath.ThDiff(targetBodyTh, cth); - var headingErrReverse = (float)CommonMath.ThDiff(cth, targetBodyTh); - var targetBodyToPath = (float)CommonMath.ThDiff(phi, targetBodyTh); - var rawBiasMagnitude = (float)(Math.Atan(conf.BiasFac * Math.Abs(lateral) / 1000f / - Math.Max(speed, 0.3f)) / Math.PI * 180.0); - var biasSign = GetLateralBiasSign(baseCrabTh, cth, speed, gcpLimit, pathLeft, lateral); - var rawBiasItem = rawBiasMagnitude * biasSign; - var biasItem = ClampAbs(rawBiasItem, conf.BiasThreshold); - var yawSplitSign = GetYawSplitSign(baseCrabTh + biasItem, speed, gcpLimit, controlRadius); - var rawDthItem = conf.FleetCrabDthLinearFac * headingErr * yawSplitSign; - var dthItem = ClampAbs(rawDthItem, conf.FleetCrabDthLinearThreshold); - var rawFrontTh = baseCrabTh + biasItem + dthItem; - var rawRearTh = baseCrabTh + biasItem - dthItem; - ResolveCrabDriveEquivalent(speed, rawFrontTh, rawRearTh, gcpLimit, out var driveSpeed, - out var frontTh, out var rearTh, out var reverseEquivalent, out var rawBaseTh); - holdFrontTh = frontTh; - holdRearTh = rearTh; - var idealAlong = Clamp(along, 0f, pathLengthMm); - var ideal = pathStart + pathDir * idealAlong; - - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleAutoEnabled = true; - self.MultiVehicleAutoVx = driveSpeed; - self.MultiVehicleAutoFrontTh = frontTh; - self.MultiVehicleAutoRearTh = rearTh; - self.MultiVehicleAutoIdealX = ideal.X; - self.MultiVehicleAutoIdealY = ideal.Y; - self.MultiVehicleAutoIdealTh = targetBodyTh; - self.MultiVehicleAutoHasIdeal = true; - self.MultiVehicleAutoCmdTime = DateTime.Now; - - if ((DateTime.Now - lastLog).TotalMilliseconds >= 300) - { - lastLog = DateTime.Now; - var snap = self.GetFleetCenterSnapshot(); - int fleetCnt; - lock (self.FleetLock) fleetCnt = self.MultiVehicleFleet.Count; - DLog.Log( - $"ITER#{iter} centerSrc={centerSource} center=({cx:0},{cy:0},{cth:0.0}) snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " + - $"along={along:0} lateral={lateral:0} remain={remain:0} headingErr={headingErr:0.0} " + - $"baseTh={baseCrabTh:0.0} bias={biasItem:0.0} dth={dthItem:0.0} " + - $"slowRatio={slowRatio:0.000} targetV={targetSpeed:0.000} rampT={rampElapsed:0.0} accel={activeAccel:0.000} auto=(vx:{driveSpeed:0.000},fTh:{frontTh:0.0},rTh:{rearTh:0.0}) " + - $"ideal=({ideal.X:0},{ideal.Y:0},{targetBodyTh:0.0}) scriptEn={self.MultiVehicleScriptEnabled} " + - $"cnt={fleetCnt}/{conf.MultiVehicleFleetNum}", - "FleetCrabDbg"); - DLog.Log( - $"CTRL iter={iter} centerSrc:{centerSource} phi:{phi:0.00} targetBody:{targetBodyTh:0.00} startTheta:{theta:0.00} " + - $"cth:{cth:0.00} crabAngle:{CrabAngleDeg:0.00} bodyToPathCfg:{BodyToPathAngleDeg:0.00} " + - $"targetBodyToPath:{targetBodyToPath:0.00} bodyToPathNow:{baseCrabTh:0.00} " + - $"headingErr(target-current):{headingErr:0.00} reverse(current-target):{headingErrReverse:0.00} yawSign:{yawSplitSign:0} " + - $"fleetCrabDthFac:{conf.FleetCrabDthLinearFac:0.000} rawDth:{rawDthItem:0.00} dth:{dthItem:0.00} dthLimit:{conf.FleetCrabDthLinearThreshold:0.00} " + - $"lateral:{lateral:0.0} biasFac:{conf.BiasFac:0.000} biasSign:{biasSign:0} rawBias:{rawBiasItem:0.00} bias:{biasItem:0.00} biasLimit:{conf.BiasThreshold:0.00} " + - $"baseTh:{baseCrabTh:0.00} rawBase:{rawBaseTh:0.00} rawOut(f:{rawFrontTh:0.00},r:{rawRearTh:0.00}) " + - $"out(f:{frontTh:0.00},r:{rearTh:0.00}) gcpLimit:{gcpLimit:0.00} revEq:{reverseEquivalent} " + - $"speedRaw:{speed:0.000} speed:{driveSpeed:0.000} rampT:{rampElapsed:0.0} accel:{activeAccel:0.000} along:{along:0.0} remain:{remain:0.0} ideal=({ideal.X:0.0},{ideal.Y:0.0},{targetBodyTh:0.00})", - "FleetCrabHeadingDbg"); - } - yield return true; - } - - if (_stopping) - stopReason = "stop"; - - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = holdFrontTh; - self.MultiVehicleAutoRearTh = holdRearTh; - self.MultiVehicleAutoCmdTime = DateTime.Now; - DLog.Log( - $"STOP_HOLD iter={iter} reason={stopReason} hold=(fTh:{holdFrontTh:0.0},rTh:{holdRearTh:0.0}) cmdSpeed={cmdSpeed:0.000}", - "FleetCrabDbg"); - var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3)); - while (!_stopping && DateTime.Now < settleEnd) - { - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleAutoEnabled = true; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = holdFrontTh; - self.MultiVehicleAutoRearTh = holdRearTh; - self.MultiVehicleAutoCmdTime = DateTime.Now; - yield return true; - } - - Cleanup(); - Hedingben.ToastText("车队蟹行完成", "FleetCrab"); - DLog.Log($"DONE iter={iter} reason={stopReason}", "FleetCrabDbg"); - } -} diff --git a/.task8-sweep/FleetCurveWalk.cs b/.task8-sweep/FleetCurveWalk.cs deleted file mode 100644 index c5b2e2e..0000000 --- a/.task8-sweep/FleetCurveWalk.cs +++ /dev/null @@ -1,411 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Numerics; -using ClumsyCore; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using FundamentalLib; -using CommonUsage.Chassis; -using CommonUsage.Mathematics; -using MDCSToolBox.Clumsy.MotionControllers; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; -using MDCSToolBox.Clumsy.Tracks; - -namespace MultiWheelC; - -public class FleetCurveWalk : MovementDefinition -{ - public BezierTrack Track; - public List ControlPoints = new(); - public float CurveSpeed = 0.2f; - public float CarDirectionBias = 0f; - public int BezierResolution = 100; - public float SlowDistance = 2000f; - public float FinishDistance = 20f; - public float FinishSpeed = 0.02f; - public float SlowingPow = 0.8f; - public float GcpThetaThreshold = 95f; - public float StartSyncTimeoutSec = 8f; - - private bool _stopping; - private MultiWheelGeometricController _controller; - private MultiWheelChassis _chassis; - private bool _savedControlPoints; - private float _savedControlRadius; - private Vector2 _savedGcp0; - private Vector2 _savedGcp1; - - public void Stop() - { - _stopping = true; - if (_controller != null) - _controller.BreakAndHold = true; - Cleanup(); - } - - public static bool TryParsePointList(string text, out List points, out string error) - { - points = new List(); - error = ""; - if (string.IsNullOrWhiteSpace(text)) - { - error = "empty control point list"; - return false; - } - - var segments = text.Split(new[] { ';', '|' }, StringSplitOptions.RemoveEmptyEntries); - for (var i = 0; i < segments.Length; i++) - { - var pair = segments[i].Split(new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); - if (pair.Length != 2) - { - error = $"invalid point #{i + 1}: {segments[i]}"; - return false; - } - - if (!TryParseFloat(pair[0], out var x) || !TryParseFloat(pair[1], out var y)) - { - error = $"invalid number in point #{i + 1}: {segments[i]}"; - return false; - } - points.Add(new Vector2(x, y)); - } - - if (points.Count < 3) - { - error = "Bezier curve requires at least 3 control points"; - return false; - } - - return true; - } - - public static List BuildRelativeControlPoints(Vector2 start, float startTh, List relativePoints) - { - var source = relativePoints ?? new List(); - var normalized = new List(); - if (source.Count == 0 || Vector2.Distance(source[0], Vector2.Zero) > 1f) - normalized.Add(Vector2.Zero); - for (var i = 0; i < source.Count; i++) - normalized.Add(source[i]); - if (normalized.Count < 2) - normalized.Add(new Vector2(1000f, 0f)); - if (normalized.Count < 3) - normalized.Add(new Vector2(2000f, 0f)); - - var result = new List(); - for (var i = 0; i < normalized.Count; i++) - result.Add(CommonMath.Transform2D(start, startTh, normalized[i])); - return result; - } - - public static List BuildAgvControlPoints(float srcX, float srcY, float dstX, float dstY, - params float[] controlPointCoords) - { - var src = new Vector2(srcX, srcY); - var dst = new Vector2(dstX, dstY); - var result = new List(); - if (controlPointCoords == null || controlPointCoords.Length == 0) - { - result.Add(src); - result.Add((src + dst) / 2f); - result.Add(dst); - return result; - } - - if (controlPointCoords.Length % 2 != 0) - throw new ArgumentException("FleetCurve controlPointCoords must contain x,y pairs."); - - var supplied = new List(); - for (var i = 0; i < controlPointCoords.Length; i += 2) - supplied.Add(new Vector2(controlPointCoords[i], controlPointCoords[i + 1])); - - if (supplied.Count >= 3 && - Vector2.Distance(supplied[0], src) <= 10f && - Vector2.Distance(supplied[supplied.Count - 1], dst) <= 10f) - return supplied; - - result.Add(src); - for (var i = 0; i < supplied.Count; i++) - result.Add(supplied[i]); - result.Add(dst); - if (result.Count < 3) - result.Insert(1, (src + dst) / 2f); - return result; - } - - private static bool TryParseFloat(string text, out float value) - { - return float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value) || - float.TryParse(text, out value); - } - - private static float ClampAbs(float value, float limit) - { - var absLimit = Math.Abs(limit); - if (absLimit <= 0) return value; - if (value > absLimit) return absLimit; - if (value < -absLimit) return -absLimit; - return value; - } - - private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY, - out float centerTh, out string source) - { - if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh)) - { - source = "fleet"; - return true; - } - - if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh)) - { - source = "slam"; - return true; - } - - source = "none"; - return false; - } - - private void Cleanup() - { - var self = PilotDefinition.Self; - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = 0; - self.MultiVehicleAutoRearTh = 0; - self.MultiVehicleAutoHasIdeal = false; - self.MultiVehicleAutoEnabled = false; - RestoreControlPointRadius(); - } - - private void ApplyFleetControlPointRadius(MultiWheelChassis chassis, float radius) - { - if (!_savedControlPoints) - { - _chassis = chassis; - _savedControlRadius = chassis.ControlPointRadius; - var gcps = chassis.GetGeometricControlPoints(); - if (gcps.Count >= 2) - { - _savedGcp0 = gcps[0].Position; - _savedGcp1 = gcps[1].Position; - } - _savedControlPoints = true; - } - - chassis.ControlPointRadius = radius; - var points = chassis.GetGeometricControlPoints(); - if (points.Count >= 2) - { - points[0].Position = new Vector2(radius, 0); - points[1].Position = new Vector2(-radius, 0); - } - } - - private void RestoreControlPointRadius() - { - if (!_savedControlPoints || _chassis == null) - return; - - _chassis.ControlPointRadius = _savedControlRadius; - var points = _chassis.GetGeometricControlPoints(); - if (points.Count >= 2) - { - points[0].Position = _savedGcp0; - points[1].Position = _savedGcp1; - } - _savedControlPoints = false; - } - - private static void WriteWarmupAuto(PilotDefinition self, Vector2 idealPos, float idealTh, - float frontTh, float rearTh) - { - self.MultiVehicleScriptEnabled = false; - self.MultiVehicleScriptMode = 0; - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleAutoEnabled = true; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoFrontTh = frontTh; - self.MultiVehicleAutoRearTh = rearTh; - self.MultiVehicleAutoIdealX = idealPos.X; - self.MultiVehicleAutoIdealY = idealPos.Y; - self.MultiVehicleAutoIdealTh = idealTh; - self.MultiVehicleAutoHasIdeal = true; - self.MultiVehicleAutoCmdTime = DateTime.Now; - } - - public override IEnumerable Get() - { - var self = PilotDefinition.Self; - var conf = PilotDefinition.Conf; - var chassis = BasicPilotBase.Chassis as MultiWheelChassis; - _stopping = false; - - if (chassis == null) - { - DLog.Log("ABORT: FleetCurveWalk requires MultiWheelChassis.", "FleetCurveDbg"); - yield break; - } - - if (conf.MultiVehicleMasterEndpoint != "/") - { - DLog.Log($"ABORT: FleetCurveWalk must run on master endpoint, endpoint={conf.MultiVehicleMasterEndpoint}", - "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve requires master vehicle", "FleetCurve"); - yield break; - } - - if (Track == null && (ControlPoints == null || ControlPoints.Count < 3)) - { - DLog.Log("ABORT: FleetCurveWalk requires a BezierTrack or at least 3 control points.", "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve requires track or >=3 control points", "FleetCurve"); - yield break; - } - - if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource)) - { - DLog.Log("ABORT: FleetCurveWalk failed to read fleet center.", "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve requires master localization", "FleetCurve"); - yield break; - } - - var baseSpeed = Math.Abs(CurveSpeed); - if (baseSpeed <= 1e-4f) - { - DLog.Log("ABORT: FleetCurveWalk speed is zero.", "FleetCurveDbg"); - yield break; - } - - var resolution = Math.Max(2, BezierResolution); - var speedFinish = Math.Min(baseSpeed, Math.Abs(FinishSpeed)); - var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold)); - var controlRadius = Math.Max(1f, Math.Abs(conf.TestCarSyncDistance) / 2f); - - ApplyFleetControlPointRadius(chassis, controlRadius); - - try - { - var track = Track; - var trackSource = "external"; - if (track == null) - { - var points = new List(ControlPoints); - track = new BezierTrack(points, resolution); - trackSource = "controlPoints"; - } - track.CarDirectionBias = CarDirectionBias; - track.Speed = baseSpeed; - - var center = new Vector2(x0, y0); - var (idealPos, idealAngle, bias, pd) = track.QueryTangentPoint(center); - var carDirection = (float)CommonMath.ThDiff(theta, CarDirectionBias); - var holdTh = ClampAbs((float)CommonMath.ThDiff(idealAngle, carDirection), gcpLimit); - var targetBodyTh = (float)CommonMath.RoundTh(idealAngle + CarDirectionBias); - - DLog.Log( - $"START center=({x0:0},{y0:0},{theta:0.0}) source={initialCenterSource} " + - $"track={track.GetType().Name} trackSource={trackSource} controls={ControlPoints?.Count ?? 0} " + - $"len={track.Length():0} speed={baseSpeed:0.000} bias={CarDirectionBias:0.0} " + - $"query=({idealPos.X:0},{idealPos.Y:0}) tangent={idealAngle:0.0} targetBody={targetBodyTh:0.0} " + - $"pathBias={bias:0.0} pd={pd:0.0} hold={holdTh:0.0} radius={controlRadius:0}", - "FleetCurveDbg"); - - var warmStart = DateTime.Now; - var warmSeqBaseline = self.BeginFleetMotionWarmup(); - WriteWarmupAuto(self, idealPos, targetBodyTh, holdTh, holdTh); - self.PrimeMasterAutoFromSlam(); - - var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, StartSyncTimeoutSec)); - var warmIter = 0; - var warmReady = false; - var warmDetail = ""; - while (!_stopping && DateTime.Now < warmEnd) - { - warmIter++; - WriteWarmupAuto(self, idealPos, targetBodyTh, holdTh, holdTh); - self.PrimeMasterAutoFromSlam(); - - if (warmIter % 5 == 0) - { - var snap = self.GetFleetCenterSnapshot(); - int cnt; - lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count; - DLog.Log( - $"WARMUP#{warmIter} snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " + - $"cnt={cnt}/{conf.MultiVehicleFleetNum} detail={warmDetail}", - "FleetCurveDbg"); - } - - if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline, - conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail)) - { - warmReady = true; - DLog.Log($"WARMUP done iter={warmIter} detail={warmDetail}", "FleetCurveDbg"); - break; - } - yield return true; - } - - if (!warmReady) - { - DLog.Log($"WARMUP timeout: fleet startup sync failed, abort curve action. detail={warmDetail}", - "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve startup sync timeout", "FleetCurve"); - Cleanup(); - yield break; - } - - _controller = new ChassisController { BaseSpeed = baseSpeed }.Get(); - _controller.MultiVehicleSync = true; - _controller.BaseSpeed = baseSpeed; - _controller.SlowDistance = Math.Max(FinishDistance + 1f, SlowDistance); - _controller.FinishDistance = Math.Max(0f, FinishDistance); - _controller.FinishSpeed = speedFinish; - _controller.SlowingPow = Math.Max(0.01f, SlowingPow); - _controller.GcpThetaThreshold = gcpLimit; - _controller.AddTrack(track, "FleetCurve"); - - Hedingben.ToastText($"FleetCurve len {track.Length():0}mm speed {baseSpeed:0.00}", "FleetCurve"); - - foreach (var running in _controller.Track()) - { - if (_stopping) - break; - if (!running) - break; - yield return true; - } - - if (!_stopping) - { - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoCmdTime = DateTime.Now; - var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3)); - while (!_stopping && DateTime.Now < settleEnd) - { - self.MultiVehicleAutoEnabled = true; - self.MultiVehicleAutoVx = 0; - self.MultiVehicleAutoCmdTime = DateTime.Now; - yield return true; - } - } - - DLog.Log($"DONE stopping={_stopping}", "FleetCurveDbg"); - } - finally - { - Cleanup(); - _controller = null; - } - } -} diff --git a/.task8-sweep/MovementTests.TireFollowing.cs b/.task8-sweep/MovementTests.TireFollowing.cs deleted file mode 100644 index ff3598e..0000000 --- a/.task8-sweep/MovementTests.TireFollowing.cs +++ /dev/null @@ -1,606 +0,0 @@ -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using ClumsyCore.Utilities; -using ClumsyDance.ClumsyDance.Detectors; -using ClumsyDance.ClumsyWalk.Detectors; -using CommonUsage.Chassis; -using CommonUsage.Mathematics; -using FundamentalLib; -using MDCSToolBox.Clumsy.Calibration; -using MDCSToolBox.Clumsy.Tracks; -using MDCSToolBox.Commons.Controllers; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Numerics; -using System.Security.Cryptography; -using System.Threading; -using LineSegment = ClumsyCore.Utilities.LineSegment; - -namespace MultiWheelC -{ - [MovementTest(name = "轮胎检测")] - public class TireDetect : MovementTest - { - public override void TestStop() - { - _running = false; - } - - public override void Test() - { - _painter = UI.GetPainter("TwoLegDetectTest", false); - _painter.Clear(); - - var frontlidar = UI.GetInput("1是用前雷达识别,2是用后雷达识别"); - var result = int.Parse(frontlidar.ToString()); - var lastDetectX = result == 1 ? PilotDefinition.Conf.TireFollowingStage1GuessX : -PilotDefinition.Conf.TireFollowingStage1GuessX; - var lastDetectY = 0f; - while (_running) - { - var ld = Detect(lastDetectX, SetFilters(lastDetectX, lastDetectY), result == 1 ? true : false); - if(ld == null) - { - //Console.WriteLine("ld == null"); - continue; - } - _painter.Clear(); - var center = (ld.Src + ld.Dst) / 2; - var distanceToCarOrigin = Vector2.Distance(Vector2.Zero, center); - var distanceLabelPos = center / 2; - _painter.DrawLine(Color.Cyan, Vector2.Zero, center, width: 2); - _painter.DrawText(Color.Yellow, $"{distanceToCarOrigin:F3}", distanceLabelPos.X, distanceLabelPos.Y); - lastDetectX = center.X; - lastDetectY = center.Y; - Thread.Sleep(100); - } - } - - public static LineSegment Detect(float guessX, List filters, bool frontlidar) - { - return new Lidar2dDetect2LegTray() - { - BlobDist = frontlidar ? PilotDefinition.Conf.TireFrontTwoLegBlobDist : PilotDefinition.Conf.TireBackTwoLegBlobDist, - BlobPtCount = frontlidar ? PilotDefinition.Conf.TireTwoLegBlobPtCount : PilotDefinition.Conf.TireTwoLegBlobPtCount, - BlobSize = frontlidar ? PilotDefinition.Conf.TireFrontTwoLegBlobSize : PilotDefinition.Conf.TireBackTwoLegBlobSize, - CenterChange = Tuple.Create(frontlidar ? PilotDefinition.Conf.TireFrontTwoLegCenterChangeX : PilotDefinition.Conf.TireBackTwoLegCenterChangeX, 0f, 0f), - LegWidth = PilotDefinition.Conf.TireTwoLegWidth, - LegWidthErr = frontlidar ? PilotDefinition.Conf.TireTwoLegWidthErr : PilotDefinition.Conf.TireTwoLegWidthErr, - Padding = frontlidar ? PilotDefinition.Conf.TireFrontPadding : PilotDefinition.Conf.TireBackPadding, - PillarFindingScope = frontlidar ? PilotDefinition.Conf.TireFrontTwoLegPillarFindingScope : PilotDefinition.Conf.TireBackTwoLegPillarFindingScope, - SgnDir = PilotDefinition.Conf.TwoLegSgnDir, - }.DetectWithGuess(frontlidar ? "frontlidar" : "leftlidar,rightlidar", new LineSegment(new Vector2(guessX, 0), Vector2.Zero), - guessCoordinateSystem: CoordinateSystem.Car2D, outCoordinateSystem: CoordinateSystem.Car2D, filters); - } - - private List SetFilters(float guessCenterX, float guessCenterY) - { - var painter = UI.GetPainter("GeneralFollowing.SetFilters", false); - painter.Clear(); - painter.Clear(3000); - - var box = new Vector2[] - { - new (guessCenterX - PilotDefinition.Conf.TireFilterLength / 2, guessCenterY - PilotDefinition.Conf.TireFilterWidth / 2), - new (guessCenterX + PilotDefinition.Conf.TireFilterLength / 2, guessCenterY - PilotDefinition.Conf.TireFilterWidth / 2), - new (guessCenterX + PilotDefinition.Conf.TireFilterLength / 2, guessCenterY + PilotDefinition.Conf.TireFilterWidth / 2), - new (guessCenterX - PilotDefinition.Conf.TireFilterLength / 2, guessCenterY + PilotDefinition.Conf.TireFilterWidth / 2), - }; - - for (var i = 0; i < box.Length; ++i) - painter.DrawLine(Color.DarkOliveGreen, box[i], box[(i + 1) % 4]); - - // PC filter in car coordinate frame - return new List() - { - new(CoordinateSystem.Car2D, - p => LessMath.IsPointInPolygon4( - box.Select(v => new PointF(v.X, v.Y)).ToArray(), new PointF(p.X, p.Y))), - }; - } - - private Painter _painter; - private bool _running = true; - } - - [MovementTest(name = "钻车测试")] - public class FollowTire : MovementTest - { - public override void TestStop() - { - _dt?.Stop(); - } - - public override void Test() - { - var front = UI.GetInput("1是用前雷达识别,2是用后雷达识别"); - var result = int.Parse(front.ToString()); - var lidarname = result == 1 ? "前雷达" : "后雷达"; - DLog.Log($"开始钻车测试,用{lidarname}识别", "TireFollowing"); - - var following = new TireFollowing() - { - GetController = () => new ChassisController().Get(), - GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, - GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, - detectors = new List() - { - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, result == 1 ? true : false), - StartGuessingX = result == 1 ? PilotDefinition.Conf.TireFollowingStage1GuessX : -PilotDefinition.Conf.TireFollowingStage1GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, - result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0) - }, - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, result == 1 ? true : false), - StartGuessingX = result == 1 ? PilotDefinition.Conf.TireFollowingStage2GuessX : -PilotDefinition.Conf.TireFollowingStage2GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, - result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0) - }, - }, - CarDirection = result == 1 ? 0f : 180f, - SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, - MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, - TireNum = PilotDefinition.Conf.TireFollowingTireNum, - WalkBlindTh = result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarWalkBlindTh : PilotDefinition.Conf.TireFollowingBackLidarWalkBlindTh, - }; - _dt = new DriveTask(following.Get()); - _dt.Wait(); - DLog.Log($"结束钻车测试", "TireFollowing"); - } - private DriveTask _dt; - } - - [MovementTest(name = "离车测试")] - public class LeaveCar : MovementTest - { - public override void TestStop() - { - _dt?.Stop(); - } - - public override void Test() - { - DLog.Log($"开始离车测试,用后雷达识别", "TireFollowing"); - var following = new TireFollowing() - { - GetController = () => new ChassisController().Get(), - GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, - GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, - detectors = new List() - { - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, false), - StartGuessingX = -PilotDefinition.Conf.TireFollowingStage2GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingLeaveCarWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - PilotDefinition.Conf.TireFollowingLeaveCarBackLidarPathTransformationX, - PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0) - }, - }, - CarDirection = 180f, - SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, - MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, - WalkBlindTh = 0, - TireNum = 1 - }; - _dt = new DriveTask(following.Get()); - _dt.Wait(); - DLog.Log($"结束离车测试", "TireFollowing"); - } - private DriveTask _dt; - } - - [MovementTest(name = "抱夹关闭")] - public class ClampTest1 : MovementTest - { - public override void TestStop() - { - _dt?.Stop(); - PilotDefinition.Self.SpeedLeftArm = 0; - PilotDefinition.Self.SpeedRightArm = 0; - } - - public override void Test() - { - _dt = new DriveTask(new ClampToTarget() - { - LeftClampTarget = PilotDefinition.Self.LeftArmUpperPos, - RightClampTarget = PilotDefinition.Self.RightArmUpperPos - }.Get()); - _dt.Wait(); - } - - private DriveTask _dt; - } - - [MovementTest(name = "抱夹打开")] - public class ClampTest2 : MovementTest - { - public override void TestStop() - { - _dt?.Stop(); - PilotDefinition.Self.SpeedLeftArm = 0; - PilotDefinition.Self.SpeedRightArm = 0; - } - - public override void Test() - { - _dt = new DriveTask(new ClampToTarget() - { - LeftClampTarget = PilotDefinition.Self.LeftArmLowerPos, - RightClampTarget = PilotDefinition.Self.RightArmLowerPos - }.Get()); - _dt.Wait(); - } - - private DriveTask _dt; - } - - [MovementTest(name = "测试前进基于轮里程")] - public class LineTrackingTest : MovementTest - { - public override void TestStop() - { - _dt?.Stop(); - } - public override void Test() - { - _dt = new DriveTask(new LineTracking() - { - Target = PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, - }.Get()); - _dt.Wait(); - } - private DriveTask _dt; - } - - [MovementTest(name = "测试后退基于轮里程")] - public class ReverseLineTrackingTest : MovementTest - { - public override void TestStop() - { - _dt?.Stop(); - } - public override void Test() - { - _dt = new DriveTask(new LineTracking() - { - Target = -PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, - }.Get()); - _dt.Wait(); - } - private DriveTask _dt; - } - - [MovementTest(name = "测试终点跟踪动作-前进")] - public class DstTrackerForward : MovementTest - { - public bool UseInteractivePick = true; - public float srcX; - public float srcY; - public float dstX; - public float dstY; - public float carDirectionBias = 0f; - private readonly Painter _painter = UI.GetPainter("DstTrackerTest"); - - public override void TestStop() - { - _dt?.Stop(); - _painter?.Clear(); - } - public override void Test() - { - var p1 = UI.GetPoint("point1"); - var p2 = UI.GetPoint("point2"); - - _painter.Clear(); - _dt = new DriveTask(new DstTracker() - { - Src = p1, - Dst = p2, - CarDirectionBias = carDirectionBias, - }.Get()); - _dt.Wait(); - } - - private DriveTask _dt; - } - - [MovementTest(name = "测试终点跟踪动作-后退")] - public class DstTrackerhoutui : MovementTest - { - public bool UseInteractivePick = true; - public float srcX; - public float srcY; - public float dstX; - public float dstY; - public float carDirectionBias = 180f; - private readonly Painter _painter = UI.GetPainter("DstTrackerTest"); - - public override void TestStop() - { - _dt?.Stop(); - _painter?.Clear(); - } - public override void Test() - { - var p1 = UI.GetPoint("point1"); - var p2 = UI.GetPoint("point2"); - - _painter.Clear(); - _dt = new DriveTask(new DstTracker() - { - Src = p1, - Dst = p2, - CarDirectionBias = carDirectionBias, - }.Get()); - _dt.Wait(); - } - - private DriveTask _dt; - } - - [MovementTest(name = "测试先直行再终点跟踪")] - public class LineTrackThenDstTrackerTest : MovementTest - { - public float carDirectionBias = 0f; - private readonly Painter _painter = UI.GetPainter("LineTrackThenDstTrackerTest"); - public override void TestStop() - { - _dt?.Stop(); - _painter?.Clear(); - } - public override void Test() - { - var src = UI.GetPoint("请在上位机选择起点(src)"); - var dst = UI.GetPoint("请在上位机选择终点(dst)"); - _painter.Clear(); - _painter.DrawLine(Color.Cyan, src.X, src.Y, dst.X, dst.Y, width: 3); - _painter.DrawCircle(Color.LimeGreen, src.X, src.Y, 80f); - _painter.DrawCircle(Color.OrangeRed, dst.X, dst.Y, 80f); - _painter.DrawText(Color.LimeGreen, "src", src.X + 80f, src.Y + 80f); - _painter.DrawText(Color.OrangeRed, "dst", dst.X + 80f, dst.Y + 80f); - IEnumerable TrackThenFollow() - { - foreach (var running in new LineTracking() - { - Target = PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, - EnableHandover = true, - HandoverDistance = 200f, - HandoverSpeed = 0.3f, - }.Get()) - { - if (!running) break; - yield return true; - } - foreach (var running in new DstTracker() - { - Src = src, - Dst = dst, - CarDirectionBias = carDirectionBias, - InitialSendSpeed = 0.3f - }.Get()) - { - if (!running) break; - yield return true; - } - yield return false; - } - _dt = new DriveTask(TrackThenFollow()); - _dt.Wait(); - } - private DriveTask _dt; - } - - [MovementTest(name = "测试先离车再终点跟踪")] - public class LeaveCarThenDstTrackerTest : MovementTest - { - private readonly Painter _painter = UI.GetPainter("LeaveCarThenDstTrackerTest"); - - public override void TestStop() - { - _dt?.Stop(); - _painter?.Clear(); - } - - public override void Test() - { - var src = UI.GetPoint("请在上位机选择离车后起点(src)"); - var dst = UI.GetPoint("请在上位机选择终点(dst)"); - - _painter.Clear(); - _painter.DrawLine(Color.Cyan, src.X, src.Y, dst.X, dst.Y, width: 3); - _painter.DrawCircle(Color.LimeGreen, src.X, src.Y, 80f); - _painter.DrawCircle(Color.OrangeRed, dst.X, dst.Y, 80f); - _painter.DrawText(Color.LimeGreen, "src", src.X + 80f, src.Y + 80f); - _painter.DrawText(Color.OrangeRed, "dst", dst.X + 80f, dst.Y + 80f); - - IEnumerable LeaveThenFollow() - { - var following = new TireFollowing() - { - GetController = () => new ChassisController().Get(), - GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, - GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, - detectors = new List() - { - new TireFollowing.DetectorDefinition() - { - DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, false), - StartGuessingX = -PilotDefinition.Conf.TireFollowingStage2GuessX, - StartGuessingY = 0, - SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, - FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, - PathTransformation = new Tuple( - PilotDefinition.Conf.TireFollowingLeaveCarBackLidarPathTransformationX, - PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, - 0), - }, - }, - CarDirection = 180f, - SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, - MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, - WalkBlindTh = 0, - TireNum = 1 - }; - - foreach (var running in following.Get()) - { - if (!running) break; - yield return true; - } - - foreach (var running in new DstTracker() - { - Src = src, - Dst = dst, - CarDirectionBias = 180f, - }.Get()) - { - if (!running) break; - yield return true; - } - - yield return false; - } - - _dt = new DriveTask(LeaveThenFollow()); - _dt.Wait(); - } - - private DriveTask _dt; - } - - - [MovementTest(name = "驱动器下使能测试")] - public class DriverDisableTest : MovementTest - { - public override void TestStop() - { - throw new NotImplementedException(); - } - - public override void Test() - { - new DriveTask(new DriverDisable(){ }.Get()).Wait(); - } - } - - [MovementTest(name = "驱动器复位测试")] - public class DriverAbleTest : MovementTest - { - public override void TestStop() - { - throw new NotImplementedException(); - } - - public override void Test() - { - new DriveTask(new DriverAble(){ }.Get()).Wait(); - } - } - - [MovementTest(name = "底盘旋转测试")] - public class RotateToAngleTest : MovementTest - { - public override void TestStop() - { - throw new NotImplementedException(); - } - - public override void Test() - { - var target = UI.GetInput("输入旋转角度:"); - var chassis = (MultiWheelChassis)PilotDefinition.Chassis; - new DriveTask(new MultiWheelRotateInPlace() - { - AngleTarget = float.Parse(target), - PidparamsRead = () => new PIDParams() - { - Kp = PilotDefinition.Conf.TireFollowingThkp, - Ki = PilotDefinition.Conf.TireFollowingThki, - Kd = PilotDefinition.Conf.TireFollowingThkd, - DeadZone = PilotDefinition.Conf.TireFollowingThDeadZone, - SpeedAccPerSec = PilotDefinition.Conf.TireFollowingThSpeedAccPerSec, - OutputUpperThreshold = PilotDefinition.Conf.TireFollowingThThresh, - MaxI = PilotDefinition.Conf.TireFollowingThMaxI, - } - }.Get()).Wait(); - } - } - - public class utils - { - public static List<(float x, float y, float th)> RemoveOutliers(List<(float x, float y, float th)> data, float threshold = 2.0f) - { - var means = CalculateMean(data); - var stdDevs = CalculateStandardDeviation(data, means); - - return data.Where(point => - Math.Abs(point.x - means.x) <= threshold * stdDevs.x && - Math.Abs(point.y - means.y) <= threshold * stdDevs.y && - AngularDistance(point.th, means.th) <= threshold * stdDevs.th - ).ToList(); - } - - public static (float x, float y, float th) CalculateMean(List<(float x, float y, float th)> data) - { - float meanX = data.Average(point => point.x); - float meanY = data.Average(point => point.y); - float sinSum = data.Sum(point => (float)Math.Sin(DegreeToRadian(point.th))); - float cosSum = data.Sum(point => (float)Math.Cos(DegreeToRadian(point.th))); - float meanTh = RadianToDegree((float)Math.Atan2(sinSum, cosSum)); - - return (meanX, meanY, meanTh); - } - - public static (float x, float y, float th) CalculateStandardDeviation(List<(float x, float y, float th)> data, (float x, float y, float th) means) - { - float varianceX = data.Average(point => (point.x - means.x) * (point.x - means.x)); - float varianceY = data.Average(point => (point.y - means.y) * (point.y - means.y)); - - // 计算角度的方差 - float varianceTh = data.Average(point => AngularDistance(point.th, means.th) * AngularDistance(point.th, means.th)); - - return ((float)Math.Sqrt(varianceX), (float)Math.Sqrt(varianceY), (float)Math.Sqrt(varianceTh)); - } - - public static float DegreeToRadian(float degree) - { - return (float)(degree * Math.PI / 180.0); - } - - public static float RadianToDegree(float radian) - { - return (float)(radian * 180.0 / Math.PI); - } - - public static float AngularDistance(float angle1, float angle2) - { - return CommonMath.ThDiff(angle1, angle2); - } - } - -} - - diff --git a/.task8-sweep/MovementTests.TwoLegDetect.cs b/.task8-sweep/MovementTests.TwoLegDetect.cs deleted file mode 100644 index c0a4f57..0000000 --- a/.task8-sweep/MovementTests.TwoLegDetect.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Numerics; -using System.Threading; -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Pilot; -using ClumsyCore.Utilities; -using ClumsyDance.ClumsyDance.Detectors; -using ClumsyDance.ClumsyWalk.Detectors; -using FundamentalLib; -using LineSegment = ClumsyCore.Utilities.LineSegment; - -namespace MultiWheelC; - -/// -/// 2腿检测:用单线激光雷达识别两腿托盘/轮胎,按上一帧结果作为下一帧猜测做闭环检测。 -/// 从 StandardMultiWheelLifter 移植;参数全部走 PilotConfig(Fields 面板),雷达选择改为配置项而非阻塞输入。 -/// -[MovementTest(name = "多舵轮-2腿检测")] -public class TwoLegDetect : MovementTest -{ - private Painter _painter; - private bool _running = true; - - public override void TestStop() => _running = false; - - public override void Test() - { - _running = true; - _painter = UI.GetPainter("MultiWheelTwoLegDetect", false); - _painter.Clear(); - - var lidar = PilotDefinition.Conf.TwoLegLidarName; - var lastDetectX = PilotDefinition.Conf.TwoLegGuessX; - var lastDetectY = 0f; - - while (_running) - { - var ld = Detect(lidar, lastDetectX, SetFilters(lastDetectX, lastDetectY)); - if (ld == null) - { - Thread.Sleep(100); - continue; - } - - var center = (ld.Src + ld.Dst) / 2; - var distanceToCarOrigin = Vector2.Distance(Vector2.Zero, center); - - _painter.Clear(); - _painter.DrawLine(Color.Cyan, Vector2.Zero, center, width: 2); - _painter.DrawText(Color.Yellow, $"{distanceToCarOrigin:F1}", center.X / 2, center.Y / 2); - - Hedingben.ToastText( - $"[Test检测] lidar:{lidar} guess x:{lastDetectX:F0} y:{lastDetectY:F0} | " + - $"中心 x:{center.X:F0} y:{center.Y:F0} dist:{distanceToCarOrigin:F0}", - "MultiWheelTwoLegDetect-test"); - - // 用本帧中心作为下一帧猜测,实现闭环跟踪 - lastDetectX = center.X; - lastDetectY = center.Y; - Thread.Sleep(100); - } - - _painter.Clear(); - } - - /// 在车体坐标系下,按猜测位置检测两腿,返回连接两腿的线段(车体系)。 - public static LineSegment Detect(string lidarName, float guessX, List filters) - { - var conf = PilotDefinition.Conf; -#pragma warning disable CS0612, CS0618 - var detector = new Lidar2dDetect2LegTray - { - BlobDist = conf.TwoLegBlobDist, - BlobPtCount = conf.TwoLegBlobPtCount, - BlobSize = conf.TwoLegBlobSize, - CenterChange = Tuple.Create(conf.TwoLegCenterChangeX, 0f, 0f), - LegWidth = conf.TwoLegWidth, - LegWidthErr = conf.TwoLegWidthErr, - Padding = conf.TwoLegPadding, - PillarFindingScope = conf.TwoLegPillarFindingScope, - SgnDir = conf.TwoLegSgnDir, - }; -#pragma warning restore CS0612, CS0618 - - var result = detector.DetectWithGuess( - lidarName, - new LineSegment(new Vector2(guessX, 0), Vector2.Zero), - guessCoordinateSystem: CoordinateSystem.Car2D, - outCoordinateSystem: CoordinateSystem.Car2D, - filters); - return ApplyOutputBias(result); - } - - private static LineSegment ApplyOutputBias(LineSegment result) - { - if (result == null) return null; - - var conf = PilotDefinition.Conf; - if (Math.Abs(conf.TwoLegOutputBiasX) < 1e-6f && Math.Abs(conf.TwoLegOutputBiasY) < 1e-6f) - return result; - - var bias = new Vector2(conf.TwoLegOutputBiasX, conf.TwoLegOutputBiasY); - return new LineSegment(result.Src + bias, result.Dst + bias); - } - - /// 在猜测中心周围构造一个矩形 ROI,过滤掉框外点云,降低误识别。 - public static List SetFilters(float guessCenterX, float guessCenterY) - { - var conf = PilotDefinition.Conf; - var painter = UI.GetPainter("MultiWheelTwoLegDetect.Filter", false); - painter.Clear(); - - var box = new[] - { - new Vector2(guessCenterX - conf.TwoLegFilterLength / 2, guessCenterY - conf.TwoLegFilterWidth / 2), - new Vector2(guessCenterX + conf.TwoLegFilterLength / 2, guessCenterY - conf.TwoLegFilterWidth / 2), - new Vector2(guessCenterX + conf.TwoLegFilterLength / 2, guessCenterY + conf.TwoLegFilterWidth / 2), - new Vector2(guessCenterX - conf.TwoLegFilterLength / 2, guessCenterY + conf.TwoLegFilterWidth / 2), - }; - - for (var i = 0; i < box.Length; ++i) - painter.DrawLine(Color.DarkOliveGreen, box[i], box[(i + 1) % 4]); - - // 点云滤波在车体坐标系下进行 - return new List - { - new(CoordinateSystem.Car2D, - p => LessMath.IsPointInPolygon4( - box.Select(v => new PointF(v.X, v.Y)).ToArray(), new PointF(p.X, p.Y))), - }; - } -} diff --git a/.task8-sweep/MovementTests.cs b/.task8-sweep/MovementTests.cs deleted file mode 100644 index 3d6322d..0000000 --- a/.task8-sweep/MovementTests.cs +++ /dev/null @@ -1,608 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using ClumsyCore; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using FundamentalLib; -using CommonUsage.Chassis; -using CommonUsage.Mathematics; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; -using MDCSToolBox.Clumsy.Tracks; - -namespace MultiWheelC; - -public class MultiForwardTest : MovementDefinition -{ - public float Speed = 0.2f; - public float DurationSeconds = 2f; - - public override IEnumerable Get() - { - var chassis = (MultiWheelChassis)BasicPilotBase.Chassis; - chassis.SetOriginBias(0, 0, 0); - var end = DateTime.Now.AddSeconds(DurationSeconds); - while (DateTime.Now < end) - { - chassis.SendMotion(Speed, 0, 0); - yield return true; - } - - chassis.SendMotion(0, 0, 0); - } -} - -// 原地旋转到指定世界坐标系朝向:先把舵轮打到旋转所需角度,对齐后再旋转,按目标角度停止(非固定时长)。 -public class MultiRotateToWorldAngle : MovementDefinition -{ - /// 目标朝向(世界坐标系,单位 deg)。 - public float TargetWorldDeg; - - /// 旋转角速度(deg/s,逆时针为正)。 - public float RotSpeed = 30f; - - /// 到位角度精度(deg)。 - public float ArriveDeg = 1f; - - /// 起转前舵轮对齐精度(deg)。 - public float WheelAlignDeg = 2f; - - public override IEnumerable Get() - { - var chassis = (MultiWheelChassis)BasicPilotBase.Chassis; - chassis.SetOriginBias(0, 0, 0); - - // 阶段一:仅把舵轮打到原地旋转所需角度(下发 0 速度,只对齐不旋转)。 - while (true) - { - chassis.SendRotateMotion(0); - if (WheelsAligned(chassis, WheelAlignDeg)) break; - yield return true; - } - - // 阶段二:旋转到目标世界朝向,到位即停。 - var target = CommonMath.RoundTh(TargetWorldDeg); - while (true) - { - var cur = CommonMath.RoundTh((float)DetourInterface.getCartLocation().th); - var diff = CommonMath.ThDiff(target, cur); // 逆时针为正 - if (Math.Abs(diff) <= ArriveDeg) break; - chassis.SendRotateMotion(Math.Sign(diff) * RotSpeed); - yield return true; - } - - chassis.PredefinedDriveStop(); - } - - private static bool WheelsAligned(MultiWheelChassis chassis, float tolDeg) - { -#pragma warning disable CS0612, CS0618 - var wheels = chassis.GetSteerWheels(); -#pragma warning restore CS0612, CS0618 - foreach (var sw in wheels) - if (Math.Abs(CommonMath.ThDiff(sw.ReadAngle(), sw.GetSendAngle())) > tolDeg) - return false; - return true; - } -} - -[MovementTest(name = "多舵轮-前进2秒")] -public class MultiForwardMovementTest : MovementTest -{ - private DriveTask _task; - - public override void Test() - { - _task = new DriveTask(new MultiForwardTest().Get()); - _task.Wait(); - } - - public override void TestStop() => _task?.Stop(); -} - -[MovementTest(name = "多舵轮-原地旋转到目标角度")] -public class MultiRotateMovementTest : MovementTest -{ - private DriveTask _task; - - public override void Test() - { - _task = new DriveTask(new MultiRotateToWorldAngle - { - TargetWorldDeg = PilotDefinition.Conf.InPlaceRotateTargetWorldDeg, - RotSpeed = PilotDefinition.Conf.InPlaceRotateSpeed, - ArriveDeg = PilotDefinition.Conf.InPlaceRotateArriveDeg, - WheelAlignDeg = PilotDefinition.Conf.InPlaceRotateWheelAlignDeg - }.Get()); - _task.Wait(); - } - - public override void TestStop() - { - _task?.Stop(); - ((MultiWheelChassis)BasicPilotBase.Chassis).PredefinedDriveStop(); - } -} - -// ===== 车队联动-原地旋转动作 ===== -// 等价于 FleetRemote 的「原地旋转」模式(已实测可用):FleetRemote 通过 Medulla 手动 IO -// (MultiVehicleManualEnabled + Mode=2 + Vth) 驱动 PilotDefinition.TickMultiVehicle 绕车队中心旋转。 -// 手动 IO 是 [AsLowerIO](Medulla→Clumsy,每周期回写),Clumsy 侧动作直接写会被覆盖; -// 因此本动作改用 Clumsy 内部脚本字段 MultiVehicleScript*(TickMultiVehicle 已将其作为手动等价输入), -// 不写一行底盘指令——实际的 SendRotateMotion + PI 纠偏 + 向从车广播均由 TickMultiVehicle 完成。 -// -// 前提:在「主车」(MultiVehicleMasterEndpoint == "/") 的 Clumsy 上运行,且从车已注册(编队就绪)。 -// 停止条件:主车 SLAM 朝向累计转过 |TargetDeltaDeg|(刚体原地旋转,整车朝向变化量 == 车队转角); -// 无定位时退化为按 |TargetDeltaDeg| / |Omega| 估算时长;并带安全超时。 -public class FleetRotateInPlace : MovementDefinition -{ - /// 角速度大小(deg/s);实际方向由 TargetDeltaDeg 的符号决定。 - public float Omega = 15f; - - /// 目标相对转角(deg,带符号,+ 为逆时针)。 - public float TargetDeltaDeg = 90f; - - /// 到位角度精度(deg)。 - public float ArriveDeg = 1.5f; - - /// 减速区宽度(deg):剩余角度小于此值时,角速度按剩余比例线性降到 MinOmega,抑制惯性超调。 - public float SlowDeg = 25f; - - /// 减速区末段最小角速度(deg/s):避免越接近目标越慢、长尾停不下/到不了位。 - public float MinOmega = 3f; - - /// 缓启动角加速度(deg/s²):起步时角速度从 0 按此斜率爬升到巡航值,抑制起步抖动/队形骤偏。仅作用于起步加速,<=0 关闭缓启动(阶跃起步)。 - public float AccelDegPerSec2 = 20f; - - /// - /// 是否用 Detour 主车航向闭环判停(读 getCartLocation().th 累计实际转角,到 |TargetDeltaDeg| 停)。 - /// 与 MultiVehicleSyncUseDetour 解耦:转到指定角度需要角度反馈,故默认 true。 - /// false 时退化为按估算时长开环停止(实际转速≠指令时不精确)。注意 true 时若无有效全局定位, - /// getCartLocation() 会阻塞(与单车 MultiRotateToWorldAngle 行为一致)。 - /// - public bool UseDetourHeading = true; - - // 注:不设超时上限——旋转持续到到位(或无定位时按估算时长结束),或被 Stop()/TestStop() 主动中止。 - - /// 到位后保持脚本使能、角速度归零的安定时长(s),让纠偏把队形稳住再撤离。 - public float SettleSec = 0.5f; - - private void ClearScript() - { - var self = PilotDefinition.Self; - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleScriptEnabled = false; - } - - public void Stop() => ClearScript(); - - public override IEnumerable Get() - { - var self = PilotDefinition.Self; - var conf = PilotDefinition.Conf; - - if (conf.MultiVehicleMasterEndpoint != "/") - { - Hedingben.ToastText("车队原地旋转需在主车(主车端点=\"/\")运行", "FleetRotate"); - yield break; - } - - var dir = Math.Sign(TargetDeltaDeg); - if (dir == 0) dir = 1; - var maxOmega = Math.Abs(Omega); - var minOmega = Math.Min(Math.Abs(MinOmega), maxOmega); // 最小不超过最大 - var slowDeg = Math.Max(1e-3f, SlowDeg); // 减速区宽度 - var accel = AccelDegPerSec2; // 缓启动角加速度,仅作用于起步,<=0 关闭 - var targetMag = Math.Abs(TargetDeltaDeg); - - var hasPos = UseDetourHeading; - var prevTh = hasPos ? (float)DetourInterface.getCartLocation().th : 0f; - var startTh = prevTh; - var accumulated = 0f; // 累计带符号转角(deg) - var start = DateTime.Now; - var lastTime = start; - var lastLog = DateTime.MinValue; - var lastCenterLog = DateTime.MinValue; - var cmdMag = 0f; // 当前实际下发角速度大小(deg/s),缓启动从 0 斜坡爬升 - var centerTracking = false; - float centerStartX = 0, centerStartY = 0, centerStartTh = 0; - float centerLastX = 0, centerLastY = 0, centerLastTh = 0, centerMaxDrift = 0; - // 无定位按时长估算时,补上缓启动斜坡少转的等效时间(≈ maxOmega/(2·accel)),使时长更接近目标角。 - var estDuration = maxOmega > 1e-3 ? targetMag / maxOmega : 0; - if (accel > 1e-3) estDuration += maxOmega / (2 * accel); - - DLog.Log( - $"REQUEST target={TargetDeltaDeg:0.0} dir={dir} omega={maxOmega:0.0} accel={accel:0.0} " + - $"slowDeg={slowDeg:0.0} minOmega={minOmega:0.0} useDetourHeading={hasPos} startTh={startTh:0.00} " + - $"estDuration={estDuration:0.00}s syncUseDetour={conf.MultiVehicleSyncUseDetour}", - "FleetRotateDbg"); - - // 使能脚本驱动的原地旋转(mode2)。TickMultiVehicle 后台循环据此执行旋转并广播给从车。 - // 起步从 0 角速度开始,由缓启动斜坡爬升,避免阶跃下发导致队形骤偏/抖动。 - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptMode = 2; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleScriptEnabled = true; - self.MultiVehicleRotateWheelsReady = false; - self.MultiVehicleRotateFleetReady = false; - - DLog.Log("WAIT_ALIGN fleet rotate wheels", "FleetRotateDbg"); - while (!self.MultiVehicleRotateFleetReady) - { - self.MultiVehicleScriptVx = 0; - self.MultiVehicleScriptVy = 0; - self.MultiVehicleScriptMode = 2; - self.MultiVehicleScriptVth = 0; - self.MultiVehicleScriptEnabled = true; - Hedingben.ToastText("车队原地旋转舵轮预对齐中", "FleetRotate"); - yield return true; - } - - float centerStartCarX = 0, centerStartCarY = 0, centerStartCarTh = 0; - if (hasPos) - { - var startPos = DetourInterface.getCartLocation(); - centerStartCarX = (float)startPos.x; - centerStartCarY = (float)startPos.y; - centerStartCarTh = (float)startPos.th; - prevTh = centerStartCarTh; - startTh = prevTh; - if (self.TryGetFleetCenterFromPose(centerStartCarX, centerStartCarY, centerStartCarTh, - out centerStartX, out centerStartY, out centerStartTh)) - { - centerLastX = centerStartX; - centerLastY = centerStartY; - centerLastTh = centerStartTh; - centerMaxDrift = 0; - centerTracking = true; - } - } - accumulated = 0f; - start = DateTime.Now; - lastTime = start; - lastLog = DateTime.MinValue; - lastCenterLog = DateTime.MinValue; - cmdMag = 0f; - DLog.Log( - $"START target={TargetDeltaDeg:0.0} dir={dir} omega={maxOmega:0.0} startTh={startTh:0.00} " + - $"fleetAligned={self.MultiVehicleRotateFleetReady}", - "FleetRotateDbg"); - if (centerTracking) - { - DLog.Log( - $"START center=({centerStartX:0.0},{centerStartY:0.0},{centerStartTh:0.00}) " + - $"car=({centerStartCarX:0.0},{centerStartCarY:0.0},{centerStartCarTh:0.00}) " + - $"target={TargetDeltaDeg:0.0} omega={maxOmega:0.0}", - "FleetRotateCenterDbg"); - } - - var stopReason = "stop()"; - while (true) - { - var now = DateTime.Now; - var dt = (float)Math.Min(0.2, Math.Max(0, (now - lastTime).TotalSeconds)); - lastTime = now; - var elapsed = (now - start).TotalSeconds; - - float desiredMag; - float curTh = 0f, remaining = 0f, actualRate = 0f; - if (hasPos) - { - var carPos = DetourInterface.getCartLocation(); - curTh = (float)carPos.th; - var step = (float)CommonMath.ThDiff(curTh, prevTh); // 本帧实际转角(逆时针为正) - accumulated += step; - actualRate = dt > 1e-3 ? step / dt : 0f; // 实际角速率(deg/s),用于对比指令 - prevTh = curTh; - - remaining = targetMag - Math.Abs(accumulated); - if (remaining <= ArriveDeg) { stopReason = "arrived"; break; } - - // 减速区:剩余角度 < SlowDeg 时,目标角速度按剩余比例线性降到 MinOmega, - // 使切断指令瞬间残余动量足够小,抑制惯性滑行造成的超调。宽度直观、便于现场调试。 - desiredMag = remaining < slowDeg - ? Math.Max(minOmega, maxOmega * (remaining / slowDeg)) - : maxOmega; - - if (centerTracking && - self.TryGetFleetCenterFromPose((float)carPos.x, (float)carPos.y, (float)carPos.th, - out centerLastX, out centerLastY, out centerLastTh)) - { - var centerDx = centerLastX - centerStartX; - var centerDy = centerLastY - centerStartY; - var centerDrift = (float)Math.Sqrt(centerDx * centerDx + centerDy * centerDy); - centerMaxDrift = Math.Max(centerMaxDrift, centerDrift); - var centerDth = (float)CommonMath.ThDiff(centerLastTh, centerStartTh); - if ((now - lastCenterLog).TotalMilliseconds >= 250) - { - lastCenterLog = now; - DLog.Log( - $"ACTION t={elapsed:0.00}s center=({centerLastX:0.0},{centerLastY:0.0},{centerLastTh:0.00}) " + - $"start=({centerStartX:0.0},{centerStartY:0.0},{centerStartTh:0.00}) " + - $"drift=({centerDx:0.0},{centerDy:0.0}) dist={centerDrift:0.0} max={centerMaxDrift:0.0} dth={centerDth:0.00} " + - $"cmdW={dir * cmdMag:0.000} actualW={actualRate:0.000} acc={accumulated:0.0} remain={remaining:0.0} " + - $"wheelReady={self.MultiVehicleRotateWheelsReady} fleetReady={self.MultiVehicleRotateFleetReady}", - "FleetRotateCenterDbg"); - } - } - } - else - { - // 无定位:按时长估算,无法测角,目标维持巡航速度到估算时长(仅缓启动整形)。 - desiredMag = maxOmega; - if (elapsed >= estDuration) { stopReason = "estDuration"; break; } - } - - // 缓启动:只对“加速(目标>当前)”按角加速度限斜率,让起步平滑爬升; - // “减速(目标<当前)”跟随上面的减速曲线立即下调,保证及时刹车不超调。 - if (accel > 1e-3 && desiredMag > cmdMag) - cmdMag = Math.Min(desiredMag, cmdMag + accel * dt); - else - cmdMag = desiredMag; - - self.MultiVehicleScriptVth = dir * cmdMag; - - // 落盘诊断(节流~150ms):实际航向/累计转角/实际角速率 vs 指令角速率,定位"开环转速不足"。 - if ((now - lastLog).TotalMilliseconds >= 150) - { - lastLog = now; - DLog.Log( - hasPos - ? $"t={elapsed:0.00}s curTh={curTh:0.00} acc={accumulated:0.0} remain={remaining:0.0} " + - $"cmdW={dir * cmdMag:0.0} actualW={actualRate:0.0} (实际/指令={(Math.Abs(cmdMag) > 1e-3 ? actualRate / (dir * cmdMag) : 0):0.00})" - : $"t={elapsed:0.00}s/{estDuration:0.00}s (无航向反馈,开环按时长) cmdW={dir * cmdMag:0.0}", - "FleetRotateDbg"); - } - - Hedingben.ToastText( - hasPos - ? $"车队原地旋转 目标{TargetDeltaDeg:0.0}° 已转{accumulated:0.0}° 余{targetMag - Math.Abs(accumulated):0.0}° ω={cmdMag:0.0}" - : $"车队原地旋转(无定位,按时长) {elapsed:0.0}/{estDuration:0.0}s ω={cmdMag:0.0}", - "FleetRotate"); - - yield return true; - } - - // 到位:角速度先归零,保持脚本使能让 TickMultiVehicle 的 PI 把队形稳住一小段时间再撤离。 - self.MultiVehicleScriptVth = 0; - var settleEnd = DateTime.Now.AddSeconds(Math.Max(0, SettleSec)); - while (DateTime.Now < settleEnd) - yield return true; - - ClearScript(); - DLog.Log( - $"DONE reason={stopReason} 累计转角={accumulated:0.0}° 目标={TargetDeltaDeg:0.0}° " + - $"用时={(DateTime.Now - start).TotalSeconds:0.00}s useDetourHeading={hasPos}", - "FleetRotateDbg"); - if (centerTracking) - { - var centerDx = centerLastX - centerStartX; - var centerDy = centerLastY - centerStartY; - var centerDrift = (float)Math.Sqrt(centerDx * centerDx + centerDy * centerDy); - var centerDth = (float)CommonMath.ThDiff(centerLastTh, centerStartTh); - DLog.Log( - $"DONE reason={stopReason} center=({centerLastX:0.0},{centerLastY:0.0},{centerLastTh:0.00}) " + - $"start=({centerStartX:0.0},{centerStartY:0.0},{centerStartTh:0.00}) " + - $"drift=({centerDx:0.0},{centerDy:0.0}) dist={centerDrift:0.0} max={centerMaxDrift:0.0} dth={centerDth:0.00} " + - $"acc={accumulated:0.0} target={TargetDeltaDeg:0.0}", - "FleetRotateCenterDbg"); - } - Hedingben.ToastText($"车队原地旋转完成({stopReason}) 累计{accumulated:0.0}°", "FleetRotate"); - } -} - -[MovementTest(name = "车队联动-原地旋转")] -public class FleetRotateInPlaceTest : MovementTest -{ - private FleetRotateInPlace _proc; - private DriveTask _task; - - public override void Test() - { - _proc = new FleetRotateInPlace - { - Omega = PilotDefinition.Conf.FleetRotateOmega, - TargetDeltaDeg = PilotDefinition.Conf.FleetRotateTargetDeltaDeg, - ArriveDeg = PilotDefinition.Conf.FleetRotateArriveDeg, - SlowDeg = PilotDefinition.Conf.FleetRotateSlowDeg, - MinOmega = PilotDefinition.Conf.FleetRotateMinOmega, - AccelDegPerSec2 = PilotDefinition.Conf.FleetRotateAccel, - SettleSec = PilotDefinition.Conf.FleetRotateSettleSec, - UseDetourHeading = PilotDefinition.Conf.FleetRotateUseDetourHeading - }; - _task = new DriveTask(_proc.Get()); - _task.Wait(); - } - - public override void TestStop() - { - _proc?.Stop(); - _task?.Stop(); - } -} - -[MovementTest(name = "车队联动-曲线行走")] -public class FleetCurveWalkTest : MovementTest -{ - private FleetCurveWalk _proc; - private DriveTask _task; - - public override void Test() - { - var self = PilotDefinition.Self; - if (!self.TryGetFleetCenterFromMembers(out var x, out var y, out var th) && - !self.TryGetFleetCenterFromSlam(out x, out y, out th)) - { - DLog.Log("FleetCurveWalkTest abort: failed to read fleet center.", "FleetCurveDbg"); - Hedingben.ToastText("FleetCurve requires master localization", "FleetCurve"); - return; - } - - var pointCount = Math.Max(3, PilotDefinition.Conf.FleetCurveTestControlPointCount); - var controlPoints = new List(); - for (var i = 0; i < pointCount; i++) - controlPoints.Add(UI.GetPoint($"FleetCurve point {i + 1}/{pointCount}")); - var fleetCenter = new Vector2(x, y); - if (Vector2.Distance(fleetCenter, controlPoints[0]) > - Vector2.Distance(fleetCenter, controlPoints[controlPoints.Count - 1])) - controlPoints.Reverse(); - var track = new BezierTrack(controlPoints) - { - Speed = PilotDefinition.Conf.FleetCurveSpeed, - CarDirectionBias = 0f - }; - - _proc = new FleetCurveWalk - { - Track = track, - CurveSpeed = PilotDefinition.Conf.FleetCurveSpeed, - CarDirectionBias = 0f, - SlowDistance = PilotDefinition.Conf.FleetCurveSlowDistance, - FinishDistance = PilotDefinition.Conf.FleetCurveFinishDistance, - FinishSpeed = PilotDefinition.Conf.FleetCurveFinishSpeed, - SlowingPow = PilotDefinition.Conf.FleetCurveSlowingPow, - GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold, - StartSyncTimeoutSec = PilotDefinition.Conf.FleetCrabStartSyncTimeoutSec - }; - _task = new DriveTask(_proc.Get()); - _task.Wait(); - } - - public override void TestStop() - { - _proc?.Stop(); - _task?.Stop(); - } -} - -[MovementTest(name = "车队联动-自动蟹行")] -public class FleetCrabWalkTest : MovementTest -{ - private FleetCrabWalk _proc; - private DriveTask _task; - - public override void Test() - { - _proc = new FleetCrabWalk - { - CrabAngleDeg = PilotDefinition.Conf.FleetCrabAngleDeg, - BodyToPathAngleDeg = PilotDefinition.Conf.FleetCrabAngleDeg, - CrabLengthMm = PilotDefinition.Conf.FleetCrabLengthMm, - CrabSpeed = PilotDefinition.Conf.FleetCrabSpeed, - FleetCrabAccel = PilotDefinition.Conf.FleetCrabAccel, - FleetCrabStartAccel = PilotDefinition.Conf.FleetCrabStartAccel, - FleetCrabSlowDistance = PilotDefinition.Conf.FleetCrabSlowDistance, - FleetCrabFinishDistance = PilotDefinition.Conf.FleetCrabFinishDistance, - FleetCrabFinishSpeed = PilotDefinition.Conf.FleetCrabFinishSpeed, - FleetCrabSlowingPow = PilotDefinition.Conf.FleetCrabSlowingPow, - GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold - }; - _task = new DriveTask(_proc.Get()); - _task.Wait(); - } - - public override void TestStop() - { - _proc?.Stop(); - _task?.Stop(); - } -} - -// ===== 调用 Playground WebAPI 瞬移小车(前移 / 左移 / 旋转)===== -// 平移/旋转量在 Fields 面板配置:WebApiTranslateMm(默认100mm)、WebApiRotateDeg(默认5度)。 - -[MovementTest(name = "多舵轮-WebAPI前移")] -public class WebApiForwardMoveTest : MovementTest -{ - public override void Test() - { - var url = PilotDefinition.Conf.PlaygroundWebApiUrl; - var name = PilotDefinition.Conf.PlaygroundRobotName; - var d = PilotDefinition.Conf.WebApiTranslateMm; - - var pose = PlaygroundWebApi.GetPose(url, name); - // 车体系前向 (d, 0) 变换到世界系:车头方向即朝向 yaw - var dst = CommonMath.Transform2D(new Vector2(pose.X, pose.Y), pose.YawDeg, new Vector2(d, 0)); - PlaygroundWebApi.Move(url, name, dst.X, dst.Y, pose.YawDeg); - Hedingben.ToastText($"前移 {d:f0}mm -> ({dst.X:f0},{dst.Y:f0})", "WebApiForward"); - } - - public override void TestStop() - { - } -} - -[MovementTest(name = "多舵轮-WebAPI左移")] -public class WebApiLeftMoveTest : MovementTest -{ - public override void Test() - { - var url = PilotDefinition.Conf.PlaygroundWebApiUrl; - var name = PilotDefinition.Conf.PlaygroundRobotName; - var d = PilotDefinition.Conf.WebApiTranslateMm; - - var pose = PlaygroundWebApi.GetPose(url, name); - // 车体系左向 (0, d) 变换到世界系(车体 +Y 即左侧) - var dst = CommonMath.Transform2D(new Vector2(pose.X, pose.Y), pose.YawDeg, new Vector2(0, d)); - PlaygroundWebApi.Move(url, name, dst.X, dst.Y, pose.YawDeg); - Hedingben.ToastText($"左移 {d:f0}mm -> ({dst.X:f0},{dst.Y:f0})", "WebApiLeft"); - } - - public override void TestStop() - { - } -} - -[MovementTest(name = "多舵轮-WebAPI旋转")] -public class WebApiRotateTest : MovementTest -{ - public override void Test() - { - var url = PilotDefinition.Conf.PlaygroundWebApiUrl; - var name = PilotDefinition.Conf.PlaygroundRobotName; - var deg = PilotDefinition.Conf.WebApiRotateDeg; - - var pose = PlaygroundWebApi.GetPose(url, name); - var ny = pose.YawDeg + deg; // 逆时针为正 - PlaygroundWebApi.Move(url, name, pose.X, pose.Y, ny); - Hedingben.ToastText($"旋转 {deg:f1}° -> {ny:f1}°", "WebApiRotate"); - } - - public override void TestStop() - { - } -} - -[MovementTest(name = "多舵轮-WebAPI恢复运动")] -public class WebApiMotionResumeTest : MovementTest -{ - public override void Test() - { - var url = PilotDefinition.Conf.PlaygroundWebApiUrl; - PlaygroundWebApi.ResumeMotion(url); - Hedingben.ToastText("已恢复车辆运动", "WebApiMotion"); - } - - public override void TestStop() - { - } -} - -[MovementTest(name = "多舵轮-WebAPI暂停运动")] -public class WebApiMotionPauseTest : MovementTest -{ - public override void Test() - { - var url = PilotDefinition.Conf.PlaygroundWebApiUrl; - PlaygroundWebApi.PauseMotion(url); // 默认 zero 模式:反馈归零 - Hedingben.ToastText("已暂停车辆运动 (zero)", "WebApiMotion"); - } - - public override void TestStop() - { - } -} diff --git a/.task8-sweep/Movements.cs b/.task8-sweep/Movements.cs deleted file mode 100644 index 6dd4a7c..0000000 --- a/.task8-sweep/Movements.cs +++ /dev/null @@ -1,326 +0,0 @@ -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using ClumsyCore.Sensors; -using ClumsyCore.Utilities; -using ClumsyDance.ClumsyWalk.Detectors; -using ClumsyDance.Sensors; -using CommonUsage.Chassis; -using FundamentalLib; -using MDCSToolBox.Clumsy.Calibration; -using MDCSToolBox.Clumsy.HighLevelSecurity; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; -using MDCSToolBox.Clumsy.Tracks; -using MDCSToolBox.Commons; -using MDCSToolBox.Commons.Controllers; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Net.Http; -using System.Numerics; -using System.Reflection; -using System.Text; -using System.Threading; -using static ClumsyCore.DTools.Painter; - -namespace MultiWheelC -{ - public class MultiWheelRotateInPlace : MovementDefinition - { - /// - /// 旋转目标角度 - /// - public float AngleTarget; - - public float MaxSpeed; - - public Func ThetaReader = () => (float)DetourInterface.getCartLocation().th; - - public MultiWheelChassis Chassis = (MultiWheelChassis)PilotDefinition.Chassis; - - public Func PidparamsRead = () => new PIDParams() { }; - - public PIDController thPid; - - private static float RangeAngle(float theta) - { - return (float)(theta - Math.Round(theta / 360.0f) * 360); - } - - public override IEnumerable Get() - { - var targetAngle = RangeAngle(AngleTarget); - var p = PidparamsRead(); - thPid = new PIDController(ThetaReader, p.Kp); - thPid.ChangeParameters(p.Kp, p.Ki, p.Kd, p.MaxI, p.DeadZone, p.OutputUpperThreshold, p.SpeedAccPerSec); - - DateTime lastTime = DateTime.Now; - while (true) - { - var s = thPid.GetResponse(targetAngle, true); - Console.WriteLine($"s:{s} AngleTarget:{AngleTarget}"); - Chassis.SendXYThSpeed(0, 0, s); - lastTime = DateTime.Now; - if (thPid.IsArrived()) break; - yield return true; - } - Chassis.SendXYThSpeed(0, 0, 0); - Console.WriteLine($"final rotate to {targetAngle}"); - } - } - - - public class ClampToTarget : MovementDefinition - { - public float LeftClampTarget; - public float RightClampTarget; - - public float MaxClampSpeed = PilotDefinition.Conf.MaxClampSpeed; - public float ClampKp = PilotDefinition.Conf.ClampControlKp; - public float ClampKi = PilotDefinition.Conf.ClampControlKi; - public float ClampKd = PilotDefinition.Conf.ClampControlKd; - public float ClampMaxI = PilotDefinition.Conf.ClampControlMaxI; - public float ClampSpeedAcc = PilotDefinition.Conf.ClampControlSpeedAcc; - public float ClampDeadZone = PilotDefinition.Conf.ClampControlDeadZone; - private PIDController leftpid, rightpid; - - public override IEnumerable Get() - { - leftpid = new PIDController(() => PilotDefinition.Self.ActualPosLeftArm, ClampKp, ClampKi, ClampKd, - ClampMaxI, ClampDeadZone, MaxClampSpeed) - { SpeedAccPerSec = ClampSpeedAcc }; - - rightpid = new PIDController(() => PilotDefinition.Self.ActualPosRightArm, ClampKp, ClampKi, ClampKd, - ClampMaxI, ClampDeadZone, MaxClampSpeed) - { SpeedAccPerSec = ClampSpeedAcc }; - - while (true) - { - var leftspeed = leftpid.GetResponse(LeftClampTarget); - var rightspeed = rightpid.GetResponse(RightClampTarget); - Console.WriteLine($"left arm speed:{leftspeed} right arm speed:{rightspeed}"); - PilotDefinition.Self.SpeedLeftArm = leftspeed; - PilotDefinition.Self.SpeedRightArm = rightspeed; - if (leftpid.IsArrived()) PilotDefinition.Self.SpeedLeftArm = 0; - if (rightpid.IsArrived()) PilotDefinition.Self.SpeedRightArm = 0; - - if (leftpid.IsArrived() && rightpid.IsArrived()) break; - yield return true; - } - - PilotDefinition.Self.SpeedLeftArm = 0; - PilotDefinition.Self.SpeedRightArm = 0; - Console.WriteLine($"left clamp to target:{LeftClampTarget} right clamp to target:{RightClampTarget}"); - } - } - - public class Sleep : MovementDefinition - { - public float Second = 2; - public override IEnumerable Get() - { - var start = DateTime.Now; - while ((DateTime.Now-start).TotalSeconds LeaveSrcFunction = null; - public Painter painter = UI.GetPainter("Line", false); - public override IEnumerable Get() - { - var curpose = DetourInterface.getCartLocation(); - Console.WriteLine($"curpose.th:{curpose.th}"); - var src = new Vector2((float)curpose.x, (float)curpose.y); - var dst = new Vector2((float)curpose.x + LineDistance * (float)Math.Cos(curpose.th), - (float)curpose.y + LineDistance * (float)Math.Sin(curpose.th)); - Console.WriteLine($"src:{src.X} {src.Y}"); - Console.WriteLine($"dst:{dst.X} {dst.Y}"); - painter.DrawLine(Color.Green, src.X, src.Y, dst.X, dst.Y, width: 3); - - var tracker = new ChassisController().Get(); - var linePath = new LineTrack(src, dst) { CarDirectionBias = LineDistance > 0 ? 0 : 180 }; - tracker.AddTrack(linePath); - var _dt = new DriveTask(tracker.Track()); - _dt.Wait(); - if (SrcId != -1 && LeaveSrcFunction != null) - { - LeaveSrcFunction(SrcId); - DLog.Log($"释放放车点{SrcId}", "TireFollowing"); - } - yield return false; - } - } - - //在世界坐标系下,从路径起点追踪到终点并停车 - public class DstTracker : MovementDefinition - { - public Vector2 Src; - public Vector2 Dst; - public float CarDirectionBias = 0f; - public Painter Painter = UI.GetPainter("DstTracker"); - - public float InitialSendSpeed = 0; - - public override IEnumerable Get() - { - Console.WriteLine($"DstTracker src:({Src.X:F2}, {Src.Y:F2}) dst:({Dst.X:F2}, {Dst.Y:F2})"); - Painter.DrawLine(Color.Cyan, Src.X, Src.Y, Dst.X, Dst.Y, width: 3); - - var tracker = new ChassisController().Get(); - - if (InitialSendSpeed != 0) - { - tracker.SkipInitialRotate = true; - tracker.InitialSendSpeed = InitialSendSpeed; - } - - var linePath = new LineTrack(Src, Dst) { CarDirectionBias = CarDirectionBias, Speed = PilotDefinition.Conf.DstTrackerMaxSpeed }; - tracker.AddTrack(linePath); - var task = new DriveTask(tracker.Track()); - task.Wait(); - - // 到点后兜底停车 - var chassis = (MultiWheelChassis)PilotDefinition.Chassis; - chassis.SendXYThSpeed(0f, 0f, 0f); - yield return false; - } - } - - //直线行走基于轮里程 - public class LineTracking : MovementDefinition - { - public float Target; - public float MaxSpeed = PilotDefinition.Conf.LineTrackMaxSpeed; - public float Kp = PilotDefinition.Conf.LineTrackKp; - public float Ki = PilotDefinition.Conf.LineTrackKi; - public float Kd = PilotDefinition.Conf.LineTrackKd; - public float DeadZone = PilotDefinition.Conf.LineTrackDeadZone; - public int SrcId = -1; - public int DstId = -1; - public Action LeaveSrcFunction = null; - private PIDController pid; - // 末段衔接:接近目标后不再让 PID 把速度降到 0,保留一个接力速度给后续动作接管 - public bool EnableHandover = false; - public float HandoverDistance = 80f; // mm - public float HandoverSpeed = 0.15f; // m/s - - public override IEnumerable Get() - { - - pid = new PIDController(() => - (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, - Kp, Ki, Kd, 0, DeadZone, MaxSpeed) - { SpeedAccPerSec = MaxSpeed / 2f }; - - var chassis = (MultiWheelChassis)PilotDefinition.Chassis; - //chassis.SetOriginBias(0, 0, 0); - DLog.Log($"直线行驶距离:{Target}", "TireFollowing"); - while (true) - { - var current = (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2; - var remain = Target - current; - if (EnableHandover && Math.Abs(remain) <= Math.Max(1f, HandoverDistance)) - { - var handoverSign = Math.Sign(remain); - if (handoverSign == 0) handoverSign = 1; - var handoverSpeed = Math.Abs(HandoverSpeed) * handoverSign; - Console.WriteLine($"handover speed: {handoverSpeed:F3}, remain: {remain:F2}"); - chassis.SendXYThSpeed(handoverSpeed, 0, 0); - // 保留一拍接力速度,让后续 DstTracker 无缝接管 - yield return true; - break; - } - var speed = pid.GetResponse(Target); - Console.WriteLine($"output: {speed} current: {(PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2}"); - - chassis.SendXYThSpeed(speed, 0, 0); - - if (pid.IsArrived()) break; - yield return true; - } - if (SrcId != -1 && LeaveSrcFunction != null) - { - LeaveSrcFunction(SrcId); - DLog.Log($"释放放车点{SrcId}", "TireFollowing"); - } - yield return false; - } - } - - public class DriverAble : MovementDefinition - { - public int WaitTimeoutMs = 2000; - public int PollIntervalMs = 50; - - public override IEnumerable Get() - { - Console.WriteLine("驱动器上使能"); - PilotDefinition.Self.ResetFromC = true; - - var start = DateTime.Now; - var timeoutMs = Math.Max(0, WaitTimeoutMs); - var pollMs = Math.Max(1, PollIntervalMs); - var success = PilotDefinition.Self.WheelAbleState; - while (!success && (DateTime.Now - start).TotalMilliseconds < timeoutMs) - { - Thread.Sleep(pollMs); - success = PilotDefinition.Self.WheelAbleState; - if (!success) yield return true; - } - - PilotDefinition.Self.ResetFromC = false; - if (success) - Console.WriteLine($"驱动器上使能完成,WheelAbleState={PilotDefinition.Self.WheelAbleState}"); - else - Console.WriteLine($"驱动器上使能超时,WheelAbleState={PilotDefinition.Self.WheelAbleState},等待{timeoutMs}ms"); - yield return false; - } - } - - public class DriverDisable : MovementDefinition - { - public int WaitTimeoutMs = 3000; - public int PollIntervalMs = 20; - - public override IEnumerable Get() - { - Console.WriteLine("驱动器下使能"); - PilotDefinition.Self.DisableFromC = true; - - var start = DateTime.Now; - var timeoutMs = Math.Max(0, WaitTimeoutMs); - var pollMs = Math.Max(1, PollIntervalMs); - var success = !PilotDefinition.Self.WheelAbleState; - while (!success && (DateTime.Now - start).TotalMilliseconds < timeoutMs) - { - Thread.Sleep(pollMs); - success = !PilotDefinition.Self.WheelAbleState; - if (!success) yield return true; - } - - PilotDefinition.Self.DisableFromC = false; - if (success) - Console.WriteLine($"驱动器下使能完成,WheelAbleState={PilotDefinition.Self.WheelAbleState}"); - else - Console.WriteLine($"驱动器下使能超时,WheelAbleState={PilotDefinition.Self.WheelAbleState},等待{timeoutMs}ms"); - yield return false; - } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs deleted file mode 100644 index 74b55ef..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 最终粗路径上的一个稠密连续点;长度、净空和位置使用 m,航向使用 rad,曲率使用 1/m。 -public sealed class CoarsePathPoint -{ - /// - /// 创建粗路径点。 - /// 参数:xMeters、yMeters 为世界坐标 m;headingRadians 与 unwrappedHeadingRadians 为航向 rad;arcLengthMeters 和 bodyClearanceMeters 为 m;vehicleCurvaturePerMeter 为 1/m。 - /// - public CoarsePathPoint( - double xMeters, - double yMeters, - double headingRadians, - double unwrappedHeadingRadians, - double arcLengthMeters, - TravelDirection direction, - double vehicleCurvaturePerMeter, - double bodyClearanceMeters, - bool isGearSwitchPoint, - CoarsePathPointSource source) - { - X = xMeters; - Y = yMeters; - Heading = headingRadians; - UnwrappedHeading = unwrappedHeadingRadians; - ArcLength = arcLengthMeters; - Direction = direction; - VehicleCurvature = vehicleCurvaturePerMeter; - BodyClearance = bodyClearanceMeters; - IsGearSwitchPoint = isGearSwitchPoint; - Source = source; - } - - /// 世界 X 坐标,单位 m。 - public double X { get; } - - /// 世界 Y 坐标,单位 m。 - public double Y { get; } - - /// 归一化后可用于几何查询的车头航向,单位 rad。 - public double Heading { get; } - - /// 跨越 ±π 后仍连续的车头航向,单位 rad。 - public double UnwrappedHeading { get; } - - /// 从路径起点累计的弧长,单位 m;必须非负且不递减。 - public double ArcLength { get; } - - /// 从上一点运动到当前点所在方向段的行驶方向。 - public TravelDirection Direction { get; } - - /// 车辆在此点采用的恒曲率原语曲率,单位 1/m。 - public double VehicleCurvature { get; } - - /// 扩大车体到障碍物的保守净空下界,单位 m。 - public double BodyClearance { get; } - - /// 此点是否为新方向段开始的换向点。 - public bool IsGearSwitchPoint { get; } - - /// 此点由起点、普通原语或终点截断产生的来源。 - public CoarsePathPointSource Source { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs deleted file mode 100644 index 1e4ad26..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 粗路径点在搜索与原语重建中的产生来源。 -public enum CoarsePathPointSource -{ - /// 请求提供的起始位姿。 - Start, - - /// 未截断恒曲率运动原语的内部积分点。 - MotionPrimitive, - - /// 首次满足目标容差而在原语内部截断的终点积分点。 - GoalTruncation, -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs deleted file mode 100644 index 351c690..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 终点候选最后一个连续运动段的方向约束。 -public enum GoalDirectionConstraint -{ - /// 不限制进入终点的运动方向。 - Any, - - /// 必须以前进方向进入终点。 - Forward, - - /// 必须以倒车方向进入终点。 - Reverse, -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs deleted file mode 100644 index dc361f0..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// -/// Hybrid A* 粗路径搜索的可调配置。 -/// 长度和距离使用 m,航向使用 rad,曲率使用 1/m;各项有效范围由规划器在执行前校验。 -/// -public sealed class HybridAStarConfiguration -{ - /// 创建采用 P0 固定安全边界和代价权重的默认配置。 - public HybridAStarConfiguration() - { - PrimitiveLengthMeters = 0.50d; - IntegrationStepMeters = 0.05d; - MaximumCollisionCheckStepMeters = 0.025d; - HeadingResolutionRadians = Math.PI / 36d; - CurvatureLevelCount = 5; - GoalPositionToleranceMeters = 0.15d; - GoalHeadingToleranceRadians = Math.PI / 36d; - MaximumExpandedNodes = 200000; - SearchTimeout = TimeSpan.FromSeconds(5d); - HeuristicWeight = 1d; - ReverseCostMultiplier = 1.5d; - GearSwitchPenaltyMeters = 1d; - CurvatureMagnitudeWeight = 0.10d; - CurvatureChangePenaltyMetersPerLevel = 0.05d; - ClearanceCostWeight = 0.20d; - ClearanceCostDistanceMeters = 0.50d; - AllowReverse = true; - } - - /// 单个恒曲率原语的最大行驶长度,单位 m。 - public double PrimitiveLengthMeters { get; set; } - - /// 原语内部输出积分点之间允许的最大弧长,单位 m。 - public double IntegrationStepMeters { get; set; } - - /// 连续碰撞检查允许的最大车辆中心位移,单位 m;实际值还受地图分辨率限制。 - public double MaximumCollisionCheckStepMeters { get; set; } - - /// 搜索状态离散所使用的航向格宽,单位 rad。 - public double HeadingResolutionRadians { get; set; } - - /// 从最大负曲率到最大正曲率的离散曲率等级数。 - public int CurvatureLevelCount { get; set; } - - /// 终点位置允许的欧氏距离误差,单位 m。 - public double GoalPositionToleranceMeters { get; set; } - - /// 终点车头航向允许的最小环形角度误差,单位 rad。 - public double GoalHeadingToleranceRadians { get; set; } - - /// 单次搜索允许扩展的最大节点数。 - public int MaximumExpandedNodes { get; set; } - - /// 单次搜索允许消耗的最长时间;超出后返回 - public TimeSpan SearchTimeout { get; set; } - - /// 二维绕障启发式的权重;1 表示不额外放大。 - public double HeuristicWeight { get; set; } - - /// 倒车原语长度代价相对前进原语的倍率。 - public double ReverseCostMultiplier { get; set; } - - /// 相邻原语发生换向时增加的等效距离代价,单位 m。 - public double GearSwitchPenaltyMeters { get; set; } - - /// 曲率绝对值对应的无量纲代价权重。 - public double CurvatureMagnitudeWeight { get; set; } - - /// 相邻曲率等级每变化一级增加的等效距离代价,单位 m。 - public double CurvatureChangePenaltyMetersPerLevel { get; set; } - - /// 车体保守净空不足时增加的无量纲代价权重。 - public double ClearanceCostWeight { get; set; } - - /// 计算净空代价时视为足够安全的车体保守净空,单位 m。 - public double ClearanceCostDistanceMeters { get; set; } - - /// 是否允许生成倒车原语;false 时搜索只生成前进原语。 - public bool AllowReverse { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs deleted file mode 100644 index 5a6a0e9..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 粗路径中方向一致的一段连续点范围;索引两端均包含在段内。 -public sealed class PathSegment -{ - /// - /// 创建方向分段。 - /// 参数:segmentIndex 为从零开始的段序号;startIndex 与 endIndex 为 的包含式索引;两个换向标记描述段首或段尾是否位于换向对。 - /// - public PathSegment(int segmentIndex, TravelDirection direction, int startIndex, int endIndex, bool startsAtGearSwitch, bool endsAtGearSwitch) - { - SegmentIndex = segmentIndex; - Direction = direction; - StartIndex = startIndex; - EndIndex = endIndex; - StartsAtGearSwitch = startsAtGearSwitch; - EndsAtGearSwitch = endsAtGearSwitch; - } - - /// 从零开始的方向段序号。 - public int SegmentIndex { get; } - - /// 此段所有连续运动点对应的行驶方向。 - public TravelDirection Direction { get; } - - /// 此段在 中的起始索引,包含该点。 - public int StartIndex { get; } - - /// 此段在 中的结束索引,包含该点。 - public int EndIndex { get; } - - /// 此段首点是否为换向后保留的新方向点。 - public bool StartsAtGearSwitch { get; } - - /// 此段尾点是否紧邻下一方向段的换向对。 - public bool EndsAtGearSwitch { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs deleted file mode 100644 index 8ec8ea3..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 一次规划的只读统计与终止说明;长度和净空单位为 m。 -public sealed class PlanningDiagnostics -{ - /// - /// 创建规划统计快照。 - /// 参数中的节点数量均为非负计数;pathLengthMeters 和 minimumBodyClearanceMeters 单位为 m;elapsed 为总耗时;pathSearchElapsed 为地图和起终点预检通过后的路径产出耗时;terminationReason 为可读终止说明,可为 null。 - /// - public PlanningDiagnostics( - int expandedNodeCount = 0, - int generatedNodeCount = 0, - int reopenedNodeCount = 0, - int staleOpenListEntryCount = 0, - int peakOpenListCount = 0, - double pathLengthMeters = 0d, - double minimumBodyClearanceMeters = 0d, - TimeSpan elapsed = default(TimeSpan), - string terminationReason = null, - TimeSpan pathSearchElapsed = default(TimeSpan)) - { - ExpandedNodeCount = expandedNodeCount; - GeneratedNodeCount = generatedNodeCount; - ReopenedNodeCount = reopenedNodeCount; - StaleOpenListEntryCount = staleOpenListEntryCount; - PeakOpenListCount = peakOpenListCount; - PathLengthMeters = pathLengthMeters; - MinimumBodyClearanceMeters = minimumBodyClearanceMeters; - Elapsed = elapsed; - PathSearchElapsed = pathSearchElapsed; - TerminationReason = terminationReason ?? string.Empty; - } - - /// 从 Open List 取出并真正扩展的节点数量。 - public int ExpandedNodeCount { get; } - - /// 生成并尝试加入搜索状态的节点数量。 - public int GeneratedNodeCount { get; } - - /// 以严格更小代价到达同一离散键而重新打开的节点数量。 - public int ReopenedNodeCount { get; } - - /// 从 Open List 取出后因已有更优条目而丢弃的陈旧堆条目数量。 - public int StaleOpenListEntryCount { get; } - - /// 搜索期间 Open List 同时容纳的最大有效或待丢弃条目数量。 - public int PeakOpenListCount { get; } - - /// 成功路径的累计弧长,单位 m;失败结果通常为 0。 - public double PathLengthMeters { get; } - - /// 成功路径所有点中车体保守净空下界的最小值,单位 m;失败结果通常为 0。 - public double MinimumBodyClearanceMeters { get; } - - /// 从规划入口到返回结果的总耗时。 - public TimeSpan Elapsed { get; } - - /// 地图和起终点预检通过后,二维启发式、Hybrid A*、回溯、装配和最终复核的耗时;不含建图,搜索前失败时为零。 - public TimeSpan PathSearchElapsed { get; } - - /// 面向调用方的终止原因;成功时可为空字符串。 - public string TerminationReason { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs deleted file mode 100644 index 057789f..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// -/// 已建规划地图上的一次 Hybrid A* 请求。 -/// 本对象只接受不可变 快照,不包含地图构建器、传感器、UI 或调试对象。 -/// -public sealed class PlanningRequest -{ - /// 创建空规划请求。调用规划器前必须提供地图、起点、终点、车辆和配置。 - public PlanningRequest() - { - StartVehicleCurvature = 0d; - GoalDirection = GoalDirectionConstraint.Any; - } - - /// 本次搜索唯一允许查询的不可变规划地图快照;位置查询单位为 m。 - public PlanningGridMap Map { get; set; } - - /// 车辆几何中心的起始位姿;位置单位 m,航向单位 rad。 - public Pose2D Start { get; set; } - - /// 车辆几何中心的目标位姿;位置单位 m,航向单位 rad。 - public Pose2D Goal { get; set; } - - /// 车辆几何、余量和曲率限制;尺寸单位 m,曲率单位 1/m。 - public VehicleParameters Vehicle { get; set; } - - /// 搜索步长、离散、终点容差、代价和资源上限配置。 - public HybridAStarConfiguration Configuration { get; set; } - - /// 车辆起步时的转向曲率,单位 1/m;默认值为 0。 - public double StartVehicleCurvature { get; set; } - - /// 起步方向约束;null 表示可从前进或倒车开始。 - public TravelDirection? StartDirection { get; set; } - - /// 目标进入方向约束;默认值为 - public GoalDirectionConstraint GoalDirection { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs deleted file mode 100644 index c69548f..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// -/// 一次粗路径规划的最终不可变结果。 -/// 只有 时才携带非空路径与方向分段;其他状态始终返回空只读集合。 -/// -public sealed class PlanningResult -{ - private static readonly IReadOnlyList EmptyPath = new ReadOnlyCollection(new List()); - private static readonly IReadOnlyList EmptySegments = new ReadOnlyCollection(new List()); - - private PlanningResult(PlanningStatus status, PlanningDiagnostics diagnostics, IReadOnlyList path, IReadOnlyList segments) - { - Status = status; - Diagnostics = diagnostics ?? new PlanningDiagnostics(terminationReason: "未提供诊断信息。"); - Path = path; - Segments = segments; - } - - /// 规划最终状态;只有 可以发布路径。 - public PlanningStatus Status { get; } - - /// 节点、耗时、路径长度、净空和终止原因统计;始终非空。 - public PlanningDiagnostics Diagnostics { get; } - - /// 成功时的稠密粗路径;失败时为不可修改的空集合。 - public IReadOnlyList Path { get; } - - /// 成功时覆盖 的包含式方向分段;失败时为不可修改的空集合。 - public IReadOnlyList Segments { get; } - - /// - /// 创建成功结果。 - /// 参数:path 与 segments 必须均为非空;diagnostics 为本次规划的统计快照。参数不符合要求时抛出 ,防止以成功状态发布不完整路径。 - /// - public static PlanningResult Success(IReadOnlyList path, IReadOnlyList segments, PlanningDiagnostics diagnostics) - { - if (path == null || path.Count == 0) - throw new ArgumentException("Successful planning results require a non-empty path.", nameof(path)); - if (segments == null || segments.Count == 0) - throw new ArgumentException("Successful planning results require non-empty segments.", nameof(segments)); - return new PlanningResult(PlanningStatus.Success, diagnostics, CopyReadOnly(path), CopyReadOnly(segments)); - } - - /// - /// 创建失败、取消或资源受限结果。 - /// 参数:status 不能为 ;diagnostics 会原样保留。返回结果的路径与分段始终为空只读集合。 - /// - public static PlanningResult Failure(PlanningStatus status, PlanningDiagnostics diagnostics) - { - if (status == PlanningStatus.Success) - throw new ArgumentException("Use Success to create a successful planning result.", nameof(status)); - return new PlanningResult(status, diagnostics, EmptyPath, EmptySegments); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) - copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs deleted file mode 100644 index 26d5fea..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 粗路径规划的最终状态;除 外均不发布路径或方向分段。 -public enum PlanningStatus -{ - /// 已得到并通过最终连续碰撞复核的完整粗路径。 - Success, - - /// 在搜索扩展检查点收到取消请求。 - Cancelled, - - /// 请求对象或其必要成员为空,或包含不符合基本契约的值。 - InvalidRequest, - - /// 请求提供的地图对象不满足规划器的结构要求。 - InvalidMap, - - /// 地图快照尚未准备好参与规划;应查看地图的阻止原因。 - MapNotReady, - - /// 车辆尺寸、安全余量或曲率限制无效。 - InvalidVehicleParameters, - - /// 原语、离散、代价、容差或资源限制配置无效。 - InvalidCurvatureConfiguration, - - /// 起始车辆几何中心或扩大车体不在地图范围内。 - StartOutsideMap, - - /// 起始扩大车体与地图障碍物相交或擦边。 - StartInCollision, - - /// 目标车辆几何中心或扩大车体不在地图范围内。 - GoalOutsideMap, - - /// 目标扩大车体与地图障碍物相交或擦边。 - GoalInCollision, - - /// 搜索达到 限制。 - SearchTimeout, - - /// 搜索达到 限制。 - SearchNodeLimitExceeded, - - /// Open List 已耗尽,或二维启发式证明目标不可达。 - NoFeasiblePath, - - /// 搜索成功节点无法按父链回溯为完整路径。 - BacktrackingFailed, - - /// 回溯路径未通过连续碰撞、终点或输出不变量复核。 - FinalValidationFailed, - - /// 规划内部发生未预期错误;不会发布部分路径。 - InternalError, -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs deleted file mode 100644 index 77af135..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// -/// 粗路径规划使用的二维连续位姿。 -/// 位置以世界坐标 m 表示,航向以 rad 表示;此值对象不在构造时归一化航向,调用方可保留展开航向。 -/// -public sealed class Pose2D -{ - /// - /// 创建二维位姿。 - /// 参数:xMeters、yMeters 为世界坐标,单位 m;headingRadians 为车头航向,单位 rad。 - /// - public Pose2D(double xMeters, double yMeters, double headingRadians) - { - X = xMeters; - Y = yMeters; - Heading = headingRadians; - } - - /// 世界 X 坐标,单位 m。 - public double X { get; } - - /// 世界 Y 坐标,单位 m。 - public double Y { get; } - - /// 车头航向,单位 rad;可以是已展开的连续航向。 - public double Heading { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs deleted file mode 100644 index 8bf17dc..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// 连续运动段的行驶方向。 -public enum TravelDirection -{ - /// 沿车辆车头方向前进。 - Forward, - - /// 与车辆车头方向相反地倒车。 - Reverse, -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs deleted file mode 100644 index 286f928..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// -/// 车辆几何与运动学参数。 -/// 所有几何尺寸均以车辆几何中心为 参考点,单位为 m;曲率单位为 1/m。 -/// -public sealed class VehicleParameters -{ - /// 创建空车辆参数。调用规划器前必须填写有效的几何尺寸和至少一种曲率限制。 - public VehicleParameters() - { - } - - /// 车辆本体长度,单位 m;不含 - public double LengthMeters { get; set; } - - /// 车辆本体宽度,单位 m;不含 - public double WidthMeters { get; set; } - - /// 碰撞检查时加在车体四周的安全余量,单位 m;不会写入地图。 - public double SafetyMarginMeters { get; set; } - - /// 车辆允许的最大绝对曲率,单位 1/m;null 表示由 提供限制。 - public double? MaximumCurvaturePerMeter { get; set; } - - /// 车辆允许的最小转弯半径,单位 m;null 表示由 提供限制。 - public double? MinimumTurningRadiusMeters { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs deleted file mode 100644 index 78b4685..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -/// -/// 一次从地图输入到 Hybrid A* 粗路径输出的完整业务请求。 -/// 地图边界、分辨率和障碍物几何位于 中并使用 mm;位姿和车辆几何使用 m,航向使用 rad,曲率使用 1/m。 -/// -public sealed class CoarsePathPlanningJob -{ - /// 创建默认目标方向、起步曲率和关闭调试旁路的业务请求。 - public CoarsePathPlanningJob() - { - StartVehicleCurvature = 0d; - GoalDirection = GoalDirectionConstraint.Any; - DebugOptions = new PlanningDebugOptions(); - } - - /// 本次唯一建图输入;边界、分辨率和障碍物几何均遵循 Map 模块的 mm 契约。 - public PlanningMapRequest MapRequest { get; set; } - - /// 车辆几何中心的起始位姿;位置单位 m,航向单位 rad。 - public Pose2D Start { get; set; } - - /// 车辆几何中心的目标位姿;位置单位 m,航向单位 rad。 - public Pose2D Goal { get; set; } - - /// 车辆尺寸、安全余量和曲率限制;尺寸单位 m,曲率单位 1/m。 - public VehicleParameters Vehicle { get; set; } - - /// 原语、离散、容差、代价和资源上限配置。 - public HybridAStarConfiguration Configuration { get; set; } - - /// 车辆起步曲率,单位 1/m;默认值为 0。 - public double StartVehicleCurvature { get; set; } - - /// 起步方向约束;null 表示可从前进或倒车开始。 - public TravelDirection? StartDirection { get; set; } - - /// 目标进入方向约束;默认值为 - public GoalDirectionConstraint GoalDirection { get; set; } - - /// 可选调试旁路配置;默认关闭且使用空接收器,不参与地图或路径计算。 - public PlanningDebugOptions DebugOptions { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs deleted file mode 100644 index b96208d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -/// -/// 一次粗路径业务编排的不可变结果。 -/// 始终同时保留地图构建结果和规划结果;调试旁路消息只用于诊断,不会修改地图或规划内容。 -/// -public sealed class CoarsePathPlanningJobResult -{ - /// - /// 创建业务编排结果。 - /// 参数:mapResult 和 planningResult 均不能为空;debugDiagnostics 可为 null,返回时会转换为只读字符串集合。 - /// - public CoarsePathPlanningJobResult(PlanningMapBuildResult mapResult, PlanningResult planningResult, - IReadOnlyList debugDiagnostics = null) - { - MapResult = mapResult ?? throw new ArgumentNullException(nameof(mapResult)); - PlanningResult = planningResult ?? throw new ArgumentNullException(nameof(planningResult)); - DebugDiagnostics = CopyDiagnostics(debugDiagnostics); - } - - /// 本次调用的地图创建结果;失败时读取 - public PlanningMapBuildResult MapResult { get; } - - /// 本次调用的粗路径结果;地图创建失败时为无路径的失败状态。 - public PlanningResult PlanningResult { get; } - - /// 调试旁路的非致命诊断信息;为空时表示未启用、未发生异常或无额外调试消息。 - public IReadOnlyList DebugDiagnostics { get; } - - private static IReadOnlyList CopyDiagnostics(IReadOnlyList source) - { - if (source == null || source.Count == 0) - return new ReadOnlyCollection(new List()); - - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) - copy.Add(source[index] ?? string.Empty); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs deleted file mode 100644 index a4e5926..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -/// -/// 从 到 Hybrid A* 粗路径的一次调用服务。 -/// 服务生命周期内长期持有同一个地图工厂和规划器,以保留地图缓存并避免将 UI、传感器或调试依赖带入规划核心。 -/// -public sealed class CoarsePathPlanningService -{ - private readonly PlanningMapFactory _mapFactory; - private readonly HybridAStarPlanner _planner; - - /// 创建长期复用默认地图工厂与 Hybrid A* 规划器的服务。 - public CoarsePathPlanningService() - : this(new PlanningMapFactory(), new HybridAStarPlanner()) - { - } - - internal CoarsePathPlanningService(PlanningMapFactory mapFactory, HybridAStarPlanner planner) - { - _mapFactory = mapFactory ?? throw new ArgumentNullException(nameof(mapFactory)); - _planner = planner ?? throw new ArgumentNullException(nameof(planner)); - } - - /// - /// 按固定顺序创建地图并执行一次 Hybrid A* 粗路径规划。 - /// 参数:job 的地图输入使用 mm,位姿和车辆尺寸使用 m,航向使用 rad,曲率使用 1/m;cancellationToken 会传递给搜索阶段。 - /// 返回:始终同时保留地图创建结果和规划结果;地图创建失败时不会启动搜索,并返回 的空路径结果。 - /// - public CoarsePathPlanningJobResult Plan(CoarsePathPlanningJob job, - CancellationToken cancellationToken = default(CancellationToken)) - { - PlanningOperationBudget budget = CreateBudget(job, cancellationToken); - PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget); - if (!mapResult.Succeeded || mapResult.Map == null) - { - PlanningResult mapFailure = PlanningResult.Failure(MapFailureStatus(mapResult), - new PlanningDiagnostics(elapsed: budget.Elapsed, terminationReason: BuildMapFailureReason(mapResult))); - return PublishDebug(job, mapResult, mapFailure); - } - - var request = new PlanningRequest - { - Map = mapResult.Map, - Start = job.Start, - Goal = job.Goal, - Vehicle = job.Vehicle, - Configuration = job.Configuration, - StartVehicleCurvature = job.StartVehicleCurvature, - StartDirection = job.StartDirection, - GoalDirection = job.GoalDirection, - }; - PlanningResult planningResult = _planner.Plan(request, budget); - return PublishDebug(job, mapResult, planningResult); - } - - private static PlanningOperationBudget CreateBudget(CoarsePathPlanningJob job, CancellationToken cancellationToken) - { - HybridAStarConfiguration configuration = job == null ? null : job.Configuration; - return configuration != null && configuration.SearchTimeout >= TimeSpan.Zero - ? new PlanningOperationBudget(cancellationToken, configuration.SearchTimeout) - : PlanningOperationBudget.Unlimited(cancellationToken); - } - - private static PlanningStatus MapFailureStatus(PlanningMapBuildResult mapResult) - { - if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.Cancelled) return PlanningStatus.Cancelled; - if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.TimedOut) return PlanningStatus.SearchTimeout; - return PlanningStatus.InvalidMap; - } - - private static CoarsePathPlanningJobResult PublishDebug(CoarsePathPlanningJob job, PlanningMapBuildResult mapResult, - PlanningResult planningResult) - { - var diagnostics = new List(); - PlanningDebugOptions options = job == null ? null : job.DebugOptions; - if (options != null && options.Enabled) - { - IPlanningDebugSink sink = options.Sink ?? NullPlanningDebugSink.Instance; - try - { - sink.Publish(mapResult, planningResult); - } - catch (Exception exception) - { - diagnostics.Add("调试旁路发布失败:" + exception.GetType().Name + "。" + exception.Message); - } - } - - return new CoarsePathPlanningJobResult(mapResult, planningResult, diagnostics); - } - - private static string BuildMapFailureReason(PlanningMapBuildResult mapResult) - { - if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.Cancelled) - return "规划地图创建已取消,未启动 Hybrid A* 搜索。"; - if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.TimedOut) - return "规划地图创建已超时,未启动 Hybrid A* 搜索。"; - string reason = mapResult == null ? string.Empty : mapResult.FailureReason; - return string.IsNullOrEmpty(reason) ? "规划地图创建失败,未启动 Hybrid A* 搜索。" : - "规划地图创建失败,未启动 Hybrid A* 搜索:" + reason; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs deleted file mode 100644 index 3f6399e..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -/// -/// 接收一次地图构建和粗路径规划完成后的旁路调试数据。 -/// 实现不得修改输入对象;服务会隔离实现抛出的异常,调试失败不会改变规划结果。 -/// -public interface IPlanningDebugSink -{ - /// - /// 发布本次编排得到的地图和规划结果。 - /// 参数:mapResult 为地图创建结果;planningResult 为对应的完整或失败规划结果;二者均不可由接收方修改。 - /// - void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult); -} - -internal sealed class NullPlanningDebugSink : IPlanningDebugSink -{ - internal static readonly NullPlanningDebugSink Instance = new NullPlanningDebugSink(); - - private NullPlanningDebugSink() - { - } - - public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) - { - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs deleted file mode 100644 index a022f20..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -/// -/// 一次粗路径编排的可选调试旁路配置。 -/// 调试开关和接收器不参与地图输入指纹、缓存键、搜索状态或路径结果。 -/// -public sealed class PlanningDebugOptions -{ - /// 创建默认关闭并使用空接收器的调试配置。 - public PlanningDebugOptions() - { - Enabled = false; - Sink = NullPlanningDebugSink.Instance; - } - - /// 是否在本次编排结束后向 发布旁路数据;默认值为 false。 - public bool Enabled { get; set; } - - /// - /// 接收地图和规划结果的旁路对象;默认为空实现。赋值为 null 时服务仍会使用空实现,且不会影响主流程。 - /// - public IPlanningDebugSink Sink { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs deleted file mode 100644 index 697a488..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Output; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath; - -/// -/// 已建 上 Hybrid A* 粗路径规划的下层门面。 -/// 本类不建图、不读取 UI 或传感器;只有路径经回溯、装配和最终连续复核后才发布成功结果。 -/// -public sealed class HybridAStarPlanner -{ - private readonly HybridAStarSearch _search; - private readonly PathBacktracker _backtracker; - private readonly CoarsePathAssembler _assembler; - private readonly CoarsePathValidator _validator; - private readonly FootprintCollisionChecker _collisionChecker; - - /// 创建使用默认搜索、回溯、装配和最终复核组件的规划器。 - public HybridAStarPlanner() - : this(new HybridAStarSearch(), new PathBacktracker(), new CoarsePathAssembler(), new CoarsePathValidator(), - new FootprintCollisionChecker()) - { - } - - internal HybridAStarPlanner(HybridAStarSearch search, PathBacktracker backtracker, CoarsePathAssembler assembler, - CoarsePathValidator validator, FootprintCollisionChecker collisionChecker) - { - _search = search ?? throw new ArgumentNullException(nameof(search)); - _backtracker = backtracker ?? throw new ArgumentNullException(nameof(backtracker)); - _assembler = assembler ?? throw new ArgumentNullException(nameof(assembler)); - _validator = validator ?? throw new ArgumentNullException(nameof(validator)); - _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); - } - - /// - /// 在请求提供的不可变规划地图上执行一次 Hybrid A* 粗路径规划。 - /// 参数:request 的位置单位为 m、航向单位为 rad、曲率单位为 1/m;cancellationToken 会在搜索扩展检查点取消。 - /// 返回:输入、边界、碰撞、搜索、回溯或最终复核失败均返回空路径;只有 携带完整路径和分段。 - /// - public PlanningResult Plan(PlanningRequest request, CancellationToken cancellationToken = default(CancellationToken)) - { - PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero - ? new PlanningOperationBudget(cancellationToken, request.Configuration.SearchTimeout) - : PlanningOperationBudget.Unlimited(cancellationToken); - return Plan(request, budget); - } - - /// 使用门面传入的共享预算执行预检、搜索和最终路径复核。 - internal PlanningResult Plan(PlanningRequest request, PlanningOperationBudget budget) - { - Stopwatch pathSearchStopwatch = null; - try - { - if (budget == null) throw new ArgumentNullException(nameof(budget)); - PlanningOperationStopReason stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) - return Failure(ToPlanningStatus(stopReason), budget, "规划在开始前已停止。", null); - - PlanningStatus preflightStatus = ValidatePreflight(request, out string preflightReason); - if (preflightStatus != PlanningStatus.Success) - return Failure(preflightStatus, budget, preflightReason, null); - - if (!IsFootprintInsideMap(request.Start, request.Map, request.Vehicle)) - return Failure(PlanningStatus.StartOutsideMap, budget, "起始扩大车体不完全位于地图边界内。", null); - if (!_collisionChecker.IsPoseCollisionFree(request.Start, request.Map, request.Vehicle, 0d, out _)) - return Failure(PlanningStatus.StartInCollision, budget, "起始扩大车体与障碍物相交或擦边。", null); - if (!IsFootprintInsideMap(request.Goal, request.Map, request.Vehicle)) - return Failure(PlanningStatus.GoalOutsideMap, budget, "目标扩大车体不完全位于地图边界内。", null); - if (!_collisionChecker.IsPoseCollisionFree(request.Goal, request.Map, request.Vehicle, 0d, out _)) - return Failure(PlanningStatus.GoalInCollision, budget, "目标扩大车体与障碍物相交或擦边。", null); - - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) - return Failure(ToPlanningStatus(stopReason), budget, "规划在搜索前已停止。", null); - pathSearchStopwatch = Stopwatch.StartNew(); - HybridAStarSearchResult searchResult = _search.Search(request, budget); - if (searchResult == null) - return Failure(PlanningStatus.InternalError, budget, "搜索器未返回结果。", null, pathSearchStopwatch); - if (searchResult.Status != PlanningStatus.Success) - return Failure(searchResult.Status, budget, - BuildSearchFailureReason(searchResult, request.Configuration), searchResult, pathSearchStopwatch); - - if (!_backtracker.TryBacktrack(searchResult, request, out BacktrackedPath backtrackedPath, out string backtrackingReason)) - return Failure(PlanningStatus.BacktrackingFailed, budget, backtrackingReason, searchResult, pathSearchStopwatch); - if (!_assembler.TryAssemble(backtrackedPath, request, out var path, out var segments, out string assemblyReason)) - return Failure(PlanningStatus.FinalValidationFailed, budget, assemblyReason, searchResult, pathSearchStopwatch); - if (!_validator.TryValidate(path, segments, request, out double minimumClearanceMeters, out string validationReason)) - return Failure(PlanningStatus.FinalValidationFailed, budget, validationReason, searchResult, pathSearchStopwatch); - - double pathLengthMeters = path[path.Count - 1].ArcLength; - return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters, - minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); - } - catch (Exception exception) - { - string reason = "规划内部错误:" + exception.GetType().Name + - (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); - return Failure(PlanningStatus.InternalError, budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None), - reason, null, pathSearchStopwatch); - } - } - - private static PlanningStatus ValidatePreflight(PlanningRequest request, out string failureReason) - { - failureReason = string.Empty; - if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || - !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || - !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) - { - failureReason = "规划请求缺少必要对象或包含非法数值。"; - return PlanningStatus.InvalidRequest; - } - - PlanningGridMap map = request.Map; - if (map.Bounds == null || map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters)) - { - failureReason = "规划地图结构无效。"; - return PlanningStatus.InvalidMap; - } - if (!map.PlanningReady) - { - failureReason = string.IsNullOrEmpty(map.PlanningBlockReason) ? "规划地图尚未就绪。" : map.PlanningBlockReason; - return PlanningStatus.MapNotReady; - } - - VehicleParameters vehicle = request.Vehicle; - if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || - !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) - { - failureReason = "车辆尺寸、安全余量或曲率限制无效。"; - return PlanningStatus.InvalidVehicleParameters; - } - - HybridAStarConfiguration configuration = request.Configuration; - if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || - (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) - { - failureReason = "Hybrid A* 曲率、离散、代价或资源配置无效。"; - return PlanningStatus.InvalidCurvatureConfiguration; - } - - return PlanningStatus.Success; - } - - private static bool IsValidConfiguration(HybridAStarConfiguration configuration) - { - return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && - NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && - NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && - NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && - configuration.HeadingResolutionRadians <= 2d * Math.PI && configuration.CurvatureLevelCount >= 3 && - configuration.CurvatureLevelCount % 2 == 1 && NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && - configuration.GoalPositionToleranceMeters >= 0d && NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && - configuration.GoalHeadingToleranceRadians >= 0d && configuration.MaximumExpandedNodes >= 0 && - configuration.SearchTimeout >= TimeSpan.Zero && NumericGuard.IsFinite(configuration.HeuristicWeight) && - configuration.HeuristicWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && - NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && - NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && - NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && - configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && NumericGuard.IsFinite(configuration.ClearanceCostWeight) && - configuration.ClearanceCostWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); - } - - private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) - { - if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; - - double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; - double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; - double longitudinalX = Math.Cos(pose.Heading); - double longitudinalY = Math.Sin(pose.Heading); - double lateralX = -longitudinalY; - double lateralY = longitudinalX; - for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) - for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) - { - double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; - double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; - if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; - } - - return true; - } - - private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult, - HybridAStarConfiguration configuration) - { - string reason = string.IsNullOrEmpty(searchResult.TerminationReason) - ? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。" - : searchResult.TerminationReason; - string resourceLimit = string.Empty; - if (searchResult.Status == PlanningStatus.SearchTimeout) - { - resourceLimit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString( - "F3", CultureInfo.InvariantCulture) + "s;"; - } - else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded) - { - resourceLimit = "节点上限=" + configuration.MaximumExpandedNodes.ToString( - CultureInfo.InvariantCulture) + ";"; - } - - return reason + resourceLimit + - "扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + - "生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + - "重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + - "陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "," + - "Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。"; - } - - private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, - HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) - { - TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed; - return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, - reason, pathSearchElapsed)); - } - - private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed, - double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed) - { - return new PlanningDiagnostics( - searchResult == null ? 0 : searchResult.ExpandedNodeCount, - searchResult == null ? 0 : searchResult.GeneratedNodeCount, - searchResult == null ? 0 : searchResult.ReopenedNodeCount, - searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, - searchResult == null ? 0 : searchResult.PeakOpenListCount, - pathLengthMeters, - minimumClearanceMeters, - elapsed, - reason, - pathSearchElapsed); - } - - private static bool IsFinitePose(Pose2D pose) - { - return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } - - private static bool IsGoalDirection(GoalDirectionConstraint direction) - { - return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; - } - - private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) - { - if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; - if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; - throw new ArgumentOutOfRangeException(nameof(stopReason)); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs deleted file mode 100644 index 29df53d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; - -/// -/// 将回溯原语装配为调用方可消费的稠密路径和包含式方向分段。 -/// 装配器保留原语边界的换向双点,其余相邻重复位姿会被删除。 -/// -public sealed class CoarsePathAssembler -{ - private const double DuplicateTolerance = 1e-8d; - private readonly FootprintCollisionChecker _collisionChecker; - - /// 创建使用默认连续车体检查器的路径装配器。 - public CoarsePathAssembler() - : this(new FootprintCollisionChecker()) - { - } - - /// 创建使用指定连续车体检查器的路径装配器。 - public CoarsePathAssembler(FootprintCollisionChecker collisionChecker) - { - _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); - } - - /// - /// 从已回溯的恒曲率原语构造稠密路径。 - /// 参数:backtrackedPath 提供原语顺序;request 提供地图和车辆;path、segments 为成功时的只读输出。 - /// 返回:首点可通过连续车体检查、每个原语积分点数据一致且方向分段完整覆盖时为 true;否则返回 false 且输出为空。 - /// - public bool TryAssemble(BacktrackedPath backtrackedPath, PlanningRequest request, - out IReadOnlyList path, out IReadOnlyList segments, out string failureReason) - { - path = EmptyPath(); - segments = EmptySegments(); - failureReason = string.Empty; - if (backtrackedPath == null || request == null || request.Map == null || request.Vehicle == null || - !IsFinitePose(backtrackedPath.Start) || !IsTravelDirection(backtrackedPath.StartDirection) || - !NumericGuard.IsFinite(backtrackedPath.StartCurvaturePerMeter)) - { - failureReason = "路径装配输入无效。"; - return false; - } - - if (!_collisionChecker.IsPoseCollisionFree(backtrackedPath.Start, request.Map, request.Vehicle, 0d, out double startClearanceMeters)) - { - failureReason = "回溯路径起点未通过连续车体检查。"; - return false; - } - - var points = new List(); - TravelDirection currentDirection = backtrackedPath.Primitives.Count > 0 - ? backtrackedPath.Primitives[0].Direction - : backtrackedPath.StartDirection; - double currentCurvature = request.StartVehicleCurvature; - double normalizedStartHeading = AngleMath.NormalizeRadians(backtrackedPath.Start.Heading); - if (!NumericGuard.IsFinite(normalizedStartHeading)) - { - failureReason = "回溯路径起点航向无效。"; - return false; - } - - points.Add(new CoarsePathPoint(backtrackedPath.Start.X, backtrackedPath.Start.Y, normalizedStartHeading, - backtrackedPath.Start.Heading, 0d, currentDirection, currentCurvature, startClearanceMeters, false, - CoarsePathPointSource.Start)); - - for (int primitiveIndex = 0; primitiveIndex < backtrackedPath.Primitives.Count; primitiveIndex++) - { - MotionPrimitive primitive = backtrackedPath.Primitives[primitiveIndex]; - if (!IsValidPrimitive(primitive)) - { - failureReason = "回溯路径包含无效原语。"; - return false; - } - - CoarsePathPoint lastPoint = points[points.Count - 1]; - if (primitive.Direction != lastPoint.Direction) - { - // 换向处的旧方向终点和新方向起点必须共存,二者位置、航向、弧长完全相同。 - points.Add(new CoarsePathPoint(lastPoint.X, lastPoint.Y, lastPoint.Heading, lastPoint.UnwrappedHeading, - lastPoint.ArcLength, primitive.Direction, primitive.CurvaturePerMeter, lastPoint.BodyClearance, - true, CoarsePathPointSource.MotionPrimitive)); - } - - Pose2D previousPose = primitive.Start; - for (int pointIndex = 0; pointIndex < primitive.Points.Count; pointIndex++) - { - Pose2D pose = primitive.Points[pointIndex]; - double bodyClearanceMeters = primitive.BodyClearancesMeters[pointIndex]; - if (!IsFinitePose(pose) || !IsValidClearance(bodyClearanceMeters)) - { - failureReason = "原语积分点或净空无效。"; - return false; - } - - CoarsePathPoint previousPoint = points[points.Count - 1]; - double arcIncrementMeters = CalculateArcIncrement(previousPose, pose, primitive.CurvaturePerMeter); - if (!NumericGuard.IsFinite(arcIncrementMeters) || arcIncrementMeters <= 0d) - { - failureReason = "原语积分点未产生正弧长。"; - return false; - } - - if (IsSamePose(previousPoint, pose)) - { - // 非换向情况下不允许重复采样点泄露到对外路径。 - previousPose = pose; - continue; - } - - double normalizedHeading = AngleMath.NormalizeRadians(pose.Heading); - double headingDelta = AngleMath.ShortestSignedDifference(previousPoint.Heading, normalizedHeading); - if (!NumericGuard.IsFinite(normalizedHeading) || !NumericGuard.IsFinite(headingDelta)) - { - failureReason = "原语积分点航向无法展开。"; - return false; - } - - CoarsePathPointSource source = primitive.IsGoalTruncation && pointIndex == primitive.Points.Count - 1 - ? CoarsePathPointSource.GoalTruncation - : CoarsePathPointSource.MotionPrimitive; - points.Add(new CoarsePathPoint(pose.X, pose.Y, normalizedHeading, - previousPoint.UnwrappedHeading + headingDelta, previousPoint.ArcLength + arcIncrementMeters, - primitive.Direction, primitive.CurvaturePerMeter, bodyClearanceMeters, false, source)); - previousPose = pose; - } - } - - if (points.Count == 0) - { - failureReason = "路径装配未产生起点。"; - return false; - } - - path = new ReadOnlyCollection(points); - segments = BuildSegments(points); - return true; - } - - private static IReadOnlyList BuildSegments(IReadOnlyList points) - { - var segments = new List(); - int startIndex = 0; - TravelDirection direction = points[0].Direction; - for (int index = 1; index < points.Count; index++) - { - if (points[index].Direction == direction) continue; - segments.Add(new PathSegment(segments.Count, direction, startIndex, index - 1, - points[startIndex].IsGearSwitchPoint, true)); - startIndex = index; - direction = points[index].Direction; - } - - segments.Add(new PathSegment(segments.Count, direction, startIndex, points.Count - 1, - points[startIndex].IsGearSwitchPoint, false)); - return new ReadOnlyCollection(segments); - } - - private static double CalculateArcIncrement(Pose2D from, Pose2D to, double curvaturePerMeter) - { - if (!IsFinitePose(from) || !IsFinitePose(to) || !NumericGuard.IsFinite(curvaturePerMeter)) return double.NaN; - if (Math.Abs(curvaturePerMeter) < 1e-12d) - { - double deltaX = to.X - from.X; - double deltaY = to.Y - from.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - - double headingDelta = AngleMath.ShortestSignedDifference(from.Heading, to.Heading); - return Math.Abs(headingDelta / curvaturePerMeter); - } - - private static bool IsValidPrimitive(MotionPrimitive primitive) - { - return primitive != null && primitive.Start != null && primitive.Points != null && primitive.BodyClearancesMeters != null && - primitive.Points.Count == primitive.BodyClearancesMeters.Count && primitive.Points.Count > 0 && - IsTravelDirection(primitive.Direction) && NumericGuard.IsFinite(primitive.CurvaturePerMeter) && - NumericGuard.IsPositiveFinite(primitive.ActualLengthMeters); - } - - private static bool IsSamePose(CoarsePathPoint point, Pose2D pose) - { - return Math.Abs(point.X - pose.X) <= DuplicateTolerance && Math.Abs(point.Y - pose.Y) <= DuplicateTolerance && - Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, pose.Heading)) <= DuplicateTolerance; - } - - private static bool IsFinitePose(Pose2D pose) - { - return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } - - private static bool IsValidClearance(double clearanceMeters) - { - return !double.IsNaN(clearanceMeters) && clearanceMeters >= 0d; - } - - private static IReadOnlyList EmptyPath() - { - return new ReadOnlyCollection(new List()); - } - - private static IReadOnlyList EmptySegments() - { - return new ReadOnlyCollection(new List()); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs deleted file mode 100644 index e9a12a3..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; - -/// -/// 对准备发布的粗路径执行独立的连续安全与输出契约复核。 -/// 复核失败的路径不得被包装为 。 -/// -public sealed class CoarsePathValidator -{ - private const double NumericTolerance = 1e-6d; - private readonly FootprintCollisionChecker _collisionChecker; - - /// 创建使用默认连续车体检查器的最终路径复核器。 - public CoarsePathValidator() - : this(new FootprintCollisionChecker()) - { - } - - /// 创建使用指定连续车体检查器的最终路径复核器。 - public CoarsePathValidator(FootprintCollisionChecker collisionChecker) - { - _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); - } - - /// - /// 复核路径数值、曲率、连续扫掠碰撞、终点约束、累计弧长和方向分段。 - /// 参数:path 与 segments 为待发布输出;request 必须是生成该路径的请求;minimumBodyClearanceMeters 返回沿途的保守净空下界。 - /// 返回:所有规则通过时为 true;否则返回 false、写入失败原因,调用方必须丢弃 path 和 segments。 - /// - public bool TryValidate(IReadOnlyList path, IReadOnlyList segments, - PlanningRequest request, out double minimumBodyClearanceMeters, out string failureReason) - { - minimumBodyClearanceMeters = 0d; - failureReason = string.Empty; - if (path == null || segments == null || request == null || request.Map == null || request.Vehicle == null || - request.Configuration == null || path.Count == 0 || segments.Count == 0) - { - failureReason = "最终路径或复核请求为空。"; - return false; - } - - if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double maximumCurvaturePerMeter)) - { - failureReason = "车辆曲率约束无效。"; - return false; - } - - CoarsePathPoint first = path[0]; - if (!IsValidPoint(first) || first.Source != CoarsePathPointSource.Start || first.IsGearSwitchPoint || - Math.Abs(first.ArcLength) > NumericTolerance || !IsSamePose(first, request.Start)) - { - failureReason = "最终路径首点不符合起点契约。"; - return false; - } - - double minimumClearance = double.PositiveInfinity; - for (int index = 0; index < path.Count; index++) - { - CoarsePathPoint current = path[index]; - if (!IsValidPoint(current) || Math.Abs(current.VehicleCurvature) > maximumCurvaturePerMeter + NumericTolerance) - { - failureReason = "最终路径包含非法数值或超限曲率。"; - return false; - } - - var currentPose = new Pose2D(current.X, current.Y, current.Heading); - if (!_collisionChecker.IsPoseCollisionFree(currentPose, request.Map, request.Vehicle, 0d, out double poseClearanceMeters) || - IsClearanceOverclaimed(current.BodyClearance, poseClearanceMeters)) - { - failureReason = "最终路径点未通过连续车体碰撞复核。"; - return false; - } - - minimumClearance = Math.Min(minimumClearance, current.BodyClearance); - if (index == 0) continue; - - CoarsePathPoint previous = path[index - 1]; - if (current.ArcLength + NumericTolerance < previous.ArcLength || - !IsUnwrappedHeadingContinuous(previous, current)) - { - failureReason = "最终路径的弧长或展开航向不连续。"; - return false; - } - - bool isDuplicate = IsDuplicatePoseAndArcLength(previous, current); - if (isDuplicate) - { - if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) - { - failureReason = "相邻重复点不是合法换向对。"; - return false; - } - } - else - { - if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + NumericTolerance || - !IsArcIncrementConsistent(previous, current)) - { - failureReason = "非换向路径点的弧长增量不一致。"; - return false; - } - } - - var previousPose = new Pose2D(previous.X, previous.Y, previous.Heading); - if (!_collisionChecker.IsSweptMotionCollisionFree(previousPose, currentPose, request.Map, request.Vehicle, - request.Configuration.MaximumCollisionCheckStepMeters, out double sweptClearanceMeters)) - { - failureReason = "最终路径相邻点之间的车体扫掠碰撞复核失败。"; - return false; - } - - minimumClearance = Math.Min(minimumClearance, sweptClearanceMeters); - } - - CoarsePathPoint last = path[path.Count - 1]; - if (!GoalToleranceChecker.IsSatisfied(new Pose2D(last.X, last.Y, last.Heading), request.Goal, request.Configuration, - last.Direction, request.GoalDirection) || (path.Count > 1 && last.Source != CoarsePathPointSource.GoalTruncation)) - { - failureReason = "最终路径末点未满足终点容差、方向或来源契约。"; - return false; - } - - if (!AreSegmentsValid(path, segments, out failureReason)) return false; - minimumBodyClearanceMeters = minimumClearance; - return true; - } - - private static bool AreSegmentsValid(IReadOnlyList path, IReadOnlyList segments, out string failureReason) - { - failureReason = string.Empty; - int expectedStartIndex = 0; - for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) - { - PathSegment segment = segments[segmentIndex]; - if (segment == null || segment.SegmentIndex != segmentIndex || segment.StartIndex != expectedStartIndex || - segment.StartIndex < 0 || segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count || - segment.StartsAtGearSwitch != path[segment.StartIndex].IsGearSwitchPoint) - { - failureReason = "方向分段索引或起始换向标记无效。"; - return false; - } - - for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) - { - if (path[pointIndex].Direction != segment.Direction) - { - failureReason = "方向分段包含不同方向的路径点。"; - return false; - } - } - - bool hasNextSegment = segmentIndex + 1 < segments.Count; - bool expectedEndsAtGearSwitch = hasNextSegment && segment.EndIndex + 1 < path.Count && - path[segment.EndIndex + 1].IsGearSwitchPoint; - if (segment.EndsAtGearSwitch != expectedEndsAtGearSwitch) - { - failureReason = "方向分段末尾换向标记无效。"; - return false; - } - - expectedStartIndex = segment.EndIndex + 1; - } - - if (expectedStartIndex != path.Count) - { - failureReason = "方向分段未完整覆盖路径点。"; - return false; - } - - return true; - } - - private static bool IsValidPoint(CoarsePathPoint point) - { - if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || - !NumericGuard.IsFinite(point.Heading) || !NumericGuard.IsFinite(point.UnwrappedHeading) || - !NumericGuard.IsFinite(point.ArcLength) || point.ArcLength < 0d || - !NumericGuard.IsFinite(point.VehicleCurvature) || !IsTravelDirection(point.Direction) || - !IsValidClearance(point.BodyClearance)) - return false; - - return Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= NumericTolerance; - } - - private static bool IsSamePose(CoarsePathPoint point, Pose2D pose) - { - return point != null && pose != null && Math.Abs(point.X - pose.X) <= NumericTolerance && - Math.Abs(point.Y - pose.Y) <= NumericTolerance && - Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, pose.Heading)) <= NumericTolerance; - } - - private static bool IsUnwrappedHeadingContinuous(CoarsePathPoint previous, CoarsePathPoint current) - { - double expectedDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); - return NumericGuard.IsFinite(expectedDelta) && - Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedDelta) <= NumericTolerance; - } - - private static bool IsDuplicatePoseAndArcLength(CoarsePathPoint previous, CoarsePathPoint current) - { - return Math.Abs(previous.X - current.X) <= NumericTolerance && Math.Abs(previous.Y - current.Y) <= NumericTolerance && - Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= NumericTolerance && - Math.Abs(previous.ArcLength - current.ArcLength) <= NumericTolerance; - } - - private static bool IsArcIncrementConsistent(CoarsePathPoint previous, CoarsePathPoint current) - { - double expectedIncrement; - if (Math.Abs(current.VehicleCurvature) < 1e-12d) - { - double deltaX = current.X - previous.X; - double deltaY = current.Y - previous.Y; - expectedIncrement = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - else - { - double headingDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); - expectedIncrement = Math.Abs(headingDelta / current.VehicleCurvature); - } - - return NumericGuard.IsFinite(expectedIncrement) && expectedIncrement > 0d && - Math.Abs((current.ArcLength - previous.ArcLength) - expectedIncrement) <= NumericTolerance; - } - - private static bool IsClearanceOverclaimed(double reportedClearanceMeters, double checkedClearanceMeters) - { - if (double.IsPositiveInfinity(reportedClearanceMeters)) return !double.IsPositiveInfinity(checkedClearanceMeters); - return reportedClearanceMeters > checkedClearanceMeters + NumericTolerance; - } - - private static bool IsValidClearance(double clearanceMeters) - { - return !double.IsNaN(clearanceMeters) && clearanceMeters >= 0d; - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs deleted file mode 100644 index 37a55e6..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; - -/// -/// 成功搜索父链重新生成后的原语序列。 -/// 起点位置使用 m/rad,起始曲率使用 1/m;原语集合按从起点到终点的顺序排列。 -/// -public sealed class BacktrackedPath -{ - internal BacktrackedPath(Pose2D start, TravelDirection startDirection, double startCurvaturePerMeter, - IReadOnlyList primitives) - { - Start = start; - StartDirection = startDirection; - StartCurvaturePerMeter = startCurvaturePerMeter; - Primitives = primitives; - } - - /// 原始请求中的起始车辆中心位姿;位置单位 m,航向单位 rad。 - public Pose2D Start { get; } - - /// 成功父链根节点记录的起步方向。 - public TravelDirection StartDirection { get; } - - /// 成功父链根节点离散后的起始曲率,单位 1/m。 - public double StartCurvaturePerMeter { get; } - - /// 按起点到终点顺序重新积分的恒曲率原语;每项都不包含自身起点。 - public IReadOnlyList Primitives { get; } -} - -/// -/// 仅依据成功节点的父索引和原语描述,确定性地重建稠密原语序列。 -/// 不复用搜索期保存的积分点,以保证输出与当前解析积分、扫掠检查规则一致。 -/// -public sealed class PathBacktracker -{ - private const double PoseComparisonTolerance = 1e-7d; - private const double LengthComparisonTolerance = 1e-7d; - private readonly MotionPrimitiveGenerator _primitiveGenerator; - - /// 创建使用默认解析积分器的回溯器。 - public PathBacktracker() - : this(new MotionPrimitiveGenerator()) - { - } - - /// 创建使用指定原语生成器的回溯器。 - public PathBacktracker(MotionPrimitiveGenerator primitiveGenerator) - { - _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator)); - } - - /// - /// 依据搜索成功节点重建从起点到终点的原语序列。 - /// 参数:searchResult 必须是携带成功节点索引的搜索结果;request 必须仍指向执行搜索的同一不可变地图和配置。 - /// 返回:父链无环、索引连续且每条原语按同一解析规则可重建时为 true;失败时 path 为 null 并返回可读原因。 - /// - public bool TryBacktrack(HybridAStarSearchResult searchResult, PlanningRequest request, - out BacktrackedPath path, out string failureReason) - { - path = null; - failureReason = string.Empty; - if (searchResult == null || request == null || searchResult.Status != PlanningStatus.Success || - !searchResult.SuccessNodeIndex.HasValue || searchResult.Nodes == null) - { - failureReason = "搜索结果不含可回溯的成功节点。"; - return false; - } - - int currentIndex = searchResult.SuccessNodeIndex.Value; - var reverseNodes = new List(); - var visited = new HashSet(); - while (currentIndex >= 0) - { - if (currentIndex >= searchResult.Nodes.Count || !visited.Add(currentIndex)) - { - failureReason = "搜索父链索引越界或存在环。"; - return false; - } - - HybridAStarNode current = searchResult.Nodes[currentIndex]; - if (current == null || current.NodeIndex != currentIndex || current.Pose == null) - { - failureReason = "搜索节点索引或连续位姿不一致。"; - return false; - } - - reverseNodes.Add(current); - currentIndex = current.ParentNodeIndex; - } - - if (reverseNodes.Count == 0) - { - failureReason = "搜索父链为空。"; - return false; - } - - reverseNodes.Reverse(); - HybridAStarNode root = reverseNodes[0]; - if (root.ParentNodeIndex != -1 || root.IncomingPrimitive != null || !IsFinitePose(root.Pose)) - { - failureReason = "搜索父链根节点无效。"; - return false; - } - - var primitives = new List(Math.Max(0, reverseNodes.Count - 1)); - Pose2D previousPose = root.Pose; - for (int index = 1; index < reverseNodes.Count; index++) - { - HybridAStarNode node = reverseNodes[index]; - MotionPrimitive descriptor = node.IncomingPrimitive; - if (node.ParentNodeIndex != reverseNodes[index - 1].NodeIndex || descriptor == null || - !IsFinitePose(node.Pose) || !NumericGuard.IsFinite(descriptor.CurvaturePerMeter) || - !IsTravelDirection(descriptor.Direction) || descriptor.ActualLengthMeters <= 0d) - { - failureReason = "搜索父链中的原语描述无效。"; - return false; - } - - // 只把恒曲率、方向和长度描述作为真源,再走一遍相同的解析积分与连续碰撞检查。 - MotionPrimitive rebuilt = _primitiveGenerator.Generate(previousPose, descriptor.CurvaturePerMeter, descriptor.Direction, request); - if (rebuilt == null || !IsEquivalent(descriptor, rebuilt) || !IsSamePose(rebuilt.End, node.Pose)) - { - failureReason = "搜索原语无法按当前解析规则确定性重建。"; - return false; - } - - primitives.Add(rebuilt); - previousPose = rebuilt.End; - } - - path = new BacktrackedPath(root.Pose, root.Direction, root.CurvaturePerMeter, - new ReadOnlyCollection(primitives)); - return true; - } - - private static bool IsEquivalent(MotionPrimitive expected, MotionPrimitive actual) - { - return expected.Direction == actual.Direction && expected.IsGoalTruncation == actual.IsGoalTruncation && - Math.Abs(expected.CurvaturePerMeter - actual.CurvaturePerMeter) <= PoseComparisonTolerance && - Math.Abs(expected.ActualLengthMeters - actual.ActualLengthMeters) <= LengthComparisonTolerance; - } - - private static bool IsSamePose(Pose2D first, Pose2D second) - { - return IsFinitePose(first) && IsFinitePose(second) && - Math.Abs(first.X - second.X) <= PoseComparisonTolerance && - Math.Abs(first.Y - second.Y) <= PoseComparisonTolerance && - Math.Abs(AngleMath.ShortestSignedDifference(first.Heading, second.Heading)) <= PoseComparisonTolerance; - } - - private static bool IsFinitePose(Pose2D pose) - { - return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/README.md b/.task8-sweep/ParkrobTrajplanner/CoarsePath/README.md deleted file mode 100644 index 730a335..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/README.md +++ /dev/null @@ -1,387 +0,0 @@ -# CoarsePath 粗路径规划(P0/P1) - -`CoarsePath` 在 `Map` 提供的不可变 `PlanningGridMap` 上执行 Hybrid A*,输出已经过连续碰撞、终点和输出不变量复核的粗路径。它只处理“能否安全地从当前车辆几何中心到达目标”的几何搜索;不读取传感器或定位,不绘制 UI,也不向底盘发送任何命令。 - -地图障碍物来源、世界坐标栅格化、距离场和缓存细节由 [Map/README.md](../Map/README.md) 说明。本模块唯一建议的业务调用入口是: - -```csharp -CoarsePathPlanningService.Plan(job, cancellationToken) -``` - -## 模块说明(Module Overview) - -| 模块 | 负责内容 | 不负责内容 | -| --- | --- | --- | -| `Map` | 外部障碍物快照、占据栅格、保守障碍距离、地图缓存 | 车辆足迹、运动原语、路径搜索、控制 | -| `CoarsePath` | 车辆扩大足迹、连续碰撞检查、前进/倒车原语、Hybrid A*、路径复核与方向分段 | 传感器读取、定位读取、速度规划、路径跟踪、底盘命令 | -| `Facade` | 将建图、搜索、取消、总预算和可选调试旁路编排为一次调用 | 修改地图内容、写入 AMR 占据、执行轨迹 | -| `Test` | 固定回归场景、P1 手动测试入口与规划结果可视化 | 真实作业地图、实时重规划或车辆控制 | - -安全余量只由 `VehicleParameters.SafetyMarginMeters` 扩大车辆矩形。它不会回写到地图障碍物,因此同一个 `PlanningGridMap` 可由不同车辆参数重复使用。 - -## 文件结构(File Structure) - -```text -CoarsePath/ -├── README.md # 本模块说明:结构、数据流、调用与测试 -├── HybridAStarPlanner.cs # 公开规划门面后的核心编排:校验、搜索、回溯和复核 -├── Contracts/ -│ ├── Pose2D.cs # 车辆几何中心位姿:m / rad -│ ├── PlanningRequest.cs # 已有 PlanningGridMap 上的一次内部搜索请求 -│ ├── PlanningResult.cs # 不可变规划结果:成功路径或空结果 -│ ├── PlanningStatus.cs # 成功、取消、无解、输入和资源限制状态 -│ ├── CoarsePathPoint.cs # 稠密路径点、方向、曲率、净空和换向标记 -│ ├── PathSegment.cs # 前进或倒车的包含式路径索引段 -│ ├── VehicleParameters.cs # 车体尺寸、安全余量和曲率限制 -│ ├── HybridAStarConfiguration.cs # 原语、离散、代价、容差和资源上限 -│ └── GoalDirectionConstraint.cs # 目标进入方向约束 -├── Vehicle/ -│ ├── VehicleKinematics.cs # 恒曲率车辆运动学积分 -│ ├── VehicleFootprint.cs # 扩大后的车辆矩形几何 -│ ├── OrientedRectangleCellIntersection.cs # 旋转矩形与占据格相交判定 -│ └── FootprintCollisionChecker.cs # 连续扫掠的足迹碰撞检查 -├── Search/ -│ ├── BinaryMinHeap.cs # 可更新优先级的 Open List -│ ├── GridDijkstraHeuristic.cs # 二维栅格可达性和距离启发式 -│ ├── GoalToleranceChecker.cs # 目标位置、航向和方向约束判定 -│ ├── MotionPrimitive.cs # 单个前进或倒车恒曲率原语 -│ ├── MotionPrimitiveGenerator.cs # 原语离散与连续积分点生成 -│ ├── SearchCostCalculator.cs # 长度、倒车、换向、曲率和净空代价 -│ ├── HybridAStarNode.cs # 搜索节点与父链信息 -│ ├── HybridAStarNodeKey.cs # 离散状态键 -│ └── HybridAStarSearch.cs # Hybrid A* 主搜索循环 -├── Output/ -│ ├── PathBacktracker.cs # 从终点节点安全回溯父链 -│ ├── CoarsePathAssembler.cs # 组装稠密路径和方向段 -│ └── CoarsePathValidator.cs # 对最终输出重新进行连续复核 -├── Facade/ -│ ├── CoarsePathPlanningJob.cs # 一次完整业务输入:地图请求、位姿、车辆、配置 -│ ├── CoarsePathPlanningJobResult.cs # 同时包含地图结果和规划结果的不可变输出 -│ ├── CoarsePathPlanningService.cs # 唯一业务调用门面 -│ ├── PlanningDebugOptions.cs # 可选调试旁路配置 -│ └── IPlanningDebugSink.cs # 调试旁路接收器契约 -└── Test/ - ├── CoarsePathScenarioFactory.cs # 六个固定场景和手动目标演示请求工厂 - └── MovementTest.CoarsePathTest.cs # 七个 Clumsy 入口、后台取消和 Painter 绘制 -``` - -## 规划数据流(Planning Data Flow) - -```text -CoarsePathPlanningJob - │ MapRequest 使用 mm;Pose2D/车辆使用 m、rad - ▼ -CoarsePathPlanningService.Plan(job, cancellationToken) - │ - ├── PlanningMapFactory.Create(job.MapRequest) - │ │ - │ ├── 失败、取消或超时 - │ │ └── MapResult + 空路径 PlanningResult,搜索不启动 - │ │ - │ └── 成功:不可变 PlanningGridMap - ▼ -PlanningRequest - │ - ▼ -HybridAStarPlanner - ├── 车辆扩大足迹与连续碰撞检查 - ├── GridDijkstraHeuristic + Hybrid A* 搜索 - ├── PathBacktracker + CoarsePathAssembler - └── CoarsePathValidator 最终复核 - ▼ -PlanningResult + MapResult - ▼ -CoarsePathPlanningJobResult -``` - -调用方只创建 `CoarsePathPlanningJob` 并消费 `CoarsePathPlanningJobResult`。`PlanningRequest`、`HybridAStarPlanner`、原语和碰撞检查器属于模块内部协作对象,不应由 UI、传感器或 MovementTest 直接拼接。 - -## 构建状态与停止(Build Status and Stop) - -必须一起处理 `MapResult` 和 `PlanningResult`。`MapResult.Status` 的类型是 `PlanningMapBuildStatus`;地图失败时,门面返回对应的空路径结果,并且不会启动 Hybrid A*。 - -| 情况 | `MapResult` | `PlanningResult` | 调用方处理 | -| --- | --- | --- | --- | -| 地图和搜索成功 | `Success` 且 `Map` 非空 | `Success`,发布完整路径和方向段 | 消费粗路径;后续模块仍需自行进行平滑、速度和控制 | -| 地图输入/来源失败 | `Failed` | `InvalidMap` | 读取 `FailureReason`,修复地图输入 | -| 调用被取消 | `Cancelled` | `Cancelled` | 不重试为普通无解;不会发布地图或部分路径 | -| 总预算耗尽 | `TimedOut` | `SearchTimeout` | 根据上层策略调整预算或稍后重试 | -| 搜索无解 | 地图成功 | `NoFeasiblePath` | 当前地图、车体和运动约束下无可行路径 | -| 节点或搜索资源受限 | 地图成功 | `SearchNodeLimitExceeded` 或 `SearchTimeout` | 读取诊断后调整配置或上层策略 | - -除 `PlanningStatus.Success` 外,`PlanningResult.Path` 与 `PlanningResult.Segments` 始终为空。取消、超时、无解、输入错误和最终复核失败都不能作为“部分可执行路径”使用。 - -### 总预算与取消 - -`HybridAStarConfiguration.SearchTimeout` 是从门面开始计时的一次总预算,依次覆盖建图、距离场、二维启发式和 Hybrid A*。同一个 `CancellationToken` 会沿调用链传递,取消优先于超时。 - -### 总耗时与路径搜索耗时 - -`PlanningDiagnostics.Elapsed` 是从 `CoarsePathPlanningService.Plan` 开始的总耗时,包含地图来源、缓存、栅格化、距离场和路径规划。`PathSearchElapsed`(路径搜索耗时)从地图和起终点预检通过后开始,包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核;搜索开始前失败时为零。 - -## 坐标与单位(Coordinates and Units) - -| 数据 | 单位 | 说明 | -| --- | --- | --- | -| `PlanningMapRequest.Bounds`、分辨率、障碍物几何 | mm | 来自 Map 的世界坐标;边界采用 `[min, max)` | -| `Pose2D.X`、`Pose2D.Y`、路径位置、车辆尺寸、安全余量、弧长 | m | CoarsePath 的连续世界坐标和长度 | -| `Pose2D.Heading`、航向容差 | rad | 核心一律使用弧度 | -| 曲率、起步曲率 | 1/m | 最大曲率或最小转弯半径至少提供一个 | -| `PlanningGridMap` 世界查询参数 | m | 越界位置按占据处理,净距为 0 | -| P1 的 AMR/手动目标 X/Y | mm | 仅在 UI 边界读取,进入核心前除以 1000 | -| P1 的 AMR/手动目标航向 | deg | 仅在 UI 边界转换为 `deg * PI / 180 -> rad` | - -起点和终点都表示车辆**几何中心**。若上游定位参考点是雷达、天线或其他安装点,必须先在上游应用安装外参;不要在 CoarsePath 内猜测偏移。车辆外扩由 `VehicleParameters.SafetyMarginMeters` 表达,不要把余量写入 Map 障碍物。 - -## 最小调用示例(Minimal Call Example) - -以下示例明确允许空图,因而只适合算法或单位演示。真实作业必须通过 `IMapObstacleSource` 提供有效障碍物快照;如何构造来源请阅读 [Map/README.md](../Map/README.md)。 - -```csharp -using System; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.Mapping; - -var service = new CoarsePathPlanningService(); // 长期持有,保留地图缓存 - -var job = new CoarsePathPlanningJob -{ - MapRequest = new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), - ResolutionMm = 50f, - ObstacleSources = Array.Empty(), - AllowExplicitEmptyMap = true, // 仅演示时明确允许 - }, - Start = new Pose2D(1d, 1d, 0d), - Goal = new Pose2D(3d, 1d, 0d), - Vehicle = new VehicleParameters - { - LengthMeters = 0.80d, - WidthMeters = 0.60d, - SafetyMarginMeters = 0.05d, - MaximumCurvaturePerMeter = 1d / 1.20d, - }, - Configuration = new HybridAStarConfiguration(), - GoalDirection = GoalDirectionConstraint.Forward, -}; - -CoarsePathPlanningJobResult result = - service.Plan(job, CancellationToken.None); - -if (!result.MapResult.Succeeded) - throw new InvalidOperationException(result.MapResult.FailureReason); - -if (result.PlanningResult.Status != PlanningStatus.Success) - throw new InvalidOperationException( - result.PlanningResult.Diagnostics.TerminationReason); - -foreach (CoarsePathPoint point in result.PlanningResult.Path) - Console.WriteLine(point.X + "," + point.Y + "," + point.Heading); -``` - -## 缓存与 SourceVersion(Cache and SourceVersion) - -`CoarsePathPlanningService` 在生命周期内长期持有 `PlanningMapFactory`,因此重复调用时能够复用地图缓存。不要每次规划都新建服务,否则会失去缓存收益。 - -| 缓存层级 | 条件 | 结果 | -| --- | --- | --- | -| `Input` | 边界、分辨率、空图策略、来源 ID、`SourceVersion`、必需性和来源结果相同 | 返回同一个不可变 `PlanningGridMap` | -| `Occupancy` | 输入版本变化,但最终占据栅格相同 | 复用占据/距离数组,生成新的快照元数据 | -| `None` | 占据内容变化 | 重建规划快照和距离场 | - -障碍来源内容改变时,调用方必须递增该来源的 `SourceVersion`。仅修改起终点、车辆、搜索配置、调试开关或调试接收器不会改变地图输入;改变障碍物却不递增版本则可能错误复用旧快照。 - -## 详细使用指南(Detailed Usage Guide) - -本节说明调用方如何从一个地图输入得到可消费的粗路径。所有业务调用都通过 `CoarsePathPlanningService.Plan(job, cancellationToken)` 完成。 - -### 第 1 步:长期持有服务 - -服务持有地图工厂和规划器,应该作为规划业务、任务执行器或上层服务的长期字段,而不是在每次调用中创建: - -```csharp -private readonly CoarsePathPlanningService _coarsePathService = - new CoarsePathPlanningService(); -``` - -### 第 2 步:准备地图请求 - -创建 `PlanningMapRequest`,其边界、分辨率和障碍物仍使用 mm。使用手工圆形/矩形或 TwoLeg 快照时,应先按 [Map/README.md](../Map/README.md) 将它们包装为 `IMapObstacleSource`,并为内容变化递增 `SourceVersion`。 - -```csharp -var mapRequest = new PlanningMapRequest -{ - Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), - ResolutionMm = 50f, - ObstacleSources = sources, - AllowExplicitEmptyMap = false, -}; -``` - -`AllowExplicitEmptyMap = true` 只在调用方明确确认空地图安全时使用。未提供有效障碍物且未显式允许空图时,地图不会进入规划。 - -### 第 3 步:填写起点、终点和方向约束 - -将车辆几何中心的世界 X/Y 从 mm 转为 m,并将航向转换为 rad 后创建 `Pose2D`。`StartDirection = null` 表示允许从前进或倒车开始;`GoalDirection` 可以限制最终进入目标的方向。 - -```csharp -var start = new Pose2D(startXmm / 1000d, startYmm / 1000d, - startHeadingDeg * Math.PI / 180d); -var goal = new Pose2D(goalXmm / 1000d, goalYmm / 1000d, - goalHeadingDeg * Math.PI / 180d); -``` - -### 第 4 步:填写车辆参数 - -车辆尺寸和安全余量全部为 m。曲率限制可填写 `MaximumCurvaturePerMeter`,或填写 `MinimumTurningRadiusMeters`;至少必须提供一个有效限制。 - -```csharp -var vehicle = new VehicleParameters -{ - LengthMeters = 0.80d, - WidthMeters = 0.60d, - SafetyMarginMeters = 0.05d, - MaximumCurvaturePerMeter = 1d / 1.20d, -}; -``` - -### 第 5 步:调整搜索配置 - -默认 `HybridAStarConfiguration` 包含原语长度、积分步长、航向离散、终点容差、代价、节点上限和总超时。若业务需要覆盖默认值,应同时理解安全影响:`MaximumCollisionCheckStepMeters` 不能以牺牲连续碰撞检查精度为代价随意增大。 - -```csharp -var configuration = new HybridAStarConfiguration -{ - SearchTimeout = TimeSpan.FromSeconds(5d), - MaximumExpandedNodes = 200000, -}; -``` - -### 第 6 步:调用并消费成功结果 - -只有 `Success` 可以发布完整路径。`Path` 是稠密点序列;`Segments` 是覆盖整条路径的前进/倒车包含式索引段,可供后续的速度规划或显示模块消费。 - -```csharp -var job = new CoarsePathPlanningJob -{ - MapRequest = mapRequest, - Start = start, - Goal = goal, - Vehicle = vehicle, - Configuration = configuration, - GoalDirection = GoalDirectionConstraint.Any, -}; - -CoarsePathPlanningJobResult result = _coarsePathService.Plan(job, cancellationToken); -if (!result.MapResult.Succeeded) - ReportMapFailure(result.MapResult.FailureReason); -else if (result.PlanningResult.Status == PlanningStatus.Success) - ConsumeCoarsePath(result.PlanningResult.Path, result.PlanningResult.Segments); -else - ReportPlanningFailure(result.PlanningResult.Diagnostics.TerminationReason); -``` - -`CoarsePathPoint.IsGearSwitchPoint` 为 `true` 表示该点是新方向段开始处。换向位置会保留一对位置、航向与弧长相同、方向不同的相邻点;`UnwrappedHeading` 用于跨越 `-pi/pi` 时保持显示连续。 - -## P1 手动测试与可视化(P1 Manual Tests and Visualization) - -P1 在 `Test/MovementTest.CoarsePathTest.cs` 提供只读测试入口。它们共用一个长期存活的 `CoarsePathPlanningService`,只提交规划并绘制结果;不发送底盘、速度或转向命令。 - -| MovementTest 名称 | 场景 | 预期 | -| --- | --- | --- | -| `粗路径规划-显式空图` | 显式允许的空图 | 前进直达成功 | -| `粗路径规划-单矩形绕行` | 中央矩形阻断直线 | 成功绕障 | -| `粗路径规划-多来源障碍` | 手工圆形、矩形和 TwoLeg 快照 | 证明多来源经过同一门面 | -| `粗路径规划-缓存命中` | 重复相同地图输入 | 后续调用显示 `Input` 缓存命中 | -| `粗路径规划-倒车换向` | 前进起步、倒车到达 | 成功路径含 `IsGearSwitchPoint` | -| `粗路径规划-无解` | 贯穿地图的障碍带 | 返回 `NoFeasiblePath` 且不发布路径 | -| `粗路径规划` | 当前 AMR 位姿、人工终点和可选人工障碍物 | 验证手动障碍物、边界、路径与可视化 | - -### 固定案例的实时 AMR 锚定 - -`粗路径规划-显式空图`、`粗路径规划-单矩形绕行`、`粗路径规划-多来源障碍`、`粗路径规划-缓存命中`、`粗路径规划-倒车换向` 和 `粗路径规划-无解` 是六个固定案例。它们不再以写死的世界起点运行:共享运行器在前台仅调用一次 `DetourInterface.getCartLocation()`,校验并冻结本次 AMR 的世界 `X(mm)`、`Y(mm)` 与航向 `deg`,再创建本次规划请求;后台规划期间不会再次读取定位。 - -`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)` - -该入口把基准案例的起点映射为冻结的 AMR 位姿,并以相同的 `ΔX/ΔY` 平移地图边界、目标、圆形/矩形障碍,以及 TwoLeg 的检测原点。因此,固定案例始终在当前 AMR 附近保留原有的相对几何关系。起点航向严格使用冻结的 AMR 航向;终点航向保持基准案例的“终点航向减起点航向”差值,叠加到当前 AMR 航向后规范化到 `[-pi, pi]`。TwoLeg 只平移检测原点,`DetectionHeadingRadians` 不会因 AMR 航向发生旋转。 - -`粗路径规划-缓存命中` 只有两次运行冻结到相同的 AMR `X/Y`、从而形成相同的平移后地图输入时,才作为缓存命中场景;AMR 位置移动后,地图输入正常未命中并重建快照。仅 AMR 航向变化不会改变固定案例的地图输入,地图缓存仍可命中。若定位读取为空、抛出异常,或 `X`、`Y`、航向含有 `NaN`/无穷值,运行器不会提交后台规划,状态与 Toast 会显示以“AMR 位姿不可用”开头的诊断原因。 - -手动 `粗路径规划` 的人工终点、障碍物和超时输入流程保持不变;它不套用固定案例的整体平移规则。 - -### AMR 位姿、手动终点与障碍物 - -`CoarsePathPlanningTest` 启动时读取一次 `DetourInterface.getCartLocation()`,将当前 AMR 世界位姿冻结为起点;随后依次输入目标世界 `X(mm)`、`Y(mm)`、航向 `deg`,以及障碍物数量 `0-20`。每个障碍物再依次输入类型和几何参数: - -| 类型输入 | 形状 | 输入参数(全部为 mm) | -| --- | --- | --- | -| `1` | 圆形 | 几何中心 `X`、`Y` 与半径;半径必须大于 0 | -| `2` | 轴对齐矩形 | 几何中心 `X`、`Y`、X 向长度、Y 向宽度;两个尺寸必须大于 0 | - -矩形只支持 `AxisAlignedRectangle`,不提供旋转角;其中心和长宽由 `ManualCoarsePathObstacle.AxisAlignedRectangle` 表达。圆形由 `ManualCoarsePathObstacle.Circle` 表达。所有无穷、NaN、非数字或不合法尺寸都会在输入阶段拒绝。 - -`CoarsePathScenarioFactory.CreateManualObstacleDemo` 在唯一边界完成转换: - -```text -AMR/目标 X、Y:mm / 1000 -> m -AMR/目标航向:deg * PI / 180 -> rad -``` - -当数量为 0 时,入口使用显式空图;`CreateManualGoalDemo` 保留为同一零障碍物场景的兼容帮助方法。数量大于 0 时,工厂将 `ManualCoarsePathObstacle` 快照封装成来源 ID 为 `manual-user-input` 的地图输入,并为每次手动快照分配新的 `SourceVersion`,避免错误复用地图缓存。规划边界覆盖起点、终点和每个障碍物的完整轮廓,再增加 8000 mm 余量并按 50 mm 对齐。 - -手动入口还要求输入一次“粗路径规划总超时”,单位为秒,只接受 `TimeSpan` 可表示范围内的有限正数秒;`0`、负数、NaN、Infinity 或溢出值都会在启动规划前拒绝。该值只覆盖本次 `CoarsePathPlanningJob.Configuration.SearchTimeout`,不会改变固定场景或全局默认值。 - -此入口使用固定演示车辆:长 `0.80 m`、宽 `0.60 m`、四周安全余量 `0.05 m`、最小转弯半径 `1.20 m`。这些值不是从现场 AMR 配置读取的,判断现场可行性前必须确认车辆参数一致。 - -`getCartLocation` 在无有效定位时可能阻塞,因此应在定位准备完成的测试环境使用。手动障碍物是测试输入,不能替代现场障碍物来源;零障碍物的显式空图也绝不代表现场不存在障碍物。 - -### 后台执行、停止与图层 - -每次测试启动时会创建独立的 `CancellationTokenSource`,以 `Task.Run` 调用门面,并先取消旧会话。`Test()` 不等待任务,也不读取 `Task.Result`;`TestStop` 取消当前令牌、使会话失效并清空图层。已取消任务完成后不会覆盖新会话,也不会显示部分路径。 - -专用世界坐标 Painter 图层为 `CoarsePathPlanningV1`。它直接读取本次 `PlanningGridMap` 的 `Bounds`、`ResolutionMm`、`SnapshotId` 和 `IsOccupied(row, col)`,因此边界、抽稀网格和占据格与实际规划快照一致,而不是重新绘制原始障碍物。 - -状态图层显示规划状态、总耗时、`PathSearchElapsed`(路径搜索耗时)、扩展/生成节点数、Open List 峰值、失败原因和固定演示车辆参数。Toast 同时显示两种耗时,并在失败时附加 `TerminationReason`,因此超时、节点上限、无解、碰撞和内部错误不会只显示成泛化失败。 - -| 颜色 | 可视化元素 | -| --- | --- | -| 灰白 | 地图边界与栅格网络 | -| 暗红 | 占据格 | -| 绿色 | 起点、前进路径和方向箭头 | -| 橙色 | 终点、航向和位置容差圈 | -| 天蓝 | 倒车路径和方向箭头 | -| 紫色 | 换向点 | -| 金色 | 已纳入安全余量的车辆检查框 | - -自动化已检查 UI 入口的后台、取消和数据来源结构。仍需在实际 Clumsy 界面手动运行“粗路径规划-单矩形绕行”和“粗路径规划”,确认图层交互显示与停止按钮效果。 - -## 常见错误(Common Errors) - -| 现象 | 原因 | 处理 | -| --- | --- | --- | -| 起点、终点或障碍物位置相差 1000 倍 | 将 mm 直接传给 `Pose2D` 或把 m 传给地图输入 | Map 输入使用 mm;`Pose2D`、车辆和路径使用 m | -| 路径朝向错误或旋转异常 | 将 P1 的 deg 直接当作核心 rad | 在 UI/上层边界执行 `deg * PI / 180`,核心只保存 rad | -| 障碍物已经变化却复用旧地图 | 内容变更后没有递增 `SourceVersion` | 每次来源快照内容变化后增加对应版本号 | -| 地图创建成功但规划被阻止 | 没有有效障碍物且未显式允许空图 | 提供有效来源;仅在确认安全的演示中设置 `AllowExplicitEmptyMap = true` | -| 无解、取消或超时后仍尝试使用路径 | 没有检查 `PlanningStatus.Success` | 仅成功时消费 `Path` 和 `Segments`;其他状态读取诊断 | -| 将粗路径直接下发给车辆 | 粗路径不包含速度、时间、执行控制或实时安全闭环 | 在后续阶段增加平滑、时间参数化、跟踪和独立安全控制 | -| P1 手动终点表现为空场地安全 | 手动入口使用显式空图演示 | 真实作业必须提供真实障碍物快照,不能复用空图语义 | - -## 第一版限制(First-Version Limits) - -当前 P0/P1 已提供安全、确定性的粗路径核心和手动结果可视化,但不包含: - -- 路径平滑或曲率连续优化; -- Reeds-Shepp 或 Dubins 精确终点连接; -- 速度、加速度、时间标注、时间轨迹和路径跟踪控制; -- 底盘命令、避障闭环、现场传感器采集或实时重规划调度; -- 横移、蟹行或其他非前进/倒车运动原语; -- 原地旋转; -- 真实作业地图接入、交互式场景编辑和 Release 性能/资源/确定性基准。 - -当前只生成汽车式恒曲率前进/倒车原语,并允许在原语边界换向;未实现的 Reeds-Shepp、横移、蟹行和原地旋转是整个粗规划核心的第一版能力边界,不是 MovementTest 单独关闭。 - -因此,调用方只能把 `Success` 结果视作后续模块的粗路径输入,不能把它当作可直接下发的时间轨迹。 diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs deleted file mode 100644 index 5e2f369..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// 为 Hybrid A* Open List 提供确定性优先级的二叉最小堆。 -/// 排序严格依次比较 F、H、较大的 G 和插入序号;F、H、G 必须为有限且非负的等效米代价。 -/// -/// 与一组搜索代价关联的节点或条目类型。 -public sealed class BinaryMinHeap -{ - private readonly List _entries = new List(); - private long _nextInsertionSequence; - - /// 创建空的确定性 Open List 堆。 - public BinaryMinHeap() - { - } - - /// 当前堆内尚未出队的条目数量。 - public int Count { get { return _entries.Count; } } - - /// - /// 将一个条目和其搜索排序代价压入堆。 - /// 参数:item 为关联条目;f、h、g 均为有限且非负的等效米代价。 - /// 失败:任一代价无效、item 为 null(仅引用类型)或插入序号耗尽时抛出异常。 - /// - public void Push(T item, double f, double h, double g) - { - if (ReferenceEquals(item, null)) throw new ArgumentNullException(nameof(item)); - ValidateCost(f, nameof(f)); - ValidateCost(h, nameof(h)); - ValidateCost(g, nameof(g)); - if (_nextInsertionSequence == long.MaxValue) - throw new InvalidOperationException("The binary heap insertion sequence has been exhausted."); - - var entry = new HeapEntry(item, f, h, g, _nextInsertionSequence); - _nextInsertionSequence++; - _entries.Add(entry); - SiftUp(_entries.Count - 1); - } - - /// - /// 弹出当前排序最优的条目。 - /// 返回:按 F、H、较大 G 和插入序号排序后的最小条目;空堆时抛出 。 - /// - public T Pop() - { - if (_entries.Count == 0) throw new InvalidOperationException("The binary heap is empty."); - - HeapEntry result = _entries[0]; - int lastIndex = _entries.Count - 1; - if (lastIndex == 0) - { - _entries.RemoveAt(0); - return result.Item; - } - - _entries[0] = _entries[lastIndex]; - _entries.RemoveAt(lastIndex); - SiftDown(0); - return result.Item; - } - - /// 清空尚未出队的条目;后续插入序号继续单调递增以保持整个实例内的确定性。 - public void Clear() - { - _entries.Clear(); - } - - private void SiftUp(int index) - { - while (index > 0) - { - int parentIndex = (index - 1) / 2; - if (Compare(_entries[index], _entries[parentIndex]) >= 0) return; - Swap(index, parentIndex); - index = parentIndex; - } - } - - private void SiftDown(int index) - { - while (true) - { - int leftChildIndex = index * 2 + 1; - if (leftChildIndex >= _entries.Count) return; - - int bestChildIndex = leftChildIndex; - int rightChildIndex = leftChildIndex + 1; - if (rightChildIndex < _entries.Count && Compare(_entries[rightChildIndex], _entries[leftChildIndex]) < 0) - bestChildIndex = rightChildIndex; - - if (Compare(_entries[bestChildIndex], _entries[index]) >= 0) return; - Swap(index, bestChildIndex); - index = bestChildIndex; - } - } - - private void Swap(int firstIndex, int secondIndex) - { - HeapEntry temporary = _entries[firstIndex]; - _entries[firstIndex] = _entries[secondIndex]; - _entries[secondIndex] = temporary; - } - - private static int Compare(HeapEntry left, HeapEntry right) - { - int comparison = left.F.CompareTo(right.F); - if (comparison != 0) return comparison; - - comparison = left.H.CompareTo(right.H); - if (comparison != 0) return comparison; - - comparison = right.G.CompareTo(left.G); - if (comparison != 0) return comparison; - - return left.InsertionSequence.CompareTo(right.InsertionSequence); - } - - private static void ValidateCost(double value, string parameterName) - { - if (!NumericGuard.IsFinite(value) || value < 0d) - throw new ArgumentOutOfRangeException(parameterName, "Search costs must be finite and non-negative."); - } - - private sealed class HeapEntry - { - public HeapEntry(T item, double f, double h, double g, long insertionSequence) - { - Item = item; - F = f; - H = h; - G = g; - InsertionSequence = insertionSequence; - } - - public T Item { get; } - public double F { get; } - public double H { get; } - public double G { get; } - public long InsertionSequence { get; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs deleted file mode 100644 index 3d18a04..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// 按位置、航向和进入方向约束判定连续位姿是否可以作为目标候选。 -public static class GoalToleranceChecker -{ - /// - /// 判断一个位姿是否满足目标容差和进入方向约束。 - /// 参数:pose 与 goal 使用世界 m/rad;configuration 提供位置 m 和航向 rad 容差;direction 为候选末段方向;goalDirection 为目标进入方向约束。 - /// 返回:输入有限、位置距离和最小环形航向误差均不超过容差且方向匹配时为 true;无效输入保守地返回 false。 - /// - public static bool IsSatisfied( - Pose2D pose, - Pose2D goal, - HybridAStarConfiguration configuration, - TravelDirection direction, - GoalDirectionConstraint goalDirection) - { - if (!IsFinitePose(pose) || !IsFinitePose(goal) || configuration == null || - !NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) || - !NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) || - configuration.GoalPositionToleranceMeters < 0d || configuration.GoalHeadingToleranceRadians < 0d || - !IsTravelDirection(direction) || !IsGoalDirection(goalDirection)) - return false; - - if (!MatchesDirection(direction, goalDirection)) return false; - - double deltaX = pose.X - goal.X; - double deltaY = pose.Y - goal.Y; - double positionDistanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - if (!NumericGuard.IsFinite(positionDistanceMeters) || positionDistanceMeters > configuration.GoalPositionToleranceMeters) - return false; - - double headingDifferenceRadians = Math.Abs(AngleMath.ShortestSignedDifference(pose.Heading, goal.Heading)); - return NumericGuard.IsFinite(headingDifferenceRadians) && headingDifferenceRadians <= configuration.GoalHeadingToleranceRadians; - } - - private static bool IsFinitePose(Pose2D pose) - { - return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } - - private static bool IsGoalDirection(GoalDirectionConstraint direction) - { - return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; - } - - private static bool MatchesDirection(TravelDirection direction, GoalDirectionConstraint constraint) - { - return constraint == GoalDirectionConstraint.Any || - (constraint == GoalDirectionConstraint.Forward && direction == TravelDirection.Forward) || - (constraint == GoalDirectionConstraint.Reverse && direction == TravelDirection.Reverse); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs deleted file mode 100644 index 509bf8a..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// 基于不可变栅格地图的目标反向八邻域 Dijkstra 启发式。 -/// 距离单位为 m;对角移动仅在两个对应正交邻格都未占据时允许,以避免从障碍夹角穿越。 -/// -public sealed class GridDijkstraHeuristic -{ - private readonly PlanningGridMap _map; - private readonly double[] _costs; - - /// - /// 从目标栅格预计算所有可达自由格到目标的二维最短距离。 - /// 参数:map 必须是已就绪的不可变地图;goalRow、goalCol 为地图内且未占据的目标格索引。 - /// 失败:地图为空、未就绪或目标格无效时抛出异常。 - /// - public GridDijkstraHeuristic(PlanningGridMap map, int goalRow, int goalCol) - { - if (!TryCreate(map, goalRow, goalCol, PlanningOperationBudget.Unlimited(CancellationToken.None), - out GridDijkstraHeuristic heuristic, out _)) - throw new InvalidOperationException("Unbounded Dijkstra construction unexpectedly stopped."); - _map = heuristic._map; - _costs = heuristic._costs; - } - - private GridDijkstraHeuristic(PlanningGridMap map, double[] costs) - { - _map = map; - _costs = costs; - } - - /// 使用共享预算创建完整二维启发式;停止时不返回部分成本数组。 - internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol, PlanningOperationBudget budget, - out GridDijkstraHeuristic heuristic, out PlanningOperationStopReason stopReason) - { - if (map == null) throw new ArgumentNullException(nameof(map)); - if (!map.PlanningReady) throw new ArgumentException("The planning map must be ready.", nameof(map)); - if (goalRow < 0 || goalRow >= map.Rows || goalCol < 0 || goalCol >= map.Cols) - throw new ArgumentOutOfRangeException(nameof(goalRow)); - if (map.IsOccupied(goalRow, goalCol)) - throw new ArgumentException("The goal grid cell must be free.", nameof(goalRow)); - if (budget == null) throw new ArgumentNullException(nameof(budget)); - - heuristic = null; - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return false; - var costs = new double[checked(map.Rows * map.Cols)]; - int workItemCount = 0; - for (int index = 0; index < costs.Length; index++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - costs[index] = double.PositiveInfinity; - } - if (!TryBuild(map, costs, goalRow, goalCol, budget, ref workItemCount, out stopReason)) return false; - heuristic = new GridDijkstraHeuristic(map, costs); - stopReason = PlanningOperationStopReason.None; - return true; - } - - /// - /// 查询指定栅格到构造时目标格的二维最短距离。 - /// 参数:row、col 为从零开始的栅格索引。 - /// 返回:单位 m 的有限最短距离;自由格不可达或索引越界时返回正无穷。 - /// - public double GetCost(int row, int col) - { - return row < 0 || row >= _map.Rows || col < 0 || col >= _map.Cols - ? double.PositiveInfinity - : _costs[row * _map.Cols + col]; - } - - private static bool TryBuild(PlanningGridMap map, double[] costs, int goalRow, int goalCol, - PlanningOperationBudget budget, ref int workItemCount, out PlanningOperationStopReason stopReason) - { - var openList = new BinaryMinHeap(); - int goalIndex = goalRow * map.Cols + goalCol; - costs[goalIndex] = 0d; - openList.Push(goalIndex, 0d, 0d, 0d); - - while (openList.Count > 0) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - int currentIndex = openList.Pop(); - int currentRow = currentIndex / map.Cols; - int currentCol = currentIndex % map.Cols; - double currentCost = costs[currentIndex]; - - for (int rowOffset = -1; rowOffset <= 1; rowOffset++) - for (int colOffset = -1; colOffset <= 1; colOffset++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - if (rowOffset == 0 && colOffset == 0) continue; - - int nextRow = currentRow + rowOffset; - int nextCol = currentCol + colOffset; - if (nextRow < 0 || nextRow >= map.Rows || nextCol < 0 || nextCol >= map.Cols || map.IsOccupied(nextRow, nextCol)) - continue; - - bool isDiagonal = rowOffset != 0 && colOffset != 0; - if (isDiagonal && (map.IsOccupied(currentRow + rowOffset, currentCol) || map.IsOccupied(currentRow, currentCol + colOffset))) - continue; - - double stepCost = isDiagonal ? Math.Sqrt(2d) * map.ResolutionMeters : map.ResolutionMeters; - double candidateCost = currentCost + stepCost; - int nextIndex = nextRow * map.Cols + nextCol; - if (candidateCost >= costs[nextIndex]) continue; - - costs[nextIndex] = candidateCost; - openList.Push(nextIndex, candidateCost, 0d, 0d); - } - } - stopReason = PlanningOperationStopReason.None; - return true; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs deleted file mode 100644 index b3ce03d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// Hybrid A* 运行期节点。 -/// 节点保留连续位姿和其离散闭集键;搜索过程中不会修改已创建节点,改进代价时会追加新节点并使旧 Open List 条目失效。 -/// -public sealed class HybridAStarNode -{ - /// - /// 创建一个搜索节点。 - /// 参数:nodeIndex 为本次搜索内稳定索引;key 为离散状态;pose 为连续车辆中心位姿;parentNodeIndex 为父节点索引,根节点使用 -1; - /// incomingPrimitive 为父节点到本节点的原语,根节点为 null;curvaturePerMeter、gCostMeters 和 hCostMeters 均使用规划器的标准单位。 - /// - public HybridAStarNode( - int nodeIndex, - HybridAStarNodeKey key, - Pose2D pose, - int parentNodeIndex, - MotionPrimitive incomingPrimitive, - double curvaturePerMeter, - double gCostMeters, - double hCostMeters) - : this(nodeIndex, key, pose, parentNodeIndex, incomingPrimitive, curvaturePerMeter, gCostMeters, hCostMeters, false) - { - } - - /// - /// 创建一个搜索节点,并显式指定它是否为原语内部命中的终点候选。 - /// 参数:isGoalCandidate 为 true 时,本节点不参与普通离散状态的最优 G 值支配;它仍必须在从 Open List 出队后重新通过连续终点和碰撞检查才能成功。 - /// - public HybridAStarNode( - int nodeIndex, - HybridAStarNodeKey key, - Pose2D pose, - int parentNodeIndex, - MotionPrimitive incomingPrimitive, - double curvaturePerMeter, - double gCostMeters, - double hCostMeters, - bool isGoalCandidate) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - if (pose == null) throw new ArgumentNullException(nameof(pose)); - - NodeIndex = nodeIndex; - Key = key; - Pose = pose; - ParentNodeIndex = parentNodeIndex; - IncomingPrimitive = incomingPrimitive; - CurvaturePerMeter = curvaturePerMeter; - GCostMeters = gCostMeters; - HCostMeters = hCostMeters; - IsGoalCandidate = isGoalCandidate; - } - - /// 本次搜索节点数组中的稳定索引。 - public int NodeIndex { get; } - - /// 本节点用于 Open/Closed 状态管理的离散键。 - public HybridAStarNodeKey Key { get; } - - /// 未量化的连续车辆中心位姿。 - public Pose2D Pose { get; } - - /// 父节点稳定索引;根节点为 -1。 - public int ParentNodeIndex { get; } - - /// 由父节点驶入本节点的已连续碰撞检查原语;根节点为 null。 - public MotionPrimitive IncomingPrimitive { get; } - - /// 含义相同的原语别名。 - public MotionPrimitive Primitive { get { return IncomingPrimitive; } } - - /// 当前末段采用的车辆曲率,单位 1/m。 - public double CurvaturePerMeter { get; } - - /// 当前节点的累计等效米代价。 - public double GCostMeters { get; } - - /// 当前节点的二维启发式等效米代价。 - public double HCostMeters { get; } - - /// - /// 本节点是否由原语内部首次满足终点条件而生成。 - /// 终点候选必须保留连续位姿,不能因同一离散键的普通低代价节点而被 Open List 准入规则压制。 - /// - public bool IsGoalCandidate { get; } - - /// Open List 排序使用的总等效米代价。 - public double FCostMeters { get { return GCostMeters + HCostMeters; } } - - /// 本节点末段的行驶方向。 - public TravelDirection Direction { get { return Key.Direction; } } - - /// 本节点末段的曲率等级索引。 - public int CurvatureLevelIndex { get { return Key.CurvatureLevelIndex; } } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs deleted file mode 100644 index e75342c..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// Hybrid A* 闭集使用的离散状态键。 -/// 位置使用地图行列索引;航向、行驶方向和曲率等级共同保留车辆运动学状态,避免把同一栅格中的不同可达姿态错误合并。 -/// -public sealed class HybridAStarNodeKey : IEquatable -{ - /// - /// 创建一个离散 Hybrid A* 状态键。 - /// 参数:row、column 为零开始的地图行列;headingIndex 为航向桶;direction 为末段行驶方向;curvatureLevelIndex 为末段曲率等级。 - /// - public HybridAStarNodeKey(int row, int column, int headingIndex, TravelDirection direction, int curvatureLevelIndex) - { - Row = row; - Column = column; - HeadingIndex = headingIndex; - Direction = direction; - CurvatureLevelIndex = curvatureLevelIndex; - } - - /// 车辆中心所在的零开始地图行索引。 - public int Row { get; } - - /// 车辆中心所在的零开始地图列索引。 - public int Column { get; } - - /// 含义相同的列索引别名。 - public int Col { get { return Column; } } - - /// 根据配置航向分辨率量化后的航向桶索引。 - public int HeadingIndex { get; } - - /// 到达当前节点的最后一段行驶方向。 - public TravelDirection Direction { get; } - - /// 到达当前节点的最后一段曲率等级索引。 - public int CurvatureLevelIndex { get; } - - /// 判断另一个键是否表示完全相同的离散搜索状态。 - public bool Equals(HybridAStarNodeKey other) - { - return other != null && Row == other.Row && Column == other.Column && HeadingIndex == other.HeadingIndex && - Direction == other.Direction && CurvatureLevelIndex == other.CurvatureLevelIndex; - } - - /// 判断另一个对象是否表示完全相同的离散搜索状态。 - public override bool Equals(object obj) - { - return Equals(obj as HybridAStarNodeKey); - } - - /// 返回用于闭集字典的稳定哈希值。 - public override int GetHashCode() - { - unchecked - { - int hashCode = Row; - hashCode = hashCode * 397 ^ Column; - hashCode = hashCode * 397 ^ HeadingIndex; - hashCode = hashCode * 397 ^ (int)Direction; - return hashCode * 397 ^ CurvatureLevelIndex; - } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs deleted file mode 100644 index cf05967..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs +++ /dev/null @@ -1,542 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// 单次 Hybrid A* 搜索的只读结果。 -/// 即使失败也会保留已经创建的运行期节点,供上层记录诊断;仅 有值。 -/// -public sealed class HybridAStarSearchResult -{ - internal HybridAStarSearchResult( - PlanningStatus status, - IEnumerable nodes, - int expandedNodeCount, - int generatedNodeCount, - int reopenedNodeCount, - int staleOpenListEntryCount, - int peakOpenListCount, - int? successNodeIndex, - string terminationReason) - { - Status = status; - Nodes = new ReadOnlyCollection(new List(nodes ?? Array.Empty())); - ExpandedNodeCount = expandedNodeCount; - GeneratedNodeCount = generatedNodeCount; - ReopenedNodeCount = reopenedNodeCount; - StaleOpenListEntryCount = staleOpenListEntryCount; - PeakOpenListCount = peakOpenListCount; - SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null; - TerminationReason = status == PlanningStatus.Success ? string.Empty : terminationReason ?? string.Empty; - } - - /// 搜索终止状态。 - public PlanningStatus Status { get; } - - /// 本次搜索已经创建的全部节点;索引与 一致。 - public IReadOnlyList Nodes { get; } - - /// 实际从 Open List 弹出并扩展的节点数量。 - public int ExpandedNodeCount { get; } - - /// 已进入 Open List 的节点数量,包含根节点和因改进代价追加的节点。 - public int GeneratedNodeCount { get; } - - /// 更优路径到达已关闭离散状态、并重新放回 Open List 的次数。 - public int ReopenedNodeCount { get; } - - /// 从 Open List 弹出后因已有更优普通状态而被丢弃的陈旧条目数量。 - public int StaleOpenListEntryCount { get; } - - /// 搜索期间 Open List 持有的最大条目数,包含等待惰性丢弃的旧条目。 - public int PeakOpenListCount { get; } - - /// 成功时最后一个从 Open List 弹出且满足终点条件的节点索引;失败时为 null。 - public int? SuccessNodeIndex { get; } - - /// 搜索边界记录的原始终止原因;成功时为空字符串,失败时非空。 - public string TerminationReason { get; } -} - -/// -/// 在不可变 上执行前进/倒车恒曲率原语的 Hybrid A* 搜索。 -/// 搜索只消费已准备好的地图快照;所有候选原语均先完成连续扩大车体碰撞检查,再参与 Open List 排序。 -/// -public sealed class HybridAStarSearch -{ - private const double CostImprovementToleranceMeters = 1e-9d; - private readonly MotionPrimitiveGenerator _primitiveGenerator; - private readonly SearchCostCalculator _costCalculator; - private readonly FootprintCollisionChecker _collisionChecker; - - /// 创建使用默认原语、代价和碰撞检查实现的搜索器。 - public HybridAStarSearch() - : this(new MotionPrimitiveGenerator(), new SearchCostCalculator(), new FootprintCollisionChecker()) - { - } - - /// 创建使用指定协作对象的搜索器,便于在不引入地图或 UI 依赖的情况下测试搜索过程。 - public HybridAStarSearch( - MotionPrimitiveGenerator primitiveGenerator, - SearchCostCalculator costCalculator, - FootprintCollisionChecker collisionChecker) - { - _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator)); - _costCalculator = costCalculator ?? throw new ArgumentNullException(nameof(costCalculator)); - _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); - } - - /// - /// 执行一次不可变地图上的 Hybrid A* 搜索。 - /// 参数:request 提供地图、起终点、车辆和搜索配置;cancellationToken 在每次节点扩展前检查。 - /// 返回:成功仅在满足目标约束的候选已从 Open List 弹出时报告;任一失败状态均不报告成功节点。 - /// - public HybridAStarSearchResult Search(PlanningRequest request, CancellationToken cancellationToken) - { - TimeSpan timeout = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero - ? request.Configuration.SearchTimeout - : TimeSpan.Zero; - PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero - ? new PlanningOperationBudget(cancellationToken, timeout) - : PlanningOperationBudget.Unlimited(cancellationToken); - return Search(request, budget); - } - - /// 使用门面传入的共享预算执行搜索;预算从整次规划开始计时。 - internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget) - { - var nodes = new List(); - int expandedNodeCount = 0; - int generatedNodeCount = 0; - int reopenedNodeCount = 0; - int staleOpenListEntryCount = 0; - int peakOpenListCount = 0; - - try - { - if (budget == null) throw new ArgumentNullException(nameof(budget)); - PlanningOperationStopReason stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) - return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - PlanningStatus validationStatus = ValidateRequest(request); - if (validationStatus != PlanningStatus.Success) - return CreateResult(validationStatus, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - PlanningGridMap map = request.Map; - HybridAStarConfiguration configuration = request.Configuration; - VehicleParameters vehicle = request.Vehicle; - - if (!IsFootprintInsideMap(request.Start, map, vehicle)) - return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (!_collisionChecker.IsPoseCollisionFree(request.Start, map, vehicle, 0d, out _)) - return CreateResult(PlanningStatus.StartInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (!IsFootprintInsideMap(request.Goal, map, vehicle)) - return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (!_collisionChecker.IsPoseCollisionFree(request.Goal, map, vehicle, 0d, out _)) - return CreateResult(PlanningStatus.GoalInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) - return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (configuration.MaximumExpandedNodes == 0) - return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - if (!map.TryWorldToGrid(request.Goal.X, request.Goal.Y, out int goalRow, out int goalColumn)) - return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (!GridDijkstraHeuristic.TryCreate(map, goalRow, goalColumn, budget, out GridDijkstraHeuristic heuristic, out stopReason)) - return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) - return CreateResult(PlanningStatus.InvalidVehicleParameters, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - IReadOnlyList curvatureLevels = _primitiveGenerator.GetCurvatureLevels(vehicle, configuration); - if (curvatureLevels.Count == 0) - return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - int headingBinCount = GetHeadingBinCount(configuration.HeadingResolutionRadians); - int startCurvatureLevelIndex = GetNearestCurvatureLevelIndex(curvatureLevels, request.StartVehicleCurvature); - if (startCurvatureLevelIndex < 0) - return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - if (!map.TryWorldToGrid(request.Start.X, request.Start.Y, out int startRow, out int startColumn)) - return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - var openList = new BinaryMinHeap(); - var bestStates = new Dictionary(); - foreach (TravelDirection startDirection in GetStartDirections(request, configuration)) - { - int headingIndex = AngleMath.ToHeadingIndex(request.Start.Heading, configuration.HeadingResolutionRadians, headingBinCount); - var key = new HybridAStarNodeKey(startRow, startColumn, headingIndex, startDirection, startCurvatureLevelIndex); - if (bestStates.ContainsKey(key)) continue; - - double hCostMeters = GetHeuristicCost(heuristic, startRow, startColumn, configuration); - if (double.IsPositiveInfinity(hCostMeters)) continue; - var node = new HybridAStarNode(nodes.Count, key, request.Start, -1, null, - curvatureLevels[startCurvatureLevelIndex], 0d, hCostMeters); - nodes.Add(node); - bestStates.Add(key, new NodeLabel(node.NodeIndex, 0d, false)); - openList.Push(node.NodeIndex, node.FCostMeters, node.HCostMeters, node.GCostMeters); - peakOpenListCount = Math.Max(peakOpenListCount, openList.Count); - generatedNodeCount++; - } - - if (openList.Count == 0) - return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null, - "二维启发式标记起点不可达目标,或起始方向无法进入 Open List。"); - - while (openList.Count > 0) - { - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) - return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (expandedNodeCount >= configuration.MaximumExpandedNodes) - return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - - int nodeIndex = openList.Pop(); - HybridAStarNode current = nodes[nodeIndex]; - NodeLabel currentLabel = null; - if (!current.IsGoalCandidate) - { - if (!bestStates.TryGetValue(current.Key, out currentLabel) || currentLabel.NodeIndex != nodeIndex || currentLabel.IsClosed) - { - staleOpenListEntryCount++; - continue; - } - - currentLabel.IsClosed = true; - } - - expandedNodeCount++; - if (current.IsGoalCandidate) - { - if (!GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection) || - !_collisionChecker.IsPoseCollisionFree(current.Pose, map, vehicle, 0d, out _)) - continue; - - return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, current.NodeIndex); - } - - if (GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection)) - return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, current.NodeIndex); - - for (int curvatureLevelIndex = 0; curvatureLevelIndex < curvatureLevels.Count; curvatureLevelIndex++) - { - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) - return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null); - if (!MotionPrimitiveGenerator.AreCurvatureLevelsAdjacent(current.CurvatureLevelIndex, curvatureLevelIndex)) continue; - - foreach (TravelDirection direction in GetSuccessorDirections(configuration)) - { - MotionPrimitive primitive = _primitiveGenerator.Generate(current.Pose, curvatureLevels[curvatureLevelIndex], direction, request); - if (primitive == null || primitive.ActualLengthMeters <= 0d) continue; - - if (!map.TryWorldToGrid(primitive.End.X, primitive.End.Y, out int row, out int column)) continue; - double hCostMeters = GetHeuristicCost(heuristic, row, column, configuration); - if (double.IsPositiveInfinity(hCostMeters)) continue; - - double bodyClearanceMeters = GetMinimumBodyClearance(primitive); - bool isGearSwitch = current.ParentNodeIndex >= 0 && current.Direction != direction; - int curvatureLevelDelta = curvatureLevelIndex - current.CurvatureLevelIndex; - double incrementalCostMeters = _costCalculator.Calculate( - primitive.ActualLengthMeters, - direction, - isGearSwitch, - curvatureLevels[curvatureLevelIndex], - maximumCurvaturePerMeter, - curvatureLevelDelta, - bodyClearanceMeters, - configuration); - double gCostMeters = current.GCostMeters + incrementalCostMeters; - if (!NumericGuard.IsFinite(gCostMeters) || gCostMeters < 0d) continue; - - int headingIndex = AngleMath.ToHeadingIndex(primitive.End.Heading, configuration.HeadingResolutionRadians, headingBinCount); - var key = new HybridAStarNodeKey(row, column, headingIndex, direction, curvatureLevelIndex); - bestStates.TryGetValue(key, out NodeLabel existingLabel); - var successor = new HybridAStarNode(nodes.Count, key, primitive.End, current.NodeIndex, primitive, - curvatureLevels[curvatureLevelIndex], gCostMeters, hCostMeters, primitive.IsGoalTruncation); - if (existingLabel != null && !ShouldEnqueueSuccessor(successor, existingLabel.GCostMeters)) continue; - - if (!successor.IsGoalCandidate && existingLabel != null && existingLabel.IsClosed) reopenedNodeCount++; - nodes.Add(successor); - if (!successor.IsGoalCandidate) - bestStates[key] = new NodeLabel(successor.NodeIndex, gCostMeters, false); - openList.Push(successor.NodeIndex, successor.FCostMeters, successor.HCostMeters, successor.GCostMeters); - peakOpenListCount = Math.Max(peakOpenListCount, openList.Count); - generatedNodeCount++; - } - } - } - - return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null, - "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"); - } - catch (Exception exception) - { - string reason = "Hybrid A* 搜索内部错误:" + exception.GetType().Name + - (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); - return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, null, reason); - } - } - - private static HybridAStarSearchResult CreateResult( - PlanningStatus status, - IEnumerable nodes, - int expandedNodeCount, - int generatedNodeCount, - int reopenedNodeCount, - int staleOpenListEntryCount, - int peakOpenListCount, - int? successNodeIndex, - string terminationReason = null) - { - string reason = status == PlanningStatus.Success - ? string.Empty - : terminationReason ?? GetDefaultTerminationReason(status); - return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason); - } - - private static string GetDefaultTerminationReason(PlanningStatus status) - { - switch (status) - { - case PlanningStatus.Cancelled: - return "Hybrid A* 搜索已取消。"; - case PlanningStatus.InvalidRequest: - return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。"; - case PlanningStatus.InvalidMap: - return "Hybrid A* 搜索地图结构无效。"; - case PlanningStatus.MapNotReady: - return "Hybrid A* 搜索地图尚未准备好。"; - case PlanningStatus.InvalidVehicleParameters: - return "Hybrid A* 搜索车辆参数无效。"; - case PlanningStatus.InvalidCurvatureConfiguration: - return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。"; - case PlanningStatus.StartOutsideMap: - return "Hybrid A* 搜索起始扩大车体不完全位于地图内。"; - case PlanningStatus.StartInCollision: - return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。"; - case PlanningStatus.GoalOutsideMap: - return "Hybrid A* 搜索目标扩大车体不完全位于地图内。"; - case PlanningStatus.GoalInCollision: - return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。"; - case PlanningStatus.SearchTimeout: - return "Hybrid A* 搜索使用的总规划预算已耗尽。"; - case PlanningStatus.SearchNodeLimitExceeded: - return "Hybrid A* 搜索达到扩展节点上限。"; - case PlanningStatus.NoFeasiblePath: - return "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"; - case PlanningStatus.BacktrackingFailed: - return "Hybrid A* 成功节点无法回溯为完整父链。"; - case PlanningStatus.FinalValidationFailed: - return "Hybrid A* 路径未通过最终复核。"; - case PlanningStatus.InternalError: - return "Hybrid A* 搜索发生未预期内部错误。"; - default: - return "Hybrid A* 搜索以未识别状态终止:" + status + "。"; - } - } - - private static PlanningStatus ValidateRequest(PlanningRequest request) - { - if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || - !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || - !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) - return PlanningStatus.InvalidRequest; - - PlanningGridMap map = request.Map; - if (map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || map.Bounds == null) - return PlanningStatus.InvalidMap; - if (!map.PlanningReady) return PlanningStatus.MapNotReady; - - VehicleParameters vehicle = request.Vehicle; - if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || - !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) - return PlanningStatus.InvalidVehicleParameters; - - HybridAStarConfiguration configuration = request.Configuration; - if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || - (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) - return PlanningStatus.InvalidCurvatureConfiguration; - - return PlanningStatus.Success; - } - - private static bool IsValidConfiguration(HybridAStarConfiguration configuration) - { - return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && - NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && - NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && - NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && - configuration.HeadingResolutionRadians <= 2d * Math.PI && - configuration.CurvatureLevelCount >= 3 && configuration.CurvatureLevelCount % 2 == 1 && - NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && configuration.GoalPositionToleranceMeters >= 0d && - NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && configuration.GoalHeadingToleranceRadians >= 0d && - configuration.MaximumExpandedNodes >= 0 && configuration.SearchTimeout >= TimeSpan.Zero && - NumericGuard.IsFinite(configuration.HeuristicWeight) && configuration.HeuristicWeight >= 0d && - NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && - NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && - NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && - NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && - NumericGuard.IsFinite(configuration.ClearanceCostWeight) && configuration.ClearanceCostWeight >= 0d && - NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); - } - - private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) - { - if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; - - double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; - double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; - double longitudinalX = Math.Cos(pose.Heading); - double longitudinalY = Math.Sin(pose.Heading); - double lateralX = -longitudinalY; - double lateralY = longitudinalX; - for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) - for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) - { - double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; - double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; - if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; - } - return true; - } - - private static IEnumerable GetStartDirections(PlanningRequest request, HybridAStarConfiguration configuration) - { - if (request.StartDirection.HasValue) - { - yield return request.StartDirection.Value; - yield break; - } - - yield return TravelDirection.Forward; - if (configuration.AllowReverse) yield return TravelDirection.Reverse; - } - - private static IEnumerable GetSuccessorDirections(HybridAStarConfiguration configuration) - { - yield return TravelDirection.Forward; - if (configuration.AllowReverse) yield return TravelDirection.Reverse; - } - - private static int GetHeadingBinCount(double headingResolutionRadians) - { - double rawBinCount = Math.Ceiling(2d * Math.PI / headingResolutionRadians); - if (!NumericGuard.IsFinite(rawBinCount) || rawBinCount < 1d || rawBinCount > int.MaxValue) - throw new ArgumentOutOfRangeException(nameof(headingResolutionRadians)); - return (int)rawBinCount; - } - - private static int GetNearestCurvatureLevelIndex(IReadOnlyList curvatureLevels, double curvaturePerMeter) - { - int bestIndex = -1; - double smallestDifference = double.PositiveInfinity; - for (int index = 0; index < curvatureLevels.Count; index++) - { - double difference = Math.Abs(curvatureLevels[index] - curvaturePerMeter); - if (difference >= smallestDifference) continue; - smallestDifference = difference; - bestIndex = index; - } - return bestIndex; - } - - private static double GetMinimumBodyClearance(MotionPrimitive primitive) - { - double minimum = double.PositiveInfinity; - foreach (double clearanceMeters in primitive.BodyClearancesMeters) - { - if (double.IsNaN(clearanceMeters) || clearanceMeters < 0d) return 0d; - minimum = Math.Min(minimum, clearanceMeters); - } - return minimum; - } - - private static double GetHeuristicCost(GridDijkstraHeuristic heuristic, int row, int column, HybridAStarConfiguration configuration) - { - double gridCostMeters = heuristic.GetCost(row, column); - if (double.IsPositiveInfinity(gridCostMeters)) return gridCostMeters; - double weightedCostMeters = gridCostMeters * configuration.HeuristicWeight; - return NumericGuard.IsFinite(weightedCostMeters) && weightedCostMeters >= 0d - ? weightedCostMeters - : double.PositiveInfinity; - } - - /// - /// 判断后继是否应进入 Open List。 - /// 参数:successor 是已完成连续碰撞检查的后继;bestKnownGCostMeters 是相同离散键普通状态的当前最优 G 值。 - /// 返回:普通状态仅在严格改善最优 G 值时入队;终点候选始终入队,以保留其未量化的连续终点位姿。 - /// - private static bool ShouldEnqueueSuccessor(HybridAStarNode successor, double bestKnownGCostMeters) - { - if (successor == null || !NumericGuard.IsFinite(bestKnownGCostMeters)) return false; - return successor.IsGoalCandidate || successor.GCostMeters < bestKnownGCostMeters - CostImprovementToleranceMeters; - } - - private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) - { - if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; - if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; - throw new ArgumentOutOfRangeException(nameof(stopReason)); - } - - private static bool IsFinitePose(Pose2D pose) - { - return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } - - private static bool IsGoalDirection(GoalDirectionConstraint direction) - { - return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; - } - - private sealed class NodeLabel - { - public NodeLabel(int nodeIndex, double gCostMeters, bool isClosed) - { - NodeIndex = nodeIndex; - GCostMeters = gCostMeters; - IsClosed = isClosed; - } - - public int NodeIndex { get; } - public double GCostMeters { get; } - public bool IsClosed { get; set; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs deleted file mode 100644 index fba41ed..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// 一条经连续碰撞检查的恒曲率运动原语。 -/// 位置和长度单位为 m,航向单位为 rad,曲率单位为 1/m; 不包含起点,只包含按积分顺序产生的后续点。 -/// -public sealed class MotionPrimitive -{ - /// - /// 创建不可变运动原语。 - /// 参数:start 为原语起点;direction 为行驶方向;curvaturePerMeter 为恒定曲率;actualLengthMeters 为已实际行驶长度; - /// points 与 bodyClearancesMeters 按同一索引保存内部积分位姿和对应的车体净空;isGoalTruncation 表示末点是否首次命中目标容差。 - /// - public MotionPrimitive( - Pose2D start, - TravelDirection direction, - double curvaturePerMeter, - double actualLengthMeters, - IEnumerable points, - IEnumerable bodyClearancesMeters, - bool isGoalTruncation) - { - if (start == null) throw new ArgumentNullException(nameof(start)); - if (points == null) throw new ArgumentNullException(nameof(points)); - if (bodyClearancesMeters == null) throw new ArgumentNullException(nameof(bodyClearancesMeters)); - - var copiedPoints = new List(points); - var copiedClearances = new List(bodyClearancesMeters); - if (copiedPoints.Count != copiedClearances.Count) - throw new ArgumentException("The point and clearance counts must match.", nameof(bodyClearancesMeters)); - - Start = start; - Direction = direction; - CurvaturePerMeter = curvaturePerMeter; - ActualLengthMeters = actualLengthMeters; - Points = new ReadOnlyCollection(copiedPoints); - BodyClearancesMeters = new ReadOnlyCollection(copiedClearances); - End = copiedPoints.Count == 0 ? start : copiedPoints[copiedPoints.Count - 1]; - IsGoalTruncation = isGoalTruncation; - } - - /// 原语起始车辆几何中心位姿。 - public Pose2D Start { get; } - - /// 原语最后一个积分点;零长度终点候选时等于 - public Pose2D End { get; } - - /// 原语对应的行驶方向。 - public TravelDirection Direction { get; } - - /// 原语全程采用的恒定车辆曲率,单位 1/m。 - public double CurvaturePerMeter { get; } - - /// 的实际行驶弧长,单位 m。 - public double ActualLengthMeters { get; } - - /// 不含起点的连续积分位姿,只读且按行驶顺序排列。 - public IReadOnlyList Points { get; } - - /// 一一对应的扩大车体保守净空下界,单位 m。 - public IReadOnlyList BodyClearancesMeters { get; } - - /// 末点是否因首次满足目标位置、航向和方向约束而截断。 - public bool IsGoalTruncation { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs deleted file mode 100644 index 817cc58..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// 生成经解析积分和连续碰撞检查的前进或倒车恒曲率原语。 -/// 每个积分点均依次经过有限数值检查、与前一点之间的扫掠碰撞检查和终点容差检查。 -/// -public sealed class MotionPrimitiveGenerator -{ - private const double StraightCurvatureThreshold = 1e-12d; - private readonly FootprintCollisionChecker _collisionChecker; - - /// 创建使用默认车辆连续碰撞检查器的原语生成器。 - public MotionPrimitiveGenerator() - : this(new FootprintCollisionChecker()) - { - } - - /// 创建使用指定连续车辆碰撞检查器的原语生成器。 - public MotionPrimitiveGenerator(FootprintCollisionChecker collisionChecker) - { - _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); - } - - /// - /// 使用完整规划请求生成一条原语。 - /// 参数:start 为当前连续位姿;curvaturePerMeter 为候选恒定曲率;direction 为前进或倒车;request 提供地图、车辆、配置和目标。 - /// 返回:输入无效、曲率超限或任一积分点碰撞时为 null;否则返回最大长度不超过配置上限的原语。 - /// - public MotionPrimitive Generate(Pose2D start, double curvaturePerMeter, TravelDirection direction, PlanningRequest request) - { - if (request == null) return null; - return Generate(start, curvaturePerMeter, direction, request.Map, request.Vehicle, request.Configuration, request.Goal, request.GoalDirection); - } - - /// - /// 使用显式地图、车辆、配置和目标生成一条原语。 - /// 参数:所有位置使用 m/rad,curvaturePerMeter 使用 1/m;goalDirection 限制末段允许的进入方向。 - /// 返回:输入无效、曲率超限或任一积分点碰撞时为 null;首次命中目标时返回 为 true 的截断原语。 - /// - public MotionPrimitive Generate( - Pose2D start, - double curvaturePerMeter, - TravelDirection direction, - PlanningGridMap map, - VehicleParameters vehicle, - HybridAStarConfiguration configuration, - Pose2D goal, - GoalDirectionConstraint goalDirection) - { - if (!IsValidInput(start, curvaturePerMeter, direction, map, vehicle, configuration, goal, goalDirection)) return null; - - if (GoalToleranceChecker.IsSatisfied(start, goal, configuration, direction, goalDirection)) - return new MotionPrimitive(start, direction, curvaturePerMeter, 0d, Array.Empty(), Array.Empty(), true); - - double pointStepMeters = Math.Min(configuration.IntegrationStepMeters, - Math.Min(configuration.MaximumCollisionCheckStepMeters, map.ResolutionMeters / 2d)); - if (!NumericGuard.IsPositiveFinite(pointStepMeters)) return null; - - var points = new List(); - var bodyClearancesMeters = new List(); - Pose2D previous = start; - double actualLengthMeters = 0d; - while (actualLengthMeters < configuration.PrimitiveLengthMeters) - { - double remainingLengthMeters = configuration.PrimitiveLengthMeters - actualLengthMeters; - double stepMeters = Math.Min(pointStepMeters, remainingLengthMeters); - if (!NumericGuard.IsPositiveFinite(stepMeters)) return null; - - Pose2D next = Integrate(previous, curvaturePerMeter, direction, stepMeters); - if (!IsFinitePose(next)) return null; - if (!_collisionChecker.IsSweptMotionCollisionFree(previous, next, map, vehicle, stepMeters, out double bodyClearanceMeters)) return null; - - actualLengthMeters += stepMeters; - points.Add(next); - bodyClearancesMeters.Add(bodyClearanceMeters); - if (GoalToleranceChecker.IsSatisfied(next, goal, configuration, direction, goalDirection)) - return new MotionPrimitive(start, direction, curvaturePerMeter, actualLengthMeters, points, bodyClearancesMeters, true); - - previous = next; - } - - return new MotionPrimitive(start, direction, curvaturePerMeter, actualLengthMeters, points, bodyClearancesMeters, false); - } - - /// - /// 获取从最大负曲率到最大正曲率均匀分布的候选曲率等级。 - /// 参数:vehicle 提供保守最大曲率;configuration 的曲率等级数必须为不小于 3 的奇数。 - /// 返回:输入无效时为空只读列表;有效时长度等于配置等级数且中间等级恒为零曲率。 - /// - public IReadOnlyList GetCurvatureLevels(VehicleParameters vehicle, HybridAStarConfiguration configuration) - { - if (configuration == null || configuration.CurvatureLevelCount < 3 || configuration.CurvatureLevelCount % 2 == 0 || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) - return Array.Empty(); - - var levels = new double[configuration.CurvatureLevelCount]; - double increment = 2d * maximumCurvaturePerMeter / (levels.Length - 1d); - for (int index = 0; index < levels.Length; index++) levels[index] = -maximumCurvaturePerMeter + increment * index; - levels[levels.Length / 2] = 0d; - return levels; - } - - /// - /// 判断两个曲率等级是否允许在相邻原语间直接切换。 - /// 参数:previousLevelIndex 与 nextLevelIndex 为从零开始的等级索引。 - /// 返回:两个索引均非负且最多相差一个等级时为 true。 - /// - public static bool AreCurvatureLevelsAdjacent(int previousLevelIndex, int nextLevelIndex) - { - return previousLevelIndex >= 0 && nextLevelIndex >= 0 && Math.Abs(previousLevelIndex - nextLevelIndex) <= 1; - } - - private static bool IsValidInput( - Pose2D start, - double curvaturePerMeter, - TravelDirection direction, - PlanningGridMap map, - VehicleParameters vehicle, - HybridAStarConfiguration configuration, - Pose2D goal, - GoalDirectionConstraint goalDirection) - { - if (!IsFinitePose(start) || !IsFinitePose(goal) || map == null || vehicle == null || configuration == null || - !NumericGuard.IsFinite(curvaturePerMeter) || !IsTravelDirection(direction) || !IsGoalDirection(goalDirection) || - !NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) || - !NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) || - !NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || - !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) - return false; - - return Math.Abs(curvaturePerMeter) <= maximumCurvaturePerMeter; - } - - private static Pose2D Integrate(Pose2D previous, double curvaturePerMeter, TravelDirection direction, double stepMeters) - { - double signedDistanceMeters = direction == TravelDirection.Forward ? stepMeters : -stepMeters; - double nextHeadingRadians = AngleMath.NormalizeRadians(previous.Heading + curvaturePerMeter * signedDistanceMeters); - if (Math.Abs(curvaturePerMeter) < StraightCurvatureThreshold) - { - return new Pose2D( - previous.X + signedDistanceMeters * Math.Cos(previous.Heading), - previous.Y + signedDistanceMeters * Math.Sin(previous.Heading), - nextHeadingRadians); - } - - return new Pose2D( - previous.X + (Math.Sin(nextHeadingRadians) - Math.Sin(previous.Heading)) / curvaturePerMeter, - previous.Y - (Math.Cos(nextHeadingRadians) - Math.Cos(previous.Heading)) / curvaturePerMeter, - nextHeadingRadians); - } - - private static bool IsFinitePose(Pose2D pose) - { - return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); - } - - private static bool IsTravelDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } - - private static bool IsGoalDirection(GoalDirectionConstraint direction) - { - return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs deleted file mode 100644 index 00babd0..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; - -/// -/// 将运动原语的长度、方向、曲率和净空转换为统一的等效米搜索代价。 -/// 此类型不修改搜索状态;换向标记必须由调用方依据相邻原语的方向关系提供。 -/// -public sealed class SearchCostCalculator -{ - /// 创建使用调用时配置参数的等效米代价计算器。 - public SearchCostCalculator() - { - } - - /// - /// 计算一条运动原语的等效米增量代价。 - /// 参数:lengthMeters 为非负弧长 m;direction 为原语方向;isGearSwitch 表示该原语前是否换向; - /// curvaturePerMeter 与 maximumCurvaturePerMeter 的单位为 1/m;curvatureLevelDelta 为相邻曲率等级差; - /// bodyClearanceMeters 为非负车体保守净空 m,可为正无穷;configuration 提供非负权重和惩罚。 - /// 返回:有限且非负的等效米代价。 - /// 失败:任一数值、方向或权重无效时抛出 。 - /// - public double Calculate( - double lengthMeters, - TravelDirection direction, - bool isGearSwitch, - double curvaturePerMeter, - double maximumCurvaturePerMeter, - int curvatureLevelDelta, - double bodyClearanceMeters, - HybridAStarConfiguration configuration) - { - ValidateInput(lengthMeters, direction, curvaturePerMeter, maximumCurvaturePerMeter, bodyClearanceMeters, configuration); - - double directionMultiplier = direction == TravelDirection.Reverse ? configuration.ReverseCostMultiplier : 1d; - double normalizedCurvature = Math.Abs(curvaturePerMeter / maximumCurvaturePerMeter); - double clearanceDeficit = GetClearanceDeficit(bodyClearanceMeters, configuration.ClearanceCostDistanceMeters); - double levelDelta = Math.Abs((double)curvatureLevelDelta); - double motionCost = lengthMeters * directionMultiplier * - (1d + configuration.CurvatureMagnitudeWeight * normalizedCurvature + - configuration.ClearanceCostWeight * clearanceDeficit); - double gearSwitchCost = isGearSwitch ? configuration.GearSwitchPenaltyMeters : 0d; - double curvatureChangeCost = configuration.CurvatureChangePenaltyMetersPerLevel * levelDelta; - double totalCost = motionCost + gearSwitchCost + curvatureChangeCost; - if (!NumericGuard.IsFinite(totalCost) || totalCost < 0d) - throw new ArgumentOutOfRangeException(nameof(lengthMeters), "The calculated cost must remain finite and non-negative."); - - return totalCost; - } - - private static void ValidateInput( - double lengthMeters, - TravelDirection direction, - double curvaturePerMeter, - double maximumCurvaturePerMeter, - double bodyClearanceMeters, - HybridAStarConfiguration configuration) - { - if (!NumericGuard.IsFinite(lengthMeters) || lengthMeters < 0d) - throw new ArgumentOutOfRangeException(nameof(lengthMeters)); - if (direction != TravelDirection.Forward && direction != TravelDirection.Reverse) - throw new ArgumentOutOfRangeException(nameof(direction)); - if (!NumericGuard.IsFinite(curvaturePerMeter)) - throw new ArgumentOutOfRangeException(nameof(curvaturePerMeter)); - if (!NumericGuard.IsPositiveFinite(maximumCurvaturePerMeter)) - throw new ArgumentOutOfRangeException(nameof(maximumCurvaturePerMeter)); - if (Math.Abs(curvaturePerMeter) > maximumCurvaturePerMeter) - throw new ArgumentOutOfRangeException(nameof(curvaturePerMeter)); - if (double.IsNaN(bodyClearanceMeters) || bodyClearanceMeters < 0d) - throw new ArgumentOutOfRangeException(nameof(bodyClearanceMeters)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - ValidateNonNegativeFinite(configuration.HeuristicWeight, nameof(configuration.HeuristicWeight)); - if (!NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier)) - throw new ArgumentOutOfRangeException(nameof(configuration.ReverseCostMultiplier)); - ValidateNonNegativeFinite(configuration.GearSwitchPenaltyMeters, nameof(configuration.GearSwitchPenaltyMeters)); - ValidateNonNegativeFinite(configuration.CurvatureMagnitudeWeight, nameof(configuration.CurvatureMagnitudeWeight)); - ValidateNonNegativeFinite(configuration.CurvatureChangePenaltyMetersPerLevel, nameof(configuration.CurvatureChangePenaltyMetersPerLevel)); - ValidateNonNegativeFinite(configuration.ClearanceCostWeight, nameof(configuration.ClearanceCostWeight)); - if (!NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters)) - throw new ArgumentOutOfRangeException(nameof(configuration.ClearanceCostDistanceMeters)); - } - - private static void ValidateNonNegativeFinite(double value, string parameterName) - { - if (!NumericGuard.IsFinite(value) || value < 0d) - throw new ArgumentOutOfRangeException(parameterName); - } - - private static double GetClearanceDeficit(double bodyClearanceMeters, double clearanceCostDistanceMeters) - { - if (double.IsPositiveInfinity(bodyClearanceMeters)) return 0d; - double deficit = 1d - bodyClearanceMeters / clearanceCostDistanceMeters; - return deficit > 0d ? deficit : 0d; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs deleted file mode 100644 index 63872e2..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test; - -/// Clumsy 粗路径手动测试可选择的固定场景。 -public enum CoarsePathTestScenario -{ - /// 明确允许的空地图直达场景。 - ExplicitEmpty, - - /// 由中央矩形阻断直线的绕行场景。 - RectangleDetour, - - /// 同时包含手工圆形、矩形与 TwoLeg 快照的多来源场景。 - ManualAndTwoLeg, - - /// 与矩形绕行输入完全一致,用于在同一服务中验证输入缓存命中。 - CacheHit, - - /// 起步前进、终点倒车进入的换向场景。 - ReverseGearSwitch, - - /// 由贯穿边界的障碍带分隔起终点的无解场景。 - NoFeasiblePath, -} - -/// 手动障碍物输入支持的世界几何类型。 -public enum ManualCoarsePathObstacleKind -{ - /// 由世界中心和半径定义的圆形障碍物。 - Circle, - - /// 由世界中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。 - AxisAlignedRectangle, -} - -/// -/// 手动粗路径测试的不可变障碍物输入。 -/// 所有中心和尺寸均使用世界 mm;矩形始终与世界坐标轴平行,不包含旋转角。 -/// -public sealed class ManualCoarsePathObstacle -{ - private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters, - double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters) - { - Kind = kind; - CenterXMillimeters = centerXMillimeters; - CenterYMillimeters = centerYMillimeters; - SizeXMillimeters = sizeXMillimeters; - SizeYMillimeters = sizeYMillimeters; - } - - /// 障碍物的支持几何类型。 - public ManualCoarsePathObstacleKind Kind { get; } - - /// 几何中心世界 X 坐标,单位 mm。 - public double CenterXMillimeters { get; } - - /// 几何中心世界 Y 坐标,单位 mm。 - public double CenterYMillimeters { get; } - - /// 圆形时为半径,矩形时为 X 方向长度;单位 mm。 - public double SizeXMillimeters { get; } - - /// 圆形时为半径,矩形时为 Y 方向宽度;单位 mm。 - public double SizeYMillimeters { get; } - - /// 创建圆形障碍物。参数:圆心和半径均使用世界 mm,半径必须为有限正数。 - public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters, - double radiusMillimeters) - { - EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); - EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); - EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters)); - return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters, - centerYMillimeters, radiusMillimeters, radiusMillimeters); - } - - /// - /// 创建轴对齐矩形障碍物。 - /// 参数:中心、X 方向长度和 Y 方向宽度均使用世界 mm;两个尺寸必须为有限正数。 - /// - public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters, - double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters) - { - EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); - EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); - EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters)); - EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters)); - return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle, - centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters); - } - - private static void EnsureFinite(double value, string parameterName) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); - } - - private static void EnsurePositiveFinite(double value, string parameterName) - { - EnsureFinite(value, parameterName); - if (value <= 0d) - throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); - } -} - -/// -/// Clumsy 粗路径测试的纯输入工厂。 -/// 固定场景不读取 UI、传感器、定位或时钟;传入 AMR 位姿的手动入口仅在此处完成世界 mm/deg 到核心 m/rad 的转换。 -/// -public static class CoarsePathScenarioFactory -{ - private const float MapXMinMillimeters = 0f; - private const float MapXMaxMillimeters = 6000f; - private const float MapYMinMillimeters = 0f; - private const float MapYMaxMillimeters = 4000f; - private const float ResolutionMillimeters = 50f; - private const double MillimetersPerMeter = 1000d; - private const double DegreesToRadians = Math.PI / 180d; - private const double ManualMapPaddingMillimeters = 8000d; - private const int MaximumManualObstacleCount = 20; - - /// - /// 创建一个新的固定测试业务请求。 - /// 返回:每次调用都返回独立的可变请求对象,供调用方安全地传入同一个长期存活的规划服务。 - /// - public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) - { - return CreateCore(scenario, null); - } - - /// - /// 创建以当前 AMR 世界位姿为起点的固定测试业务请求。 - /// 参数:X/Y 使用世界 mm,航向使用 deg;地图、目标和障碍物仅随 AMR 坐标平移,TwoLeg 朝向保持不变。 - /// - public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, - double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees) - { - return CreateCore(scenario, - new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees)); - } - - private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, FixedScenarioAnchor anchor) - { - switch (scenario) - { - case CoarsePathTestScenario.ExplicitEmpty: - return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - case CoarsePathTestScenario.RectangleDetour: - case CoarsePathTestScenario.CacheHit: - return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - case CoarsePathTestScenario.ManualAndTwoLeg: - return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor)); - case CoarsePathTestScenario.ReverseGearSwitch: - return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - case CoarsePathTestScenario.NoFeasiblePath: - return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - default: - throw new ArgumentOutOfRangeException(nameof(scenario)); - } - } - - /// - /// 创建传入 AMR 世界位姿和手动世界终点的空图演示请求。 - /// 参数:X/Y 使用世界 mm,航向使用 deg;返回请求中的 使用世界 m/rad。 - /// 注意:这是坐标、路径和取消流程的演示空图,不能表示现场不存在障碍物。 - /// - public static CoarsePathPlanningJob CreateManualGoalDemo( - double startXMillimeters, double startYMillimeters, double startHeadingDegrees, - double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees) - { - return CreateManualObstacleDemo(startXMillimeters, startYMillimeters, startHeadingDegrees, - goalXMillimeters, goalYMillimeters, goalHeadingDegrees, - Array.Empty(), 0L); - } - - /// - /// 创建传入 AMR 世界位姿、手动世界终点和手动障碍物快照的测试请求。 - /// 参数:位姿 X/Y、障碍物中心和尺寸使用世界 mm,航向使用 deg;返回的 使用 m/rad。 - /// 障碍物非空时 obstacleSnapshotVersion 必须为正数,以避免长期服务错误复用旧地图;零障碍物才创建显式空图演示。 - /// - public static CoarsePathPlanningJob CreateManualObstacleDemo( - double startXMillimeters, double startYMillimeters, double startHeadingDegrees, - double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees, - IReadOnlyList obstacles, long obstacleSnapshotVersion) - { - EnsureFinite(startXMillimeters, nameof(startXMillimeters)); - EnsureFinite(startYMillimeters, nameof(startYMillimeters)); - EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees)); - EnsureFinite(goalXMillimeters, nameof(goalXMillimeters)); - EnsureFinite(goalYMillimeters, nameof(goalYMillimeters)); - EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees)); - - if (obstacles == null) throw new ArgumentNullException(nameof(obstacles)); - if (obstacles.Count > MaximumManualObstacleCount) - throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle count exceeds the supported limit."); - if (obstacles.Count != 0 && obstacleSnapshotVersion <= 0L) - throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion), "Obstacle snapshots require a positive version."); - - return CreateJob( - CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters, - obstacles, obstacleSnapshotVersion), - ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), - ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), - null, - GoalDirectionConstraint.Any); - } - - private static CoarsePathPlanningJob CreateExplicitEmpty(FixedScenarioTransform transform) - { - return CreateJob( - CreateMap(true, Array.Empty(), transform), - transform.Pose(1d, 2d, 0d), - transform.Pose(5d, 2d, 0d), - null, - GoalDirectionConstraint.Forward); - } - - private static CoarsePathPlanningJob CreateRectangleDetour(FixedScenarioTransform transform) - { - IMapObstacleSource[] sources = - { - new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] - { - new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f), - transform.Y(1200f), transform.Y(2800f)), - }), - }; - CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d), - transform.Pose(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); - // 固定绕行场景保留最优启发式;30 秒覆盖较慢测试环境,UI 仍可随时取消。 - job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d); - return job; - } - - private static CoarsePathPlanningJob CreateManualAndTwoLeg(FixedScenarioTransform transform) - { - IMapObstacleSource[] sources = - { - new ManualObstacleSource("manual", 2L, true, new IMapObstacle[] - { - new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f), - new AxisAlignedRectangleObstacle(transform.X(3000f), transform.X(3600f), - transform.Y(2000f), transform.Y(2600f)), - }), - new TwoLegObstacleSource("two-leg", 1L, true, new TwoLegProjectionInput(true, - transform.X(3900f), transform.Y(2500f), 0d, - -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")), - }; - CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 1d, 0d), - transform.Pose(5d, 3d, 0d), null, GoalDirectionConstraint.Forward); - // 多来源场景的最优绕行会受机器负载影响;放宽演示总预算但保留全部碰撞与目标判定。 - job.Configuration.SearchTimeout = TimeSpan.FromSeconds(15d); - return job; - } - - private static CoarsePathPlanningJob CreateReverseGearSwitch(FixedScenarioTransform transform) - { - return CreateJob( - CreateMap(true, Array.Empty(), transform), - transform.Pose(1d, 2d, 0d), - transform.Pose(4d, 2d, 0d), - TravelDirection.Forward, - GoalDirectionConstraint.Reverse); - } - - private static CoarsePathPlanningJob CreateNoFeasiblePath(FixedScenarioTransform transform) - { - IMapObstacleSource[] sources = - { - new ManualObstacleSource("manual", 3L, true, new IMapObstacle[] - { - new AxisAlignedRectangleObstacle(transform.X(2900f), transform.X(3100f), - transform.Y(0f), transform.Y(4000f)), - }), - }; - return CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d), - transform.Pose(5d, 2d, 0d), - null, GoalDirectionConstraint.Forward); - } - - private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal, - TravelDirection? startDirection, GoalDirectionConstraint goalDirection) - { - return new CoarsePathPlanningJob - { - MapRequest = mapRequest, - Start = start, - Goal = goal, - Vehicle = new VehicleParameters - { - LengthMeters = 0.80d, - WidthMeters = 0.60d, - SafetyMarginMeters = 0.05d, - MaximumCurvaturePerMeter = 1d / 1.20d, - }, - Configuration = new HybridAStarConfiguration(), - StartDirection = startDirection, - GoalDirection = goalDirection, - }; - } - - private static PlanningMapRequest CreateMap(bool allowExplicitEmptyMap, IReadOnlyList sources, - FixedScenarioTransform transform) - { - return new PlanningMapRequest - { - Bounds = new MapBoundsMm(transform.X(MapXMinMillimeters), transform.X(MapXMaxMillimeters), - transform.Y(MapYMinMillimeters), transform.Y(MapYMaxMillimeters)), - ResolutionMm = ResolutionMillimeters, - ObstacleSources = sources, - AllowExplicitEmptyMap = allowExplicitEmptyMap, - }; - } - - private sealed class FixedScenarioAnchor - { - public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees) - { - EnsureFinite(xMillimeters, nameof(xMillimeters)); - EnsureFinite(yMillimeters, nameof(yMillimeters)); - EnsureFinite(headingDegrees, nameof(headingDegrees)); - XMillimeters = xMillimeters; - YMillimeters = yMillimeters; - HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians); - } - - public double XMillimeters { get; } - public double YMillimeters { get; } - public double HeadingRadians { get; } - } - - private sealed class FixedScenarioTransform - { - private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians) - { - DeltaXMillimeters = deltaXMillimeters; - DeltaYMillimeters = deltaYMillimeters; - HeadingDeltaRadians = headingDeltaRadians; - } - - public double DeltaXMillimeters { get; } - public double DeltaYMillimeters { get; } - public double HeadingDeltaRadians { get; } - - public static FixedScenarioTransform From(double baselineStartXMillimeters, - double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor) - { - if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d); - return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters, - anchor.YMillimeters - baselineStartYMillimeters, - NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians)); - } - - public float X(float value) - { - return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); - } - - public float Y(float value) - { - return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); - } - - public Pose2D Pose(double xMeters, double yMeters, double headingRadians) - { - return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter, - (yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter, - NormalizeRadians(headingRadians + HeadingDeltaRadians)); - } - } - - private static PlanningMapRequest CreateManualDemoMap(double startXMillimeters, double startYMillimeters, - double goalXMillimeters, double goalYMillimeters, IReadOnlyList obstacles, - long obstacleSnapshotVersion) - { - double minimumX = Math.Min(startXMillimeters, goalXMillimeters); - double maximumX = Math.Max(startXMillimeters, goalXMillimeters); - double minimumY = Math.Min(startYMillimeters, goalYMillimeters); - double maximumY = Math.Max(startYMillimeters, goalYMillimeters); - for (int index = 0; index < obstacles.Count; index++) - { - ManualCoarsePathObstacle obstacle = obstacles[index] ?? - throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles)); - double halfX; - double halfY; - switch (obstacle.Kind) - { - case ManualCoarsePathObstacleKind.Circle: - EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles)); - halfX = obstacle.SizeXMillimeters; - halfY = obstacle.SizeYMillimeters; - break; - case ManualCoarsePathObstacleKind.AxisAlignedRectangle: - EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles)); - EnsurePositiveFinite(obstacle.SizeYMillimeters, nameof(obstacles)); - halfX = obstacle.SizeXMillimeters / 2d; - halfY = obstacle.SizeYMillimeters / 2d; - break; - default: - throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported."); - } - - minimumX = Math.Min(minimumX, obstacle.CenterXMillimeters - halfX); - maximumX = Math.Max(maximumX, obstacle.CenterXMillimeters + halfX); - minimumY = Math.Min(minimumY, obstacle.CenterYMillimeters - halfY); - maximumY = Math.Max(maximumY, obstacle.CenterYMillimeters + halfY); - } - - float xMin = ToGridLowerBound(minimumX - ManualMapPaddingMillimeters); - float xMax = ToGridUpperBound(maximumX + ManualMapPaddingMillimeters); - float yMin = ToGridLowerBound(minimumY - ManualMapPaddingMillimeters); - float yMax = ToGridUpperBound(maximumY + ManualMapPaddingMillimeters); - bool isExplicitEmptyMap = obstacles.Count == 0; - return new PlanningMapRequest - { - Bounds = new MapBoundsMm(xMin, xMax, yMin, yMax), - ResolutionMm = ResolutionMillimeters, - ObstacleSources = isExplicitEmptyMap ? Array.Empty() : - new IMapObstacleSource[] - { - new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, - ConvertManualObstacles(obstacles)), - }, - AllowExplicitEmptyMap = isExplicitEmptyMap, - }; - } - - private static IMapObstacle[] ConvertManualObstacles(IReadOnlyList obstacles) - { - var result = new IMapObstacle[obstacles.Count]; - for (int index = 0; index < obstacles.Count; index++) - { - ManualCoarsePathObstacle obstacle = obstacles[index] ?? - throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles)); - switch (obstacle.Kind) - { - case ManualCoarsePathObstacleKind.Circle: - result[index] = new CircleObstacle(ToFiniteFloat(obstacle.CenterXMillimeters, nameof(obstacles)), - ToFiniteFloat(obstacle.CenterYMillimeters, nameof(obstacles)), - ToFiniteFloat(obstacle.SizeXMillimeters, nameof(obstacles))); - break; - case ManualCoarsePathObstacleKind.AxisAlignedRectangle: - double halfX = obstacle.SizeXMillimeters / 2d; - double halfY = obstacle.SizeYMillimeters / 2d; - result[index] = new AxisAlignedRectangleObstacle( - ToFiniteFloat(obstacle.CenterXMillimeters - halfX, nameof(obstacles)), - ToFiniteFloat(obstacle.CenterXMillimeters + halfX, nameof(obstacles)), - ToFiniteFloat(obstacle.CenterYMillimeters - halfY, nameof(obstacles)), - ToFiniteFloat(obstacle.CenterYMillimeters + halfY, nameof(obstacles))); - break; - default: - throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported."); - } - } - return result; - } - - private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees) - { - return new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter, - headingDegrees * DegreesToRadians); - } - - private static double NormalizeRadians(double angle) - { - double normalized = angle % (2d * Math.PI); - if (normalized <= -Math.PI) return normalized + 2d * Math.PI; - return normalized > Math.PI ? normalized - 2d * Math.PI : normalized; - } - - private static float ToGridLowerBound(double millimeters) - { - double rounded = Math.Floor(millimeters / ResolutionMillimeters) * ResolutionMillimeters; - return ToFiniteFloat(rounded, nameof(millimeters)); - } - - private static float ToGridUpperBound(double millimeters) - { - double rounded = Math.Ceiling(millimeters / ResolutionMillimeters) * ResolutionMillimeters; - return ToFiniteFloat(rounded, nameof(millimeters)); - } - - private static float ToFiniteFloat(double value, string parameterName) - { - if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue) - throw new ArgumentOutOfRangeException(parameterName, "Value cannot be represented as a finite millimeter coordinate."); - return (float)value; - } - - private static void EnsureFinite(double value, string parameterName) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); - } - - private static void EnsurePositiveFinite(double value, string parameterName) - { - EnsureFinite(value, parameterName); - if (value <= 0d) - throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs deleted file mode 100644 index 2fa47d5..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs +++ /dev/null @@ -1,628 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using FundamentalLib; -using MDCSToolBox.Clumsy.Pilot; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Test; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC; - -/// -/// 显式空图的粗路径规划测试入口。 -/// 只负责创建纯规划请求;规划、取消与可视化均由共享执行器处理,不会向底盘发送任何命令。 -/// -[MovementTest(name = "粗路径规划-显式空图")] -public sealed class CoarsePathExplicitEmptyTest : MovementTest -{ - /// - public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.ExplicitEmpty, "显式空图"); - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); -} - -/// -/// 单矩形绕行的粗路径规划测试入口。 -/// -[MovementTest(name = "粗路径规划-单矩形绕行")] -// TODO:#在该测试下发生了红色矩形栅格碰撞但依然规划成功,需要进一步核实与确认 -public sealed class CoarsePathRectangleDetourTest : MovementTest -{ - /// - public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.RectangleDetour, "单矩形绕行"); - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); -} - -/// -/// 手工圆形、矩形和 TwoLeg 快照组合的粗路径规划测试入口。 -/// -[MovementTest(name = "粗路径规划-多来源障碍")] -public sealed class CoarsePathManualAndTwoLegTest : MovementTest -{ - /// - public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.ManualAndTwoLeg, "多来源障碍"); - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); -} - -/// -/// 重复输入地图缓存命中的粗路径规划测试入口。 -/// -[MovementTest(name = "粗路径规划-缓存命中")] -public sealed class CoarsePathCacheHitTest : MovementTest -{ - /// - public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.CacheHit, "缓存命中"); - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); -} - -/// -/// 前进起步、倒车到达并显示换向点的粗路径规划测试入口。 -/// -[MovementTest(name = "粗路径规划-倒车换向")] -public sealed class CoarsePathReverseGearSwitchTest : MovementTest -{ - /// - public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.ReverseGearSwitch, "倒车换向"); - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); -} - -/// -/// 障碍带完全隔开起终点的无解粗路径规划测试入口。 -/// -[MovementTest(name = "粗路径规划-无解")] -public sealed class CoarsePathNoFeasiblePathTest : MovementTest -{ - /// - public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.NoFeasiblePath, "无解障碍带"); - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); -} - -/// -/// 使用当前 AMR 车身几何中心位姿和人工终点的粗路径规划演示入口。 -/// 输入的 X/Y 使用世界 mm、航向使用 deg;进入规划核心前由场景工厂一次性转换为 m/rad。 -/// -[MovementTest(name = "粗路径规划")] -public sealed class CoarsePathPlanningTest : MovementTest -{ - private const int MaximumManualObstacleCount = 20; - private static long _nextManualObstacleSnapshotVersion; - - /// - /// 读取一次 AMR 当前世界位姿、手动终点和障碍物快照后启动规划。 - /// 注意:getCartLocation 在无定位时可能阻塞;全部输入会在启动后台任务前冻结,不会被规划线程重复读取。 - /// - public override void Test() - { - try - { - var amrPose = DetourInterface.getCartLocation(); - double goalXmm = ReadFiniteInput("粗路径终点 X(世界 mm)"); - double goalYmm = ReadFiniteInput("粗路径终点 Y(世界 mm)"); - double goalHeadingDeg = ReadFiniteInput("粗路径终点航向(世界 deg)"); - TimeSpan searchTimeout = ReadPositiveTimeoutInput("粗路径规划总超时(秒,必须大于 0)"); - IReadOnlyList obstacles = ReadManualObstacles(); - long snapshotVersion = obstacles.Count == 0 ? 0L : - Interlocked.Increment(ref _nextManualObstacleSnapshotVersion); - CoarsePathPlanningJob job = CoarsePathScenarioFactory.CreateManualObstacleDemo( - amrPose.x, amrPose.y, amrPose.th, goalXmm, goalYmm, goalHeadingDeg, - obstacles, snapshotVersion); - job.Configuration.SearchTimeout = searchTimeout; - CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job); - } - catch (Exception exception) - { - CoarsePathPlanningTestRunner.ShowInputFailure(exception); - } - } - - /// - public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); - - private static IReadOnlyList ReadManualObstacles() - { - int count = ReadBoundedIntegerInput("手动障碍物数量(0-20)", 0, MaximumManualObstacleCount); - var obstacles = new List(count); - for (int index = 0; index < count; index++) - { - string label = "障碍物 " + (index + 1); - int kind = ReadBoundedIntegerInput(label + " 类型(1圆形,2矩形)", 1, 2); - double centerXmm = ReadFiniteInput(label + " 中心 X(世界 mm)"); - double centerYmm = ReadFiniteInput(label + " 中心 Y(世界 mm)"); - if (kind == 1) - { - double radiusMm = ReadPositiveFiniteInput(label + " 半径 r(mm)"); - obstacles.Add(ManualCoarsePathObstacle.Circle(centerXmm, centerYmm, radiusMm)); - } - else - { - double lengthXmm = ReadPositiveFiniteInput(label + " X方向长度(mm)"); - double widthYmm = ReadPositiveFiniteInput(label + " Y方向宽度(mm)"); - obstacles.Add(ManualCoarsePathObstacle.AxisAlignedRectangle(centerXmm, centerYmm, - lengthXmm, widthYmm)); - } - } - return obstacles; - } - - private static int ReadBoundedIntegerInput(string prompt, int minimum, int maximum) - { - object raw = UI.GetInput(prompt); - string text = Convert.ToString(raw, CultureInfo.CurrentCulture); - int value; - if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out value) && - !int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) - throw new ArgumentException("输入必须是整数:" + prompt); - if (value < minimum || value > maximum) - throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt); - return value; - } - - private static double ReadPositiveFiniteInput(string prompt) - { - double value = ReadFiniteInput(prompt); - if (value <= 0d) - throw new ArgumentOutOfRangeException(nameof(prompt), "输入必须为正数:" + prompt); - return value; - } - - private static TimeSpan ReadPositiveTimeoutInput(string prompt) - { - double timeoutSeconds = ReadFiniteInput(prompt); - if (timeoutSeconds <= 0d) - throw new ArgumentOutOfRangeException(nameof(prompt), "输入必须为正数:" + prompt); - try - { - return TimeSpan.FromSeconds(timeoutSeconds); - } - catch (OverflowException) - { - throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt); - } - } - - private static double ReadFiniteInput(string prompt) - { - object raw = UI.GetInput(prompt); - string text = Convert.ToString(raw, CultureInfo.CurrentCulture); - double value; - if (!double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) && - !double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) - throw new ArgumentException("输入必须是有限数字:" + prompt); - if (double.IsNaN(value) || double.IsInfinity(value)) - throw new ArgumentException("输入必须是有限数字:" + prompt); - return value; - } -} - -/// -/// 粗路径 MovementTest 的共享后台会话、停止和绘制实现。 -/// 同一时刻只允许一个会话绘制;启动新会话或停止时会取消旧会话,但不会等待旧任务退出。 -/// -internal static class CoarsePathPlanningTestRunner -{ - private const string PainterLayerName = "CoarsePathPlanningV1"; - private const float MillimetersPerMeter = 1000f; - private const int MaximumVisibleGridLines = 100; - - private static readonly object SessionSync = new object(); - private static readonly CoarsePathPlanningService PlanningService = new CoarsePathPlanningService(); - private static readonly Painter Painter = UI.GetPainter(PainterLayerName, true); - - private static CancellationTokenSource _activeCancellation; - private static Task _activeTask; - private static long _nextSessionId; - private static long _activeSessionId; - - /// - /// 固定场景启动时冻结的 AMR 位姿。规划后台不会重新读取定位,确保输入一致。 - /// - private sealed class AmrPoseSnapshot - { - public AmrPoseSnapshot(double xMillimeters, double yMillimeters, double headingDegrees) - { - EnsureFiniteAmrValue(xMillimeters, "X"); - EnsureFiniteAmrValue(yMillimeters, "Y"); - EnsureFiniteAmrValue(headingDegrees, "航向"); - XMillimeters = xMillimeters; - YMillimeters = yMillimeters; - HeadingDegrees = headingDegrees; - } - - public double XMillimeters { get; } - public double YMillimeters { get; } - public double HeadingDegrees { get; } - - public string DisplayText - { - get - { - return "AMR 起点:X=" + XMillimeters.ToString("F0", CultureInfo.InvariantCulture) + - " mm,Y=" + YMillimeters.ToString("F0", CultureInfo.InvariantCulture) + - " mm,航向=" + HeadingDegrees.ToString("F1", CultureInfo.InvariantCulture) + " deg"; - } - } - } - - /// - /// 创建指定固定场景并将其提交给共享后台服务。 - /// - internal static void RunScenario(CoarsePathTestScenario scenario, string scenarioName) - { - try - { - var pose = DetourInterface.getCartLocation(); - if (ReferenceEquals(pose, null)) throw new ArgumentException("AMR 位姿为空。"); - var snapshot = new AmrPoseSnapshot(pose.x, pose.y, pose.th); - CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenario, - snapshot.XMillimeters, snapshot.YMillimeters, snapshot.HeadingDegrees); - Run(scenarioName, job, snapshot); - } - catch (Exception exception) - { - ShowInputFailure(new ArgumentException("AMR 位姿不可用:" + exception.Message, exception)); - } - } - - /// - /// 提交已经冻结输入的一次规划请求。调用立即返回,结果只会由对应会话的完成回调绘制。 - /// - internal static void Run(string scenarioName, CoarsePathPlanningJob job) - { - Run(scenarioName, job, null); - } - - private static void Run(string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose) - { - if (job == null) throw new ArgumentNullException(nameof(job)); - - var cancellation = new CancellationTokenSource(); - CancellationTokenSource previousCancellation; - long sessionId; - lock (SessionSync) - { - previousCancellation = _activeCancellation; - _activeCancellation = cancellation; - _activeTask = null; - sessionId = ++_nextSessionId; - _activeSessionId = sessionId; - } - - // 先发布新会话编号,再取消旧任务,避免旧完成回调覆盖新画面。 - if (previousCancellation != null) previousCancellation.Cancel(); - Painter.Clear(); - DrawPending(scenarioName, job, amrPose); - - Task task = Task.Run(() => PlanningService.Plan(job, cancellation.Token)); - lock (SessionSync) - { - if (_activeSessionId == sessionId) _activeTask = task; - } - - _ = task.ContinueWith(completed => Finish(sessionId, scenarioName, job, amrPose, cancellation, completed), - TaskScheduler.Default); - } - - /// - /// 取消当前会话并清空专用图层;不等待后台任务结束。 - /// 已取消任务的完成回调只释放资源,不再记录或绘制结果。 - /// - internal static void Stop() - { - CancellationTokenSource cancellation; - lock (SessionSync) - { - cancellation = _activeCancellation; - _activeCancellation = null; - _activeTask = null; - _activeSessionId = 0; - } - - if (cancellation != null) cancellation.Cancel(); - Painter.Clear(); - Hedingben.ToastText("粗路径规划已请求停止。", PainterLayerName); - } - - /// - /// 显示输入读取或校验失败,且不改变现有规划任务。 - /// - internal static void ShowInputFailure(Exception exception) - { - string message = exception == null ? "未知输入错误。" : exception.Message; - Painter.DrawText(Color.LightYellow, "粗路径规划未启动:" + message, 0f, 0f); - Hedingben.ToastText("粗路径规划未启动:" + message, PainterLayerName); - } - - private static void Finish(long sessionId, string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose, - CancellationTokenSource cancellation, Task completed) - { - try - { - CoarsePathPlanningJobResult result = completed.GetAwaiter().GetResult(); - bool isCurrent; - lock (SessionSync) - { - isCurrent = _activeSessionId == sessionId; - if (isCurrent) - { - _activeTask = null; - _activeCancellation = null; - } - } - - if (!isCurrent) return; - DrawResult(scenarioName, job, amrPose, result); - Hedingben.ToastText(BuildToastMessage(scenarioName, result), PainterLayerName); - } - catch (Exception exception) - { - bool isCurrent; - lock (SessionSync) - { - isCurrent = _activeSessionId == sessionId; - if (isCurrent) - { - _activeTask = null; - _activeCancellation = null; - } - } - - if (isCurrent) - Hedingben.ToastText("粗路径规划任务异常:" + exception.GetType().Name + "。" + exception.Message, - PainterLayerName); - } - finally - { - cancellation.Dispose(); - } - } - - private static void DrawPending(string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose) - { - DrawPose(job.Start, Color.LimeGreen, "起点"); - DrawPose(job.Goal, Color.Orange, "终点"); - Painter.DrawText(Color.LightGray, "场景:" + scenarioName + "(规划中)", 0f, 0f); - if (amrPose != null) Painter.DrawText(Color.LightGray, amrPose.DisplayText, 0f, -120f); - } - - private static void DrawResult(string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose, - CoarsePathPlanningJobResult result) - { - Painter.Clear(); - PlanningGridMap map = result.MapResult.Map; - if (map != null) DrawMap(map); - - DrawPose(job.Start, Color.LimeGreen, "起点"); - DrawGoal(job.Goal, job.Configuration, Color.Orange); - if (result.PlanningResult.Status == PlanningStatus.Success) - DrawPath(result.PlanningResult, job.Vehicle); - - DrawLegend(map); - DrawStatus(scenarioName, job, result, map, amrPose); - } - - private static void DrawMap(PlanningGridMap map) - { - float xMin = map.Bounds.XMin; - float xMax = map.Bounds.XMax; - float yMin = map.Bounds.YMin; - float yMax = map.Bounds.YMax; - float resolution = map.ResolutionMm; - int gridStride = Math.Max(1, (int)Math.Ceiling(Math.Max(map.Rows, map.Cols) / (double)MaximumVisibleGridLines)); - - // 真实 ResolutionMm 决定网格位置,gridStride 只影响显示抽稀。 - for (int col = 0; col <= map.Cols; col += gridStride) - { - float x = Math.Min(xMax, xMin + col * resolution); - Painter.DrawLine(Color.FromArgb(80, Color.SlateGray), x, yMin, x, yMax, width: 1); - } - for (int row = 0; row <= map.Rows; row += gridStride) - { - float y = Math.Min(yMax, yMin + row * resolution); - Painter.DrawLine(Color.FromArgb(80, Color.SlateGray), xMin, y, xMax, y, width: 1); - } - - DrawRectangle(Color.Gainsboro, xMin, yMin, xMax, yMax, 3); - if (xMin <= 0f && 0f < xMax) Painter.DrawLine(Color.DimGray, 0f, yMin, 0f, yMax, width: 2); - if (yMin <= 0f && 0f < yMax) Painter.DrawLine(Color.DimGray, xMin, 0f, xMax, 0f, width: 2); - - for (int row = 0; row < map.Rows; row++) - { - for (int col = 0; col < map.Cols; col++) - { - if (!map.IsOccupied(row, col)) continue; - float x = xMin + col * resolution + resolution / 2f; - float y = yMin + row * resolution; - Painter.DrawLine(Color.FromArgb(150, Color.Firebrick), x, y, x, y + resolution, - width: Math.Max(1, (int)Math.Round(resolution))); - } - } - } - - private static void DrawPose(Pose2D pose, Color color, string label) - { - if (pose == null) return; - float x = ToMillimeters(pose.X); - float y = ToMillimeters(pose.Y); - Painter.DrawCircle(color, x, y, 80f); - DrawHeadingArrow(x, y, pose.Heading, color, 260f); - Painter.DrawText(color, label, x + 100f, y + 100f); - } - - private static void DrawGoal(Pose2D goal, HybridAStarConfiguration configuration, Color color) - { - DrawPose(goal, color, "终点"); - if (goal == null || configuration == null) return; - Painter.DrawCircle(Color.FromArgb(150, color), ToMillimeters(goal.X), ToMillimeters(goal.Y), - ToMillimeters(configuration.GoalPositionToleranceMeters)); - } - - private static void DrawPath(PlanningResult planningResult, VehicleParameters vehicle) - { - if (planningResult.Path == null || planningResult.Path.Count == 0) return; - - int frameStride = Math.Max(1, planningResult.Path.Count / 10); - for (int index = 1; index < planningResult.Path.Count; index++) - { - CoarsePathPoint previous = planningResult.Path[index - 1]; - CoarsePathPoint current = planningResult.Path[index]; - Color color = current.Direction == TravelDirection.Forward ? Color.LimeGreen : Color.DeepSkyBlue; - Painter.DrawLine(color, ToMillimeters(previous.X), ToMillimeters(previous.Y), - ToMillimeters(current.X), ToMillimeters(current.Y), width: 4); - - if (index % frameStride == 0 || current.IsGearSwitchPoint || index == planningResult.Path.Count - 1) - DrawVehicleFrame(current, vehicle); - if (index % Math.Max(1, frameStride / 2) == 0) - DrawHeadingArrow(ToMillimeters(current.X), ToMillimeters(current.Y), - current.Heading + (current.Direction == TravelDirection.Reverse ? Math.PI : 0d), color, 140f); - if (!current.IsGearSwitchPoint) continue; - - float x = ToMillimeters(current.X); - float y = ToMillimeters(current.Y); - Painter.DrawCircle(Color.MediumPurple, x, y, 100f); - Painter.DrawText(Color.MediumPurple, "换向", x + 110f, y - 110f); - } - - DrawVehicleFrame(planningResult.Path[0], vehicle); - } - - private static void DrawVehicleFrame(CoarsePathPoint point, VehicleParameters vehicle) - { - if (point == null || vehicle == null) return; - float halfLength = ToMillimeters(vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters); - float halfWidth = ToMillimeters(vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters); - float centerX = ToMillimeters(point.X); - float centerY = ToMillimeters(point.Y); - double cos = Math.Cos(point.Heading); - double sin = Math.Sin(point.Heading); - - TransformVehicleCorner(centerX, centerY, cos, sin, halfLength, halfWidth, out float frontLeftX, out float frontLeftY); - TransformVehicleCorner(centerX, centerY, cos, sin, halfLength, -halfWidth, out float frontRightX, out float frontRightY); - TransformVehicleCorner(centerX, centerY, cos, sin, -halfLength, -halfWidth, out float rearRightX, out float rearRightY); - TransformVehicleCorner(centerX, centerY, cos, sin, -halfLength, halfWidth, out float rearLeftX, out float rearLeftY); - Painter.DrawLine(Color.Gold, frontLeftX, frontLeftY, frontRightX, frontRightY, width: 2); - Painter.DrawLine(Color.Gold, frontRightX, frontRightY, rearRightX, rearRightY, width: 2); - Painter.DrawLine(Color.Gold, rearRightX, rearRightY, rearLeftX, rearLeftY, width: 2); - Painter.DrawLine(Color.Gold, rearLeftX, rearLeftY, frontLeftX, frontLeftY, width: 2); - } - - private static void TransformVehicleCorner(float centerX, float centerY, double cos, double sin, - float longitudinal, float lateral, out float x, out float y) - { - x = centerX + (float)(cos * longitudinal - sin * lateral); - y = centerY + (float)(sin * longitudinal + cos * lateral); - } - - private static void DrawHeadingArrow(float x, float y, double headingRadians, Color color, float length) - { - float endX = x + (float)Math.Cos(headingRadians) * length; - float endY = y + (float)Math.Sin(headingRadians) * length; - Painter.DrawLine(color, x, y, endX, endY, endArrow: true, width: 3); - } - - private static void DrawLegend(PlanningGridMap map) - { - float x = map == null ? 0f : map.Bounds.XMin + 150f; - float y = map == null ? 250f : map.Bounds.YMax - 180f; - Painter.DrawText(Color.White, "图例", x, y); - DrawLegendItem(x, y - 130f, Color.Gainsboro, "边界 / 栅格"); - DrawLegendItem(x, y - 260f, Color.Firebrick, "占据格"); - DrawLegendItem(x, y - 390f, Color.LimeGreen, "起点 / 前进"); - DrawLegendItem(x, y - 520f, Color.Orange, "终点 / 容差"); - DrawLegendItem(x, y - 650f, Color.DeepSkyBlue, "倒车"); - DrawLegendItem(x, y - 780f, Color.MediumPurple, "换向"); - DrawLegendItem(x, y - 910f, Color.Gold, "扩大车体检查框"); - } - - private static void DrawLegendItem(float x, float y, Color color, string text) - { - Painter.DrawLine(color, x, y, x + 90f, y, width: 5); - Painter.DrawText(color, text, x + 120f, y - 30f); - } - - private static void DrawStatus(string scenarioName, CoarsePathPlanningJob job, - CoarsePathPlanningJobResult result, PlanningGridMap map, AmrPoseSnapshot amrPose) - { - float x = map == null ? 0f : map.Bounds.XMin + 150f; - float y = map == null ? -250f : map.Bounds.YMin + 150f; - string snapshot = map == null ? "无" : map.SnapshotId.ToString(CultureInfo.InvariantCulture); - string resolution = map == null ? "无" : map.ResolutionMm.ToString("F0", CultureInfo.InvariantCulture) + " mm"; - PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics; - string reason = diagnostics.TerminationReason ?? string.Empty; - string turningRadius = "无"; - if (job != null && VehicleKinematics.TryGetMaximumCurvaturePerMeter(job.Vehicle, out double maximumCurvaturePerMeter)) - turningRadius = (1d / maximumCurvaturePerMeter).ToString("F2", CultureInfo.InvariantCulture) + " m"; - Painter.DrawText(Color.White, "场景:" + scenarioName, x, y); - float detailOffset = 0f; - if (amrPose != null) - { - Painter.DrawText(Color.White, amrPose.DisplayText, x, y + 120f); - detailOffset = 120f; - } - Painter.DrawText(Color.White, "地图:" + result.MapResult.Status + ",缓存:" + result.MapResult.CacheHit + ",快照:" + snapshot, - x, y + 120f + detailOffset); - Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" + result.PlanningResult.Status + ",总耗时:" + - diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + " ms,路径搜索:" + - diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + " ms", x, y + 240f + detailOffset); - Painter.DrawText(Color.White, "节点:扩展=" + diagnostics.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + - ",生成=" + diagnostics.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + ",Open List峰值=" + - diagnostics.PeakOpenListCount.ToString(CultureInfo.InvariantCulture), x, y + 360f + detailOffset); - if (job != null && job.Vehicle != null) - { - Painter.DrawText(Color.White, "演示车辆:长=" + job.Vehicle.LengthMeters.ToString("F2", CultureInfo.InvariantCulture) + - " m,宽=" + job.Vehicle.WidthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,余量=" + - job.Vehicle.SafetyMarginMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,最小转弯半径=" + - turningRadius, x, y + 480f + detailOffset); - } - if (!string.IsNullOrEmpty(reason)) - Painter.DrawText(Color.LightYellow, "原因:" + reason, x, y + 600f + detailOffset); - } - - private static string BuildToastMessage(string scenarioName, CoarsePathPlanningJobResult result) - { - PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics; - string message = "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status + ",规划=" + - result.PlanningResult.Status + ",总耗时=" + - diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + "ms,路径搜索=" + - diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + "ms"; - if (result.PlanningResult.Status != PlanningStatus.Success && !string.IsNullOrEmpty(diagnostics.TerminationReason)) - message += ",原因=" + diagnostics.TerminationReason; - return message + "。"; - } - - private static void DrawRectangle(Color color, float xMin, float yMin, float xMax, float yMax, int width) - { - Painter.DrawLine(color, xMin, yMin, xMax, yMin, width: width); - Painter.DrawLine(color, xMax, yMin, xMax, yMax, width: width); - Painter.DrawLine(color, xMax, yMax, xMin, yMax, width: width); - Painter.DrawLine(color, xMin, yMax, xMin, yMin, width: width); - } - - private static float ToMillimeters(double meters) => (float)(meters * MillimetersPerMeter); - - private static void EnsureFiniteAmrValue(double value, string name) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - throw new ArgumentException(name + " 必须是有限数。"); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs deleted file mode 100644 index c403d4a..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; - -/// -/// 对以车辆几何中心表示的扩大矩形执行连续碰撞检查。 -/// 地图查询使用 m;任何地图外车辆部分、占据格相交或擦边均按碰撞处理。 -/// -public sealed class FootprintCollisionChecker -{ - /// 创建连续车辆碰撞检查器。 - public FootprintCollisionChecker() - { - } - - /// - /// 判断单个车辆位姿是否无碰撞。 - /// 参数:pose 为车辆几何中心的世界 m/rad 位姿;map 为不可变规划地图;vehicle 为车辆尺寸; - /// additionalMarginMeters 为临时额外安全余量,单位 m;bodyClearanceMeters 输出不含该临时余量的保守车体净空下界,单位 m。 - /// 返回:扩大车辆矩形完整位于地图内且不与任何占据格相交或擦边时为 true;无效输入保守地返回 false。 - /// - public bool IsPoseCollisionFree(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, - double additionalMarginMeters, out double bodyClearanceMeters) - { - bodyClearanceMeters = 0d; - if (map == null || !NumericGuard.IsFinite(additionalMarginMeters) || additionalMarginMeters < 0d || - !VehicleFootprint.TryCreate(pose, vehicle, 0d, out VehicleFootprint bodyFootprint) || - !VehicleFootprint.TryCreate(pose, vehicle, additionalMarginMeters, out VehicleFootprint checkedFootprint)) - return false; - - if (!AreCornersInsideMap(checkedFootprint, map)) return false; - - double centerDistanceMeters = map.GetConservativeObstacleDistanceMeters(pose.X, pose.Y); - bodyClearanceMeters = GetBodyClearance(centerDistanceMeters, bodyFootprint.CircumscribedRadiusMeters); - if (centerDistanceMeters > checkedFootprint.CircumscribedRadiusMeters) return true; - - return !IntersectsOccupiedCell(checkedFootprint, map); - } - - /// - /// 判断两个位姿之间的平移和转向扫掠是否无碰撞。 - /// 参数:from、to 为世界 m/rad 位姿;maximumCenterStepMeters 为允许的最大中心采样间距,单位 m; - /// minimumBodyClearanceMeters 输出沿途不含临时扫掠余量的保守车体净空下界,单位 m。 - /// 返回:端点和每个分段扫掠均无碰撞时为 true;无效输入、地图外或任一中间碰撞时返回 false。 - /// - public bool IsSweptMotionCollisionFree(Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, - double maximumCenterStepMeters, out double minimumBodyClearanceMeters) - { - minimumBodyClearanceMeters = 0d; - if (map == null || from == null || to == null || !NumericGuard.IsFinite(maximumCenterStepMeters) || maximumCenterStepMeters <= 0d || - !NumericGuard.IsFinite(from.X) || !NumericGuard.IsFinite(from.Y) || !NumericGuard.IsFinite(from.Heading) || - !NumericGuard.IsFinite(to.X) || !NumericGuard.IsFinite(to.Y) || !NumericGuard.IsFinite(to.Heading) || - !VehicleFootprint.TryCreate(from, vehicle, 0d, out VehicleFootprint bodyFootprint)) - return false; - - double allowedStepMeters = Math.Min(maximumCenterStepMeters, map.ResolutionMeters / 2d); - if (!NumericGuard.IsPositiveFinite(allowedStepMeters)) return false; - - if (!IsPoseCollisionFree(from, map, vehicle, 0d, out double fromClearanceMeters)) return false; - minimumBodyClearanceMeters = fromClearanceMeters; - - double deltaX = to.X - from.X; - double deltaY = to.Y - from.Y; - double centerDistanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - if (!NumericGuard.IsFinite(centerDistanceMeters)) return false; - double headingDeltaRadians = AngleMath.ShortestSignedDifference(from.Heading, to.Heading); - if (!NumericGuard.IsFinite(headingDeltaRadians)) return false; - double rawSegmentCount = Math.Ceiling(centerDistanceMeters / allowedStepMeters); - if (!NumericGuard.IsFinite(rawSegmentCount) || rawSegmentCount > int.MaxValue) return false; - int segmentCount = Math.Max(1, (int)rawSegmentCount); - - Pose2D previousPose = from; - for (int segment = 1; segment <= segmentCount; segment++) - { - double endFraction = (double)segment / segmentCount; - double middleFraction = ((double)segment - 0.5d) / segmentCount; - var currentPose = new Pose2D( - from.X + deltaX * endFraction, - from.Y + deltaY * endFraction, - from.Heading + headingDeltaRadians * endFraction); - var middlePose = new Pose2D( - from.X + deltaX * middleFraction, - from.Y + deltaY * middleFraction, - from.Heading + headingDeltaRadians * middleFraction); - double segmentDeltaX = currentPose.X - previousPose.X; - double segmentDeltaY = currentPose.Y - previousPose.Y; - double segmentCenterDisplacementMeters = Math.Sqrt(segmentDeltaX * segmentDeltaX + segmentDeltaY * segmentDeltaY); - double segmentHeadingDeltaRadians = currentPose.Heading - previousPose.Heading; - double temporaryMarginMeters = 0.5d * (segmentCenterDisplacementMeters + - bodyFootprint.CircumscribedRadiusMeters * Math.Abs(segmentHeadingDeltaRadians)); - if (!NumericGuard.IsFinite(temporaryMarginMeters) || - !IsPoseCollisionFree(middlePose, map, vehicle, temporaryMarginMeters, out double middleClearanceMeters)) - return false; - minimumBodyClearanceMeters = Math.Min(minimumBodyClearanceMeters, middleClearanceMeters); - previousPose = currentPose; - } - - if (!IsPoseCollisionFree(to, map, vehicle, 0d, out double toClearanceMeters)) return false; - minimumBodyClearanceMeters = Math.Min(minimumBodyClearanceMeters, toClearanceMeters); - return true; - } - - private static bool AreCornersInsideMap(VehicleFootprint footprint, PlanningGridMap map) - { - for (int index = 0; index < 4; index++) - { - footprint.GetCorner(index, out double cornerX, out double cornerY); - if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; - } - return true; - } - - private static double GetBodyClearance(double centerDistanceMeters, double bodyRadiusMeters) - { - if (double.IsPositiveInfinity(centerDistanceMeters)) return double.PositiveInfinity; - if (!NumericGuard.IsFinite(centerDistanceMeters) || !NumericGuard.IsFinite(bodyRadiusMeters)) return 0d; - return Math.Max(0d, centerDistanceMeters - bodyRadiusMeters); - } - - private static bool IntersectsOccupiedCell(VehicleFootprint footprint, PlanningGridMap map) - { - GetCellRange(map, footprint.MinX, footprint.MaxX, footprint.MinY, footprint.MaxY, - out int firstRow, out int lastRow, out int firstCol, out int lastCol); - for (int row = firstRow; row <= lastRow; row++) - for (int col = firstCol; col <= lastCol; col++) - { - if (!map.IsOccupied(row, col)) continue; - GetCellBoundsMeters(map, row, col, out double cellMinX, out double cellMaxX, out double cellMinY, out double cellMaxY); - if (OrientedRectangleCellIntersection.Intersects(footprint, cellMinX, cellMaxX, cellMinY, cellMaxY)) return true; - } - return false; - } - - private static void GetCellRange(PlanningGridMap map, double minX, double maxX, double minY, double maxY, - out int firstRow, out int lastRow, out int firstCol, out int lastCol) - { - double minimumMapX = map.Bounds.XMin / 1000d; - double minimumMapY = map.Bounds.YMin / 1000d; - firstCol = Clamp((int)Math.Floor((minX - minimumMapX) / map.ResolutionMeters) - 1, 0, map.Cols - 1); - lastCol = Clamp((int)Math.Floor((maxX - minimumMapX) / map.ResolutionMeters), 0, map.Cols - 1); - firstRow = Clamp((int)Math.Floor((minY - minimumMapY) / map.ResolutionMeters) - 1, 0, map.Rows - 1); - lastRow = Clamp((int)Math.Floor((maxY - minimumMapY) / map.ResolutionMeters), 0, map.Rows - 1); - } - - private static void GetCellBoundsMeters(PlanningGridMap map, int row, int col, - out double cellMinX, out double cellMaxX, out double cellMinY, out double cellMaxY) - { - cellMinX = map.Bounds.XMin / 1000d + col * map.ResolutionMeters; - cellMinY = map.Bounds.YMin / 1000d + row * map.ResolutionMeters; - cellMaxX = Math.Min(map.Bounds.XMax / 1000d, cellMinX + map.ResolutionMeters); - cellMaxY = Math.Min(map.Bounds.YMax / 1000d, cellMinY + map.ResolutionMeters); - } - - private static int Clamp(int value, int minimum, int maximum) - { - return value < minimum ? minimum : value > maximum ? maximum : value; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs deleted file mode 100644 index 53234be..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; - -/// 旋转矩形与轴对齐栅格的分离轴相交判定。 -internal static class OrientedRectangleCellIntersection -{ - /// 任一投影轴没有严格分离时返回 true;擦边按相交处理。 - public static bool Intersects(VehicleFootprint rectangle, double cellMinX, double cellMaxX, double cellMinY, double cellMaxY) - { - if (rectangle == null || cellMaxX < cellMinX || cellMaxY < cellMinY) return false; - double cellCenterX = (cellMinX + cellMaxX) / 2d; - double cellCenterY = (cellMinY + cellMaxY) / 2d; - double cellHalfX = (cellMaxX - cellMinX) / 2d; - double cellHalfY = (cellMaxY - cellMinY) / 2d; - - return !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, rectangle.AxisLongitudinalX, rectangle.AxisLongitudinalY) && - !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, rectangle.AxisLateralX, rectangle.AxisLateralY) && - !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, 1d, 0d) && - !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, 0d, 1d); - } - - private static bool HasStrictSeparation(VehicleFootprint rectangle, double cellCenterX, double cellCenterY, double cellHalfX, double cellHalfY, - double axisX, double axisY) - { - double rectangleCenter = rectangle.CenterX * axisX + rectangle.CenterY * axisY; - double cellCenter = cellCenterX * axisX + cellCenterY * axisY; - double rectangleRadius = rectangle.HalfLengthMeters * Math.Abs(rectangle.AxisLongitudinalX * axisX + rectangle.AxisLongitudinalY * axisY) + - rectangle.HalfWidthMeters * Math.Abs(rectangle.AxisLateralX * axisX + rectangle.AxisLateralY * axisY); - double cellRadius = cellHalfX * Math.Abs(axisX) + cellHalfY * Math.Abs(axisY); - return Math.Abs(rectangleCenter - cellCenter) > rectangleRadius + cellRadius; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs deleted file mode 100644 index aa88e55..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; - -/// 以车辆几何中心为原点的扩大旋转矩形。 -internal sealed class VehicleFootprint -{ - private VehicleFootprint(Pose2D pose, double halfLengthMeters, double halfWidthMeters) - { - CenterX = pose.X; - CenterY = pose.Y; - HalfLengthMeters = halfLengthMeters; - HalfWidthMeters = halfWidthMeters; - AxisLongitudinalX = Math.Cos(pose.Heading); - AxisLongitudinalY = Math.Sin(pose.Heading); - AxisLateralX = -AxisLongitudinalY; - AxisLateralY = AxisLongitudinalX; - CircumscribedRadiusMeters = Math.Sqrt(halfLengthMeters * halfLengthMeters + halfWidthMeters * halfWidthMeters); - - double minX = double.PositiveInfinity; - double maxX = double.NegativeInfinity; - double minY = double.PositiveInfinity; - double maxY = double.NegativeInfinity; - for (int index = 0; index < 4; index++) - { - GetCorner(index, out double x, out double y); - minX = Math.Min(minX, x); - maxX = Math.Max(maxX, x); - minY = Math.Min(minY, y); - maxY = Math.Max(maxY, y); - } - MinX = minX; - MaxX = maxX; - MinY = minY; - MaxY = maxY; - } - - public double CenterX { get; } - public double CenterY { get; } - public double HalfLengthMeters { get; } - public double HalfWidthMeters { get; } - public double AxisLongitudinalX { get; } - public double AxisLongitudinalY { get; } - public double AxisLateralX { get; } - public double AxisLateralY { get; } - public double CircumscribedRadiusMeters { get; } - public double MinX { get; } - public double MaxX { get; } - public double MinY { get; } - public double MaxY { get; } - - /// 创建包含车辆安全余量和临时扫掠余量的矩形。 - public static bool TryCreate(Pose2D pose, VehicleParameters vehicle, double additionalMarginMeters, out VehicleFootprint footprint) - { - footprint = null; - if (pose == null || vehicle == null || !NumericGuard.IsFinite(pose.X) || !NumericGuard.IsFinite(pose.Y) || - !NumericGuard.IsFinite(pose.Heading) || !NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || - !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || - vehicle.SafetyMarginMeters < 0d || !NumericGuard.IsFinite(additionalMarginMeters) || additionalMarginMeters < 0d) - return false; - - double totalMarginMeters = vehicle.SafetyMarginMeters + additionalMarginMeters; - if (!NumericGuard.IsFinite(totalMarginMeters)) return false; - double halfLengthMeters = vehicle.LengthMeters / 2d + totalMarginMeters; - double halfWidthMeters = vehicle.WidthMeters / 2d + totalMarginMeters; - if (!NumericGuard.IsPositiveFinite(halfLengthMeters) || !NumericGuard.IsPositiveFinite(halfWidthMeters)) return false; - - footprint = new VehicleFootprint(pose, halfLengthMeters, halfWidthMeters); - return NumericGuard.IsFinite(footprint.CircumscribedRadiusMeters) && NumericGuard.IsFinite(footprint.MinX) && - NumericGuard.IsFinite(footprint.MaxX) && NumericGuard.IsFinite(footprint.MinY) && NumericGuard.IsFinite(footprint.MaxY); - } - - /// 获取指定角点。索引按逆时针顺序为 0 到 3。 - public void GetCorner(int index, out double x, out double y) - { - double longitudinalSign; - double lateralSign; - switch (index) - { - case 0: longitudinalSign = 1d; lateralSign = 1d; break; - case 1: longitudinalSign = -1d; lateralSign = 1d; break; - case 2: longitudinalSign = -1d; lateralSign = -1d; break; - case 3: longitudinalSign = 1d; lateralSign = -1d; break; - default: throw new ArgumentOutOfRangeException(nameof(index)); - } - x = CenterX + longitudinalSign * HalfLengthMeters * AxisLongitudinalX + lateralSign * HalfWidthMeters * AxisLateralX; - y = CenterY + longitudinalSign * HalfLengthMeters * AxisLongitudinalY + lateralSign * HalfWidthMeters * AxisLateralY; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs deleted file mode 100644 index 88bed55..0000000 --- a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; - -/// -/// 从车辆参数提取运动学限制的辅助方法。 -/// 曲率单位为 1/m;当最大曲率与最小转弯半径同时给出时,始终选择更保守的较小曲率。 -/// -public static class VehicleKinematics -{ - /// - /// 尝试获取车辆允许的最大绝对曲率。 - /// 参数:vehicle 为车辆参数;maximumCurvaturePerMeter 为输出的正有限曲率,单位 1/m。 - /// 返回:至少提供一种正有限曲率限制时为 true;任一已提供限制无效或两种限制均未提供时为 false。 - /// - public static bool TryGetMaximumCurvaturePerMeter(VehicleParameters vehicle, out double maximumCurvaturePerMeter) - { - maximumCurvaturePerMeter = 0d; - if (vehicle == null) return false; - - bool hasMaximumCurvature = vehicle.MaximumCurvaturePerMeter.HasValue; - bool hasMinimumRadius = vehicle.MinimumTurningRadiusMeters.HasValue; - if (hasMaximumCurvature && !NumericGuard.IsPositiveFinite(vehicle.MaximumCurvaturePerMeter.Value)) return false; - if (hasMinimumRadius && !NumericGuard.IsPositiveFinite(vehicle.MinimumTurningRadiusMeters.Value)) return false; - if (!hasMaximumCurvature && !hasMinimumRadius) return false; - - if (hasMaximumCurvature && hasMinimumRadius) - { - maximumCurvaturePerMeter = System.Math.Min( - vehicle.MaximumCurvaturePerMeter.Value, - 1d / vehicle.MinimumTurningRadiusMeters.Value); - return true; - } - - maximumCurvaturePerMeter = hasMaximumCurvature - ? vehicle.MaximumCurvaturePerMeter.Value - : 1d / vehicle.MinimumTurningRadiusMeters.Value; - return NumericGuard.IsPositiveFinite(maximumCurvaturePerMeter); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html b/.task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html deleted file mode 100644 index 6269f51..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html +++ /dev/null @@ -1,812 +0,0 @@ - - - - - -AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案 - - - -
-

AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案

-

- 面向四舵轮AMR的第一阶段工程实现:暂不考虑蟹行与纯横移,仅采用车式运动模型, - 支持前进、倒车及换向,输出供后续SQP使用的无时间空间粗路径。 - 本文统一整理已经确认的第1至第14步,作为软件设计、编码、调试与验收依据。 -

-
- 步骤1–12:已确认 - 步骤13:性能优化TODO - 步骤14:接口与验收已确认 - 地图分辨率初值:0.05 m - 原语长度:0.50 m - 积分步长:0.05 m -
-
- -
- - -
-
-

0. 总体概览

-
-
车式运动
车头/车尾方向运动,不考虑蟹行
-
前进 + 倒车
允许在原语边界切换方向
-
空间粗路径
不包含速度、加速度、时间戳
-
- -

0.1 总体职责边界

- - - - - - -
模块主要职责不负责
Hybrid A*拓扑可达、无碰撞、基本运动学可行、前进/倒车结构、目标位置与航向接近最终速度、加速度、时间、四轮舵角与轮速
路径后处理回溯、稠密点恢复、关键点保留、弧长重采样、快速曲线平滑、完整复核动态约束和最终轨迹时间参数化
后续SQP终点精确收敛、速度/加速度/角速度/时间戳、换向点停车、动态可跟踪性重新决定绕障侧或凭空增加倒车拓扑
底盘逆解与控制四轮舵角、轮速分配和轨迹跟踪全局绕障搜索
- -

0.2 总体数据流

-
-
感知障碍物 / 点云 → 过滤、投影到二维
-
二维占据栅格 + 障碍物距离场
-
起点、终点、车辆参数、规划配置
-
二维Dijkstra启发图 + Hybrid A*搜索
-
机会式/强制 Reeds–Shepp 终点连接
-
父节点回溯 + 0.05 m内部积分点恢复
-
关键点保留 + 分方向弧长重采样
-
三次B样条优先,局部Bézier/五次多项式,局部QP兜底
-
最终碰撞、净空、曲率、换向结构校验
-
输出空间粗路径 → 后续SQP
-
- -
-第一版核心原则:先保证算法完整跑通、路径正确、可解释、可复现;性能优化整体放入第十三步TODO,待前12步稳定后再开展。 -
-
- -
-

1任务边界与输出定义 已确认

-

1.1 车辆运动模式

-

目标平台为四舵轮AMR,但第一阶段只使用车式运动能力:

-
    -
  • 车辆沿车头或车尾方向运动;
  • -
  • 允许前进、倒车以及换向;
  • -
  • 暂不考虑蟹行、纯横移和任意方向全向运动;
  • -
  • 车体航向角始终表示车头朝向,即使车辆正在倒车。
  • -
- -

1.2 Hybrid A*最小输出

-

输出空间路径点序列,至少包含:

-
位置:x, y
-车体航向:heading
-运动方向:Forward / Reverse
-累计弧长:s
-参考/几何曲率:kappa
-换向标记:isGearSwitchPoint
-安全净空:clearance
-

不输出速度、加速度、角速度、时间戳、四个舵轮角度与轮速。

- -

1.3 成功判据

-
    -
  • 路径全程无碰撞且不进入未知区域;
  • -
  • 粗路径基本满足最大曲率约束;
  • -
  • 包含正确绕障侧、前进/后退和换向结构;
  • -
  • 终点位置与航向满足第九步终止规则;
  • -
  • 可作为后续SQP的有效初值。
  • -
-
- -
-

2地图表达与环境输入 已确认

-

2.1 地图生成流程

-
原始点云 / 障碍信息 - ↓ 过滤噪声、地面、离群点 -投影到二维平面 - ↓ -二维占据栅格 Occupancy Grid - ↓ -障碍物距离场 Distance Field / EDT
- -

2.2 栅格定义

-
    -
  • 地图分辨率初值:0.05 m
  • -
  • 状态:Free、Occupied、Unknown;
  • -
  • 第一版中Unknown按障碍物处理;
  • -
  • 地图包含Origin、Resolution、Width、Height及Version;
  • -
  • 算法内部不得写死分辨率,统一读取地图参数。
  • -
- -

2.3 车体碰撞模型

-

AMR使用旋转矩形车体碰撞模型,不能只用中心点或单圆近似。安全余量主要加到车体矩形上:

-
CheckLength = VehicleLength + 2 × SafetyMargin
-CheckWidth  = VehicleWidth  + 2 × SafetyMargin
-

初始安全余量为0.03 m。避免地图膨胀与车体扩大同时重复计算同一余量。

-
- -
-

3车辆运动模型与最大曲率 已确认

-

3.1 参考点

-

车辆状态参考点采用AMR几何中心。

- -

3.2 基于路径弧长的运动学

-
dx/ds = cos(theta)
-dy/ds = sin(theta)
-dtheta/ds = kappa
-

其中theta为车头航向角,kappa为车体中心路径曲率。

- -

3.3 最大曲率来源

-

必须同时支持两种来源:

-
    -
  1. 外部直接输入MaxCurvature,或输入MinTurningRadius并换算kappa_max = 1 / R_min
  2. -
  3. 由四轮转向几何调用ComputeMaxCurvature(VehicleKinematicParameters)计算。
  4. -
-

推荐三种模式:

-
ExternalOnly
-GeometryOnly
-ConservativeMinimum
-

当两种来源同时存在时,建议取更保守的较小值。若均无有效值,则返回配置错误。

- -
-曲率变化率不在本步骤强制建模。第一版先保证曲率不超过上限;渐变曲率原语和实车舵轮速率约束作为后续升级。 -
-
- -
-

4Hybrid A*搜索节点状态 已确认

-

4.1 连续状态

-
(x, y, theta, direction, curvatureIndex)
-
    -
  • x, y, theta保持连续值用于运动积分;
  • -
  • direction为Forward或Reverse;
  • -
  • curvatureIndex表示当前离散曲率等级。
  • -
- -

4.2 Closed Set键

-
(ix, iy, iHeading, direction, curvatureIndex)
-

推荐初始离散:

-
    -
  • 位置Closed Set分辨率:0.05–0.10 m
  • -
  • 航向离散:
  • -
  • 方向和曲率等级均进入键,避免把运动状态不同的节点错误合并。
  • -
- -

4.3 搜索元数据

-

节点还应保存G/H/F代价、父节点索引、生成当前节点的原语积分点、最小净空、终止原因等。这些属于搜索管理信息,不属于车辆物理状态。

-
- -
-

5运动原语与扩展规则 已确认

-

5.1 第一版原语集合

-
{ -kappa_max, -0.5 kappa_max, 0, 0.5 kappa_max, kappa_max }
-

每种曲率均支持Forward与Reverse。

- -

5.2 曲率切换限制

-

相邻原语曲率等级最多变化1级。例如:

-
允许:0 → 0.5κmax
-允许:0.5κmax → κmax
-不允许:κmax → -κmax
-

该限制可减少“左打死后立即右打死”的不合理跳变。

- -

5.3 换向规则

-
    -
  • 前进/倒车只能在运动原语边界切换;
  • -
  • 起始曲率若上层可提供则使用实际值,否则默认0;
  • -
  • 第一版采用恒定曲率原语。
  • -
- -
-TODO:当恒定曲率原语造成曲率跳变过大、平滑后仍不可跟踪或实车舵轮变化速度不足时,增加线性渐变曲率原语。 -
-
- -
-

6原语长度、积分与终止 已确认

-

6.1 固定参数

-
PrimitiveLength = 0.50 m
-IntegrationStep = 0.05 m
-

完整原语最多包含10个内部积分点。只有原语终点进入Open List,内部积分点用于碰撞、净空和最终路径恢复。

- -

6.2 终止条件

- - - - - - - -
条件处理
累计长度达到0.50 m生成正常候选节点
任一积分点碰撞原语无效,立即终止
越界或进入未知区域原语无效
中途满足目标规则提前终止并保存实际有效段
Reeds–Shepp连接成功结束搜索
- -
-TODO:后续根据障碍物距离、目标距离和窄通道自动调整原语长度;碰撞积分步长仍保持0.05 m。 -
-
- -
-

7碰撞检测与航向栅格模板 已确认

-

7.1 两级碰撞检测

-
    -
  1. 距离场快速放行:若车辆中心到最近障碍物距离大于扩大车体外接圆半径及离散补偿,则该点必然安全;
  2. -
  3. 旋转矩形模板精确检测:距离不足以保证安全时,检查当前航向下扩大车体覆盖的所有栅格。
  4. -
- -

7.2 外接圆仅用于“安全放行”

-
R_outer = 0.5 × sqrt(CheckLength² + CheckWidth²)
-

D > R_outer表示安全;D ≤ R_outer只表示“无法确定”,不等于碰撞,必须进入矩形模板检查。

- -

7.3 航向栅格模板的准备

-

航向分辨率5°时,在初始化阶段自动生成72组模板:

-
0°, 5°, 10°, ... , 355°
-

每个模板保存旋转后的扩大车体矩形与地图栅格相交所对应的相对栅格偏移:

-
GridOffset { Dx, Dy }
-

运行时把当前车辆中心转换为栅格坐标,选择最近航向模板,将模板中的相对偏移平移到当前位置并查询占据状态。

- -

7.4 模板生成规则

-
    -
  • 模板以车体几何中心为原点;
  • -
  • 车体尺寸包含安全余量与少量离散补偿;
  • -
  • 只要栅格方块与旋转矩形有交集,就加入模板;
  • -
  • 不能只判断栅格中心是否在矩形内部,否则可能漏掉边角碰撞;
  • -
  • 真实车体始终是矩形,模板呈阶梯状只是栅格化结果。
  • -
- -

7.5 每个积分点的处理

-
读取距离场
-  ↓
-可安全放行?——是→继续
-  ↓否
-选择航向模板
-  ↓
-平移模板并查询Occupied / Unknown / 越界
-  ↓
-任一命中→碰撞;全部通过→安全
-
- -
-

8搜索代价与启发函数 已确认

-

8.1 节点排序

-
f(n) = g(n) + w_h × h(n)
-

第一版HeuristicWeight = 1.3

- -

8.2 累计代价

-
g_new =
-g_parent
-+ C_length
-+ C_reverse
-+ C_switch
-+ C_curvature
-+ C_curvatureChange
-+ C_obstacle
-
    -
  • 路径长度是基础代价;
  • -
  • 后退系数初值ReversePenalty = 1.3
  • -
  • 换向固定惩罚初值GearSwitchPenalty = 2.0
  • -
  • 曲率代价轻度抑制长期极限转向;
  • -
  • 曲率变化代价偏好平缓动作序列;
  • -
  • 障碍物代价让路径尽量远离障碍物。
  • -
- -

8.3 双启发

-
h = max(h_2D, h_RS)
-
    -
  • h_2D:从目标反向运行二维Dijkstra得到的绕障距离;
  • -
  • h_RS:基于当前位姿、目标位姿和最小转弯半径的Reeds–Shepp距离。
  • -
-

所有权重必须外部可配置。

-
- -
-

9终点判定、SQP精修与Reeds–Shepp 已确认

-

9.1 基本终点容差

-
GoalPositionTolerance = 0.15 m
-GoalHeadingTolerance  = 5°
-

在每个0.05 m内部积分点检查位置和航向误差,避免越过目标。

- -

9.2 容差到达与SQP

-

Hybrid A*可以在容差范围内结束,由后续SQP通过终端约束精确收敛到目标。但必须满足:

-
    -
  • 粗路径已包含正确绕障、倒车和换向拓扑;
  • -
  • 当前终点、目标位姿及末端局部区域无碰撞;
  • -
  • 终点附近存在足够局部调整空间;
  • -
  • SQP只负责局部修正,不负责创造新的换向或改变绕障侧。
  • -
- -

9.3 目标进入方向

-
GoalDirectionConstraint = Any | Forward | Reverse
-

默认Any。该字段约束最后一段运动方向,不改变目标车头航向定义。

- -

9.4 Reeds–Shepp模式

- - - - - -
模式连接失败后的行为适用场景
Disabled不尝试,满足容差即可结束开阔终点、调试对照
Opportunistic失败继续搜索,满足容差仍可交SQP默认、普通导航
RequiredNearGoal失败不能按容差结束,必须继续找可连接节点狭窄钻入、精确泊入
- -

9.5 自动触发

-
2–5 m:每扩展10个节点尝试一次
-≤2 m:每扩展3个节点尝试一次
-采样碰撞步长:0.05 m
-

连接路径必须检查地图边界、Unknown、扩大车体碰撞、安全净空、最大曲率、方向约束及与当前曲率的衔接。

- -

9.6 默认策略

-
ReedsSheppMode = Opportunistic
-

无需人工每次开启,由程序自动触发。任务类型可覆盖默认模式。

-
- -
-

10搜索回溯与原始稠密路径 已确认

-

10.1 回溯内容

-

搜索成功后从终止节点沿父节点索引回溯到起点,反转节点链,并恢复每条原语保存的0.05 m内部积分点。

- -

10.2 不能只输出0.5 m搜索节点

-

只输出原语终点会丢失圆弧形状、碰撞检查细节和换向局部结构。因此必须复用搜索时已经计算的内部点。

- -

10.3 路径点字段

-
x, y
-heading, unwrappedHeading
-curvature
-direction
-arcLength
-clearance
-isGearSwitchPoint
-source
- -

10.4 特殊处理

-
    -
  • 删除相邻原语重复边界点;
  • -
  • 中途终止原语只保留真实有效积分点;
  • -
  • Reeds–Shepp连接点追加到普通搜索路径之后;
  • -
  • 机会式容差终点不允许在本步骤被“硬改”为目标点;
  • -
  • 航向同时保存归一化值和连续解缠值;
  • -
  • 累计弧长始终单调增加,方向单独保存;
  • -
  • 按换向点划分Forward/Reverse连续段。
  • -
-
- -
-

11关键点保留与弧长重采样 已确认

-

11.1 处理目标

-

原始0.05 m稠密路径不直接全部送入平滑和SQP。先保留关键结构,再按照累计弧长重采样。

- -

11.2 必须保留的关键点

-
    -
  • 起点与终点;
  • -
  • 所有换向点;
  • -
  • 曲率等级变化点;
  • -
  • 直线/弯道切换点;
  • -
  • Reeds–Shepp分段和连接点;
  • -
  • 障碍物附近最小净空点;
  • -
  • 必要的明显航向变化点。
  • -
- -

11.3 分方向重采样

-

每个Forward或Reverse段独立处理,不能跨越换向点插值。

- -

11.4 推荐间距

-
普通区域:0.10 m
-重点区域:0.05 m
-

以下区域采用0.05 m:

-
    -
  • 障碍距离小于0.50 m;
  • -
  • |κ| ≥ 0.5 κmax;
  • -
  • 目标2 m范围内;
  • -
  • 换向点前后0.30 m;
  • -
  • 曲率变化点前后0.25 m;
  • -
  • Reeds–Shepp连接段与窄通道。
  • -
- -

11.5 重采样后复核

-

新插值点必须重新执行车体碰撞检查。最终输出点可为0.10 m,但碰撞验证内部步长仍不得大于0.05 m。

-
- -
-

12快速曲线平滑、局部QP兜底与最终校验 已确认

-

12.1 最终采用的分层方案

-
-
按Forward/Reverse方向段拆分
-
普通路径段:三次B样条快速平滑
-
局部短转角:分段三次Bézier
-
末端短连接:五次多项式(可选)
-
0.05 m完整碰撞、净空、曲率验证
-
失败局部:安全走廊约束QP
-
局部QP仍失败:回退第十一步原始可行路径
-
- -

12.2 为什么不默认全路径QP

-

全路径多轮QP还需要走廊生成、曲率线性化和反复碰撞检测,可能与后续SQP功能重复并增加耗时。因此第一版采用快速曲线优先,仅对失败局部使用QP。

- -

12.3 平滑结构约束

-
    -
  • 换向点固定,不能使用一条曲线跨越换向;
  • -
  • 不改变绕障侧、前进/后退顺序和换向次数;
  • -
  • Reeds–Shepp精确目标点固定;
  • -
  • 容差终点不在本步骤强行修改为目标;
  • -
  • 障碍物附近控制点移动范围收紧。
  • -
- -

12.4 典型偏移范围

-
普通区域最大偏移:0.10–0.15 m
-障碍物附近最大偏移:0.02–0.05 m
-局部QP问题区间:失败点前后各约1.0 m
- -

12.5 航向与曲率

-
    -
  • 平滑后必须根据新几何路径重新计算切线与航向;
  • -
  • 前进段车头航向与路径切线一致;
  • -
  • 倒车段车头航向与路径切线相差π;
  • -
  • 区分几何曲率与车辆控制曲率;
  • -
  • 建议平滑后最大车辆曲率不超过0.95 κmax
  • -
- -

12.6 最终验证

-
    -
  • 0.05 m采样完整车体碰撞检测;
  • -
  • Unknown与越界检查;
  • -
  • 安全余量检查;
  • -
  • 最大曲率与曲率变化检查;
  • -
  • 起终点、换向点、方向分段和路径连续性检查。
  • -
-
- -
-

13性能优化阶段 整体TODO

-

第十三步不作为第一版主流程实现要求。前12步完整跑通并建立正确性基线后,再进行性能剖析与针对性优化。

- -

13.1 第一版仅保留防失控措施

-
    -
  • 较宽松的搜索超时;
  • -
  • 最大扩展节点和最大生成节点限制;
  • -
  • 取消请求机制;
  • -
  • 各阶段基础耗时与节点数量统计。
  • -
- -

13.2 后续性能TODO

-
    -
  1. 统计地图处理、Dijkstra、Hybrid A*、碰撞检测、Reeds–Shepp、回溯和平滑耗时;
  2. -
  3. 根据剖析结果识别真实瓶颈;
  4. -
  5. 优化Open List、Closed Set、节点内存与对象分配;
  6. -
  7. 预计算运动原语、航向模板、距离场和启发图;
  8. -
  9. 地图裁剪、路径热启动、增量更新、多分辨率搜索;
  10. -
  11. 最终验证平均、P95、最坏时间和100 ms达成率。
  12. -
- -
-开发顺序:正确性 → 可行性 → 可复现性 → 性能剖析 → 针对性优化 → 100 ms验收。 -
-
- -
-

14输入输出接口、错误状态与验收 已确认

-

14.1 推荐模块

-
HybridAStarPlanner
-├── MapProcessor
-├── VehicleModel
-├── MotionPrimitiveGenerator
-├── CollisionChecker
-├── HeuristicProvider
-├── ReedsSheppConnector
-├── GoalChecker
-├── PathBacktracker
-├── PathResampler
-├── PathSmoother
-├── PathValidator
-└── PlanningDiagnostics
- -

14.2 主接口

-
public interface IHybridAStarPlanner
-{
-    PlanningResult Plan(PlanningRequest request);
-}
- -

14.3 输入

-
    -
  • 二维占据栅格;
  • -
  • 起点位姿与可选起始方向、起始曲率;
  • -
  • 目标位姿与目标进入方向;
  • -
  • 车辆尺寸、安全余量、最大曲率/最小转弯半径;
  • -
  • Hybrid A*配置、Reeds–Shepp模式和平滑配置。
  • -
- -

14.4 输出

-

输出不带时间的空间路径点和分段信息:

-
X, Y
-Heading, UnwrappedHeading
-ArcLength
-GeometricCurvature
-VehicleCurvature
-Direction
-BodyClearance
-IsGearSwitchPoint
-Source
- -

14.5 终点到达类型

-
ExactByReedsShepp
-ReachedWithinTolerance
-ExactAfterDirectSearch
- -

14.6 主要返回状态

-
Success
-SuccessWithToleranceGoal
-SuccessWithSmoothingFallback
-InvalidRequest / InvalidMap / InvalidVehicleParameters
-StartOutsideMap / StartInCollision / StartInUnknownArea
-GoalOutsideMap / GoalInCollision / GoalInUnknownArea
-InvalidCurvatureConfiguration
-SearchTimeout / SearchNodeLimitExceeded / NoFeasiblePath
-ReedsSheppRequiredButFailed
-BacktrackingFailed / ResamplingFailed / FinalValidationFailed
-Cancelled / InternalError
- -

14.7 第一版验收重点

-
    -
  • 有效场景能输出完整路径;
  • -
  • 起点一致,终点满足第九步规则;
  • -
  • 完整车体全程无碰撞且不进入Unknown;
  • -
  • 最大曲率不超过车辆上限;
  • -
  • 前进、倒车和换向结构正确;
  • -
  • 回溯无断点和异常重复;
  • -
  • 平滑失败可以回退原路径;
  • -
  • 失败场景返回明确状态;
  • -
  • 后续SQP能够读取并收敛。
  • -
-
- -
-

附录A:第一版推荐配置参数

- - - - - - - - - - - - - - - - - - - - - - - - - - -
参数推荐初值说明
MapResolution0.05 m读取地图配置,不写死
HeadingResolution72个航向模板
SafetyMargin0.03 m主要加在车体矩形
PrimitiveLength0.50 m第一版固定
IntegrationStep0.05 m碰撞与积分步长
CurvatureLevels-1,-0.5,0,0.5,1 × κmax5级
GoalPositionTolerance0.15 mHybrid A*基础容差
GoalHeadingToleranceHybrid A*基础容差
HeuristicWeight1.3Weighted A*
ReversePenalty1.3允许倒车但略微惩罚
GearSwitchPenalty2.0减少频繁换向
ReedsSheppModeOpportunistic默认自动机会式
AnalyticExpansionDistance5.0 m进入后周期尝试
NearGoalDistance2.0 m提高尝试频率
AnalyticExpansionInterval10节点2–5 m
NearGoalInterval3节点≤2 m
NormalResampleSpacing0.10 m普通区域
FineResampleSpacing0.05 m重点区域
FineObstacleDistance0.50 m小于此值加密
FineGoalDistance2.0 m目标附近加密
GearSwitchDenseRange±0.30 m换向点附近
CurvatureTransitionDenseRange±0.25 m曲率切换附近
CurvatureLimitRatio0.95平滑后留控制余量
SearchTimeLimitMs3000–5000 ms第一版防失控,不是性能指标
-
- -
-

附录B:总体伪代码

-
PlanningResult Plan(request)
-{
-    ValidateRequest(request);
-    ValidateMap(request.Map);
-    ResolveVehicleCurvatureLimit(request.Vehicle);
-    ValidateStartAndGoal();
-
-    PrepareDistanceFieldIfNeeded();
-    PrepareHeadingFootprintTemplatesIfNeeded();
-    BuildGoalDijkstraHeuristic();
-
-    startNode = CreateStartNode();
-    PushOpen(startNode);
-
-    while (OpenList not empty)
-    {
-        CheckCancellationAndSafetyLimits();
-
-        current = PopBestValidNode();
-
-        if (ShouldTryReedsShepp(current))
-        {
-            rsPath = TryReedsShepp(current, goal);
-            if (ValidateAnalyticPath(rsPath))
-                return BuildFinalResult(current, rsPath);
-        }
-
-        if (GoalChecker.CanTerminateByTolerance(current))
-            return BuildFinalResult(current, noAnalyticPath);
-
-        foreach (primitive in GenerateAllowedPrimitives(current))
-        {
-            integrated = IntegratePrimitive(
-                current,
-                primitive,
-                step = 0.05 m,
-                maxLength = 0.50 m);
-
-            if (!integrated.Valid)
-                continue;
-
-            child = CreateChildNode(integrated);
-            if (!ImproveBestCost(child))
-                continue;
-
-            PushOpen(child);
-        }
-    }
-
-    return Failure(NoFeasiblePath);
-}
-
-BuildFinalResult(goalNode, analyticPath)
-{
-    rawDense = BacktrackAndRestorePrimitivePoints(goalNode);
-    AppendAnalyticPathIfAny(rawDense, analyticPath);
-
-    segmented = SplitByMotionDirection(rawDense);
-    resampled = PreserveKeyPointsAndResample(segmented);
-
-    smoothed = TryFastSplineSmoothing(resampled);
-    if (!ValidatePath(smoothed))
-        smoothed = TryLocalCurveOrQPFallback(resampled);
-
-    if (!ValidatePath(smoothed))
-        smoothed = resampled;
-
-    if (!ValidatePath(smoothed))
-        return Failure(FinalValidationFailed);
-
-    return Success(smoothed, diagnostics);
-}
-
- -
-

附录C:建议开发里程碑

- - - - - - - - - - - -
阶段范围完成标准
M0 数据与工具Pose、地图、车辆参数、角度/坐标工具单元测试通过
M1 运动学与碰撞恒曲率积分、航向模板、矩形碰撞可视化验证不同角度无漏检
M2 最小Hybrid A*无障碍、只前进、基础启发稳定从起点到终点
M3 障碍与倒车Dijkstra、前进/倒车、换向代价绕障与一次倒车场景通过
M4 终点模块容差、GoalDirection、Reeds–Shepp三模式普通与狭窄目标场景通过
M5 后处理回溯、0.05 m稠密点、分段、重采样路径结构无断点、换向明确
M6 平滑与复核B样条、Bézier、局部QP兜底不改变拓扑,失败可回退
M7 接口与集成PlanningRequest/Result、状态、诊断可接入SQP
M8 性能TODO剖析、优化、100 ms目标在正确性基线后执行
-
-
-
- -
- 文档用途:四舵轮AMR第一阶段非结构化道路粗路径规划开发依据。内部单位统一采用米、弧度、1/米。 -
- - diff --git a/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md b/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md deleted file mode 100644 index 9ee4f13..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md +++ /dev/null @@ -1,726 +0,0 @@ -# Hybrid A* 粗路径规划实施计划 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。 - -**目标:** 在 ClumsyPilot `netstandard2.0` 项目中交付可复用的静态规划地图和 Hybrid A* 粗路径规划模块,通过一次 `CoarsePathPlanningService.Plan(job)` 完成建图、快照复用、粗路径搜索和可选调试发布。 - -**架构:** `Map` 模块统一接收人工障碍、TwoLeg 及未来障碍来源,生成只含外部障碍物的不可变 `PlanningGridMap`;`HybridAStarPlanner` 只消费该快照并完成保守碰撞检查与搜索;`CoarsePathPlanningService` 负责编排。核心逻辑不读取传感器、UI 或系统时间,Clumsy `MovementTest` 和 PNG 导出只作为旁路适配器。 - -**技术栈:** C# 10、.NET Standard 2.0、PowerShell 反射契约测试、Clumsy `MovementTest`、StbImageWriteSharp 1.16.7。 - -**设计依据:** `docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md` - -## 全局约束 - -- 所有命令均从仓库根目录执行;项目文件为 `ClumsyPilot/ClumsyPilot.csproj`。 -- 目标框架保持 `netstandard2.0`,不得直接使用 `PriorityQueue`、`Math.Clamp`、`double.IsFinite` 或依赖 `record/init` 的实现。 -- 地图边界、人工障碍和 TwoLeg 快照使用世界坐标 mm;规划内部统一使用 m、rad、1/m。 -- 地图只保存外部障碍物,不写入 AMR 自身足迹,不在地图侧添加车辆安全膨胀。 -- 安全余量只在碰撞检查时扩大车体矩形,防止地图膨胀与车体膨胀重复计算。 -- `ResolutionMm` 必须在 20~200 mm;地图最多 4,000,000 格,分配前使用 `checked` 检查。 -- 地图坐标采用 `[XMin, XMax) × [YMin, YMax)`;地图外始终按占据处理。 -- `PrimitiveLengthMeters=0.50` 表示原语最大长度;`IntegrationStepMeters=0.05` 表示积分最大步长。 -- 碰撞采样中心位移不得超过 `min(0.025 m, Map.ResolutionMeters / 2)`。 -- 默认终点容差为 0.15 m 和 5°。每个原语在内部积分点逐点检查目标,第一次满足条件即截断为终点候选。 -- 生成终点候选时不能立即成功;候选必须进入 Open List,作为当前最佳有效条目出队时才成功。 -- 第一版只支持恒曲率前进、倒车及原语边界换向;不支持蟹行、横移、原地旋转、Reeds-Shepp 精确连接、平滑、速度规划或底盘控制。 -- PowerShell 脚本首行设置 `$ErrorActionPreference = 'Stop'`,统一加载 `bin/Debug/netstandard2.0/ClumsyPilot.dll`。 -- 用户已明确不需要 Git 自检。本计划不包含 `git diff`、`git add`、`git commit` 等步骤;版本管理由用户另行处理。 - -## P0 与 P1 完成定义 - -| 阶段 | 必须完成的结果 | 阶段出口 | -| --- | --- | --- | -| P0-MAP:当前首要工作 | Map 文件结构、统一障碍来源、TwoLeg 投影、栅格化、距离场、快照缓存、PNG 迁移、自动测试和 `MovementTest.MapTest` 实际调用 | Map 独立构建成功;3 个 Map 脚本通过;Clumsy MapTest 只通过 `PlanningMapFactory.Create` 完成建图与显示 | -| P0-PLAN:Map Gate 之后 | CoarsePath 契约、保守碰撞、原语截断、Open List、Hybrid A*、输出校验和一次调用门面 | 6 个非 UI P0 脚本全部通过;固定场景可由 `CoarsePathPlanningService` 返回经最终复核的粗路径 | -| P1:集成与优化 | CoarsePath MovementTest、性能基准、旧地图退役和 README | 7 个功能脚本及 Release 基准通过;UI 入口只做调试展示;旧地图类型不再成为规划运行时入口 | - -执行顺序固定为“P0-MAP 独立闭环 → P0-PLAN → P1”。除了 Map 自己的 `MovementTest.MapTest` 和 PNG 调试辅助外,在 Map Gate 通过前不编写粗路径搜索或 CoarsePath UI,避免旧 `MovementTest.Trapmaptest.cs` 的传感器和渲染依赖进入核心模块。 - -## 当前首要里程碑:P0-MAP 独立闭环 - -当前阶段只处理 `Utils` 中被 Map 使用的无状态工具、完整 `Map` 目录、Map 自动化测试和 Map 实际调用。不得提前创建 `CoarsePath/Search`、`CoarsePath/Vehicle` 或 `CoarsePath/Output` 运行时代码。 - -### Map 实施顺序 - -1. 建立 `Map/Core`、`Map/Obstacles`、`Map/Sources`、`Map/Planning`、`Map/Test/Visualization` 目录和命名空间。 -2. 完成 `MapBoundsMm`、连续行优先 `EnvironmentGridMap`、圆/矩形 DTO 和唯一栅格化器。 -3. 完成统一 `IMapObstacleSource`、人工来源、TwoLeg 快照来源和事务式 `EnvironmentMapBuilder`。 -4. 完成 `PlanningGridMap`、精确 EDT、保守距离场和 mm→m 适配。 -5. 完成 `PlanningMapFactory`、容量 4 的两级快照缓存和地图变化判断。 -6. 将旧 `TrapMapImageExporter.cs` 能力拆到 `Map/Test/Visualization`,只消费只读快照。 -7. 完成三个 Map PowerShell 脚本和 `MovementTest.MapTest`,用真实入口验证建图、复用和显示。 - -对应详细任务的执行次序为:`Task 1 → Task 3 → Task 4 → Task 5 → Task 6 → Task 13 → Task 14 的 MapTest 部分 → Map Gate`。`Task 2` 和 `Task 7~12` 在 Map Gate 之后执行。 - -### Map 实际调用契约 - -`PlanningMapFactory` 应由长期存在的服务或测试对象持有,不能每次调用都重新 `new`,否则容量 4 的快照缓存无法跨调用复用: - -```csharp -private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); - -public PlanningMapBuildResult CreateCurrentMap( - MapBoundsMm bounds, - long manualVersion, - IReadOnlyList manualObstacles, - long twoLegVersion, - TwoLegProjectionInput twoLegSnapshot) -{ - return _mapFactory.Create(new PlanningMapRequest - { - Bounds = bounds, - ResolutionMm = 50f, - ObstacleSources = new IMapObstacleSource[] - { - new ManualObstacleSource( - "manual", manualVersion, true, manualObstacles), - new TwoLegObstacleSource( - "two-leg", twoLegVersion, false, twoLegSnapshot), - }, - AllowExplicitEmptyMap = false, - }); -} -``` - -调用方只判断统一结果,不接触 builder、rasterizer、投影器或 adapter: - -```csharp -PlanningMapBuildResult result = CreateCurrentMap( - bounds, manualVersion, manualObstacles, twoLegVersion, twoLegSnapshot); - -if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) - return; - -PlanningGridMap map = result.Map; -bool worldOriginIsOccupied = map.IsOccupiedWorld(0d, 0d); -double worldOriginClearance = - map.GetConservativeObstacleDistanceMeters(0d, 0d); -``` - -### Map Gate - -以下条件必须全部满足,才开始 CoarsePath 契约和搜索: - -- [ ] 新 `Map` 运行时代码不读取 `TwoLegDetect`、定位、Painter、Toast、UI 或系统时间。 -- [ ] 人工、TwoLeg 以及未来来源全部经 `IMapObstacleSource.ProjectToWorld()` 输出世界 mm 几何,再由唯一 rasterizer 写图。 -- [ ] 地图不写 AMR 自身,不使用旧默认 300 mm 膨胀。 -- [ ] `PlanningMapFactory.Create` 对完整输入命中、占据命中和占据变化给出可区分结果。 -- [ ] `MovementTest.MapTest` 只调用 `PlanningMapFactory` 和可选 PNG exporter,不复制地图构造逻辑。 -- [ ] 下列命令全部通过: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -``` - -### P0-MAP 执行状态(2026-07-27) - -- [x] 已完成 Task 1、Task 3~6、Task 13,以及 Task 14 的 `MovementTest.MapTest` 部分。 -- [x] 已建立新的 `Map` 目录结构、统一障碍物来源与 TwoLeg 检测时位姿投影;Map 核心不读取传感器、UI 或系统时钟。 -- [x] 已完成只读规划快照、保守 EDT 距离场、容量 4 的 LRU 缓存、PNG 可视化拆分和实际 MapTest 调用。 -- [x] 已通过上述 Map Gate 命令;另外 `verify_planning_utils.ps1` 也已通过。 -- [x] 未执行 Git 自检、暂存或提交。 - -## 最终目录与文件职责 - -```text -ClumsyPilot/ParkrobTrajplanner/ -├── Initial_plan/ ------ 方案与实施文档,不放运行时代码 -├── Utils/ ------ 通用、无状态、确定性数值工具 -│ ├── AngleMath.cs ------ 角度归一化、最短角差和航向离散索引 -│ ├── UnitConverter.cs ------ mm/m、deg/rad 和半径/曲率转换 -│ ├── CoordinateTransform.cs ------ 车体坐标与世界坐标二维刚体变换 -│ ├── NumericGuard.cs ------ 有限值、正值和参数范围校验 -│ └── GridIndex.cs ------ 不可变行列索引值对象 -├── Map/ ------ MultiWheelC.TrajectoryPlanning.Mapping -│ ├── Core/ -│ │ ├── EnvironmentGridMap.cs ------ 只保存外部障碍物的 mm 占据栅格 -│ │ ├── MapBoundsMm.cs ------ 有限、非退化的 mm 地图边界 -│ │ ├── MapBuildRequest.cs ------ 内部环境图构建请求 -│ │ ├── EnvironmentMapBuildResult.cs ------ 环境图、来源摘要和失败原因 -│ │ └── EnvironmentMapBuilder.cs ------ 校验来源并事务式合并障碍图层 -│ ├── Obstacles/ -│ │ ├── IMapObstacle.cs ------ 世界坐标障碍物公共几何契约 -│ │ ├── AxisAlignedRectangleObstacle.cs ------ 轴对齐矩形障碍 DTO -│ │ ├── CircleObstacle.cs ------ 圆形障碍 DTO -│ │ └── MapObstacleRasterizer.cs ------ 唯一占据栅格写入器 -│ ├── Sources/ -│ │ ├── IMapObstacleSource.cs ------ 纯快照障碍来源统一接口 -│ │ ├── ObstacleSourceStatus.cs ------ Applied/Empty/Unavailable/Invalid -│ │ ├── ObstacleProjectionResult.cs ------ 来源版本、状态、诊断和障碍集合 -│ │ ├── ManualObstacleSource.cs ------ 输出人工圆和矩形 -│ │ ├── TwoLegProjectionInput.cs ------ 两腿端点、检测状态和检测位姿快照 -│ │ ├── TwoLegObstacleSource.cs ------ TwoLeg 统一来源适配器 -│ │ └── TwoLegObstacleProjector.cs ------ 车体系两腿端点投影到世界系 -│ ├── Planning/ -│ │ ├── PlanningGridMap.cs ------ 只读 m 占据图、距离场和快照元数据 -│ │ ├── EuclideanDistanceTransform.cs ------ 线性时间精确二维欧氏距离变换 -│ │ ├── ObstacleDistanceField.cs ------ 不高估真实净空的距离下界 -│ │ ├── PlanningMapCache.cs ------ 容量 4 的线程安全两级快照缓存 -│ │ └── PlanningMapAdapter.cs ------ 占据深拷贝、mm→m 和距离场生成 -│ ├── PlanningMapRequest.cs ------ Map 模块统一输入 -│ ├── PlanningMapBuildResult.cs ------ Map 模块统一输出和缓存命中类型 -│ ├── PlanningMapFactory.cs ------ Map 模块唯一公共创建入口 -│ └── Test/ -│ ├── MovementTest.MapTest.cs ------ Clumsy UI 地图构建/显示入口 -│ └── Visualization/ -│ ├── PlanningMapImageExportRequest.cs ------ 快照、叠加层和输出选项 -│ ├── PlanningMapImageExportResult.cs ------ PNG 状态、路径、尺寸和诊断 -│ ├── PlanningMapImageExporter.cs ------ 校验、渲染编排和原子发布 -│ ├── PlanningMapImageRenderer.cs ------ 地图/车辆/路径绘制到 RGBA -│ └── ValidatedPngWriter.cs ------ Stb 编码、PNG 结构和 CRC 校验 -├── CoarsePath/ ------ MultiWheelC.TrajectoryPlanning.CoarsePath -│ ├── Contracts/ -│ │ ├── Pose2D.cs ------ m/rad 不可变二维位姿 -│ │ ├── TravelDirection.cs ------ Forward/Reverse -│ │ ├── GoalDirectionConstraint.cs ------ Any/Forward/Reverse -│ │ ├── VehicleParameters.cs ------ 尺寸、安全余量和最大曲率 -│ │ ├── PlanningRequest.cs ------ 纯规划器输入 -│ │ ├── HybridAStarConfiguration.cs ------ 原语、离散、代价、限额和容差 -│ │ ├── PlanningResult.cs ------ 状态、诊断、稠密路径和分段 -│ │ ├── PlanningStatus.cs ------ 输入、碰撞、搜索和验证状态 -│ │ ├── PlanningDiagnostics.cs ------ 节点、堆、耗时、路径和终止统计 -│ │ ├── CoarsePathPoint.cs ------ 位姿、弧长、方向、曲率和净空 -│ │ ├── CoarsePathPointSource.cs ------ Start/MotionPrimitive/GoalTruncation -│ │ └── PathSegment.cs ------ 包含式方向分段索引 -│ ├── Vehicle/ -│ │ ├── VehicleKinematics.cs ------ 解析保守最大曲率 -│ │ ├── VehicleFootprint.cs ------ 扩大矩形、AABB 和外接圆 -│ │ ├── OrientedRectangleCellIntersection.cs ------ 旋转矩形与格矩形精确相交 -│ │ └── FootprintCollisionChecker.cs ------ 边界、快速放行、精确和扫掠检查 -│ ├── Search/ -│ │ ├── MotionPrimitive.cs ------ 恒曲率原语和实际截断长度 -│ │ ├── MotionPrimitiveGenerator.cs ------ 解析积分并保留内部采样点 -│ │ ├── BinaryMinHeap.cs ------ netstandard2.0 确定性 Open List -│ │ ├── SearchCostCalculator.cs ------ 统一计算等效米搜索代价 -│ │ ├── HybridAStarNode.cs ------ 连续状态、代价、父索引和原语 -│ │ ├── HybridAStarNodeKey.cs ------ 位置/航向/方向/曲率离散键 -│ │ ├── GoalToleranceChecker.cs ------ 位置、航向和进入方向判断 -│ │ ├── GridDijkstraHeuristic.cs ------ 八邻域、禁止切角的二维启发 -│ │ └── HybridAStarSearch.cs ------ 扩展、重开、限额和候选管理 -│ ├── Output/ -│ │ ├── PathBacktracker.cs ------ 根据父索引重建原语内部点 -│ │ ├── CoarsePathAssembler.cs ------ 弧长、换向点和方向分段 -│ │ └── CoarsePathValidator.cs ------ 数值、碰撞、曲率、终点和分段复核 -│ ├── HybridAStarPlanner.cs ------ 只消费 PlanningGridMap 的下层门面 -│ ├── Facade/ -│ │ ├── CoarsePathPlanningJob.cs ------ 一次调用所需全部输入 -│ │ ├── CoarsePathPlanningJobResult.cs ------ 地图结果与粗路径结果 -│ │ ├── PlanningDebugOptions.cs ------ 地图、路径、碰撞调试开关 -│ │ ├── IPlanningDebugSink.cs ------ 不改变规划状态的调试消费接口 -│ │ └── CoarsePathPlanningService.cs ------ 建图、搜索和调试的一次调用入口 -│ └── Test/ -│ ├── CoarsePathScenarioFactory.cs ------ 固定、可复现的地图与规划场景 -│ └── MovementTest.CoarsePathTest.cs ------ 后台规划并在 Clumsy UI 显示 -│ └── README.md ------ 粗规划模块边界、调用示例和文档链接 - -ClumsyPilot/tests/ -├── verify_planning_utils.ps1 ------ 工具与公共契约 -├── verify_planning_map_factory.ps1 ------ 来源、事务、缓存与快照 -├── verify_planning_map_adapter.ps1 ------ 栅格、适配和距离场 -├── verify_planning_map_image.ps1 ------ PNG 渲染与原子发布 -├── verify_coarse_path_collision.ps1 ------ 亚栅格、擦边和扫掠碰撞 -├── verify_coarse_path_search.ps1 ------ 原语截断、堆、代价和搜索 -├── verify_coarse_path_integration.ps1 ------ 一次调用端到端验证 -└── benchmark_coarse_path.ps1 ------ Release 性能与内存门槛 -``` - -## 固定公共调用方式 - -常规业务代码只保留一个长期存在的服务实例: - -```csharp -var result = planningService.Plan(new CoarsePathPlanningJob -{ - MapRequest = new PlanningMapRequest - { - Bounds = bounds, - ResolutionMm = 50f, - ObstacleSources = new IMapObstacleSource[] - { - new ManualObstacleSource( - "manual", manualVersion, true, manualObstacles), - new TwoLegObstacleSource("two-leg", twoLegVersion, false, twoLegSnapshot), - }, - AllowExplicitEmptyMap = true, - }, - Start = startPose, - Goal = goalPose, - Vehicle = vehicle, - Configuration = configuration, - StartVehicleCurvature = 0d, - StartDirection = null, - GoalDirection = GoalDirectionConstraint.Any, - Debug = new PlanningDebugOptions - { - VisualizeMap = true, - VisualizePath = true, - VisualizeCollisionChecks = false, - }, -}, cancellationToken); -``` - -已有地图快照复用或纯搜索测试才直接调用 `HybridAStarPlanner.Plan(PlanningRequest, CancellationToken)`。业务调用方不得直接拼装 builder、rasterizer、碰撞器、运动原语或搜索节点。 - -## P0:正确可用 - -### Task 1:建立 Utils 与测试基座 - -**文件:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs` -- Create: `ClumsyPilot/tests/verify_planning_utils.ps1` - -**产出接口:** - -```csharp -double AngleMath.NormalizeRadians(double radians); -double AngleMath.ShortestSignedDifference(double from, double to); -int AngleMath.ToHeadingIndex(double heading, double resolution, int binCount); -double UnitConverter.MillimetersToMeters(double value); -double UnitConverter.DegreesToRadians(double value); -bool NumericGuard.IsFinite(double value); -readonly struct GridIndex { int Row; int Col; } -``` - -- [ ] 写失败测试:断言 `2π→0`、`179°→-179°` 最短角差为 `2°`、`1250 mm→1.25 m`、车体系 `(1000,0)` 在世界位姿 `(2000,3000,90°)` 后得到 `(2,4) m`,并拒绝 NaN/Infinity。 -- [ ] 执行以下命令,预期构建成功、脚本因新类型不存在返回非零。 - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -``` - -- [ ] 实现:角度统一到 `[-π,π)`;航向索引先归一化再 `floor`;刚体变换使用 `world=origin+R(heading)×local`;`GridIndex` 实现值相等和稳定哈希。 -- [ ] 重跑脚本,预期输出 `Planning utility checks passed.`。 - -### Task 2:锁定 CoarsePath 公共契约与默认参数 - -**文件:** - -- Create: `CoarsePath/Contracts/` 下目录树列出的 12 个契约文件 -- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1` - -**固定默认值:** - -```csharp -PrimitiveLengthMeters = 0.50; -IntegrationStepMeters = 0.05; -MaximumCollisionCheckStepMeters = 0.025; -HeadingResolutionRadians = Math.PI / 36d; -CurvatureLevelCount = 5; -GoalPositionToleranceMeters = 0.15; -GoalHeadingToleranceRadians = Math.PI / 36d; -MaximumExpandedNodes = 200000; -SearchTimeout = TimeSpan.FromSeconds(5); -HeuristicWeight = 1.0; -ReverseCostMultiplier = 1.5; -GearSwitchPenaltyMeters = 1.0; -CurvatureMagnitudeWeight = 0.10; -CurvatureChangePenaltyMetersPerLevel = 0.05; -ClearanceCostWeight = 0.20; -ClearanceCostDistanceMeters = 0.50; -``` - -- [ ] 扩展失败测试:断言上述默认值,三个方向/来源枚举,以及 `PlanningRequest` 的起始曲率、起始方向、目标进入方向。 -- [ ] 断言 `PlanningStatus` 包含:`Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed`、`InternalError`。 -- [ ] 实现不可变 `Pose2D`;结果工厂只允许成功结果携带路径,失败结果路径为空。 -- [ ] `PlanningDiagnostics` 固定记录扩展节点数、生成节点数、重新打开节点数、陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因。 -- [ ] `CoarsePathPoint` 固定包含 `X/Y`、`Heading/UnwrappedHeading`、`ArcLength`、`Direction`、`VehicleCurvature`、`BodyClearance`、`IsGearSwitchPoint` 和 `Source`;`PathSegment` 固定包含方向和包含式 `StartIndex/EndIndex`。 -- [ ] 重跑工具脚本,预期契约和默认值全部通过。 - -### Task 3:实现环境栅格、边界与统一栅格化 - -**文件:** - -- Create: `Map/Core/MapBoundsMm.cs` -- Create: `Map/Core/EnvironmentGridMap.cs` -- Create: `Map/Obstacles/IMapObstacle.cs` -- Create: `Map/Obstacles/AxisAlignedRectangleObstacle.cs` -- Create: `Map/Obstacles/CircleObstacle.cs` -- Create: `Map/Obstacles/MapObstacleRasterizer.cs` -- Create: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` - -上述 Map/CoarsePath 相对路径均位于 `ClumsyPilot/ParkrobTrajplanner/`。 - -- [ ] 写边界和栅格失败测试:20/200 mm 合法,范围外失败;4,000,001 格分配前失败;`XMax/YMax` 排他;非完整末格裁剪;地图外占据;圆/矩形与格边或格角接触时保守占据;完全在地图外的合法障碍不写格。 -- [ ] 运行脚本,预期新 Map 类型不存在。 -- [ ] 实现私有行优先 `byte[]`,索引固定为 `row*Cols+col`;世界转格使用 `floor((value-min)/resolution)`;不得返回内部缓冲区。 -- [ ] 实现唯一栅格化器:圆使用“圆心到格矩形最近点距离”,轴对齐矩形使用闭区间相交,只遍历裁剪后的候选包围盒。 -- [ ] 确认不存在 `MarkVehicleFootprint` 或安全距离参数,重跑脚本通过。 - -### Task 4:统一人工与 TwoLeg 障碍来源 - -**文件:** - -- Create: `Map/Sources/` 下目录树列出的 7 个文件 -- Create: `Map/Core/MapBuildRequest.cs` -- Create: `Map/Core/EnvironmentMapBuildResult.cs` -- Create: `Map/Core/EnvironmentMapBuilder.cs` -- Create: `ClumsyPilot/tests/verify_planning_map_factory.ps1` - -**固定接口:** - -```csharp -public interface IMapObstacleSource -{ - string SourceId { get; } - long SourceVersion { get; } - bool IsRequired { get; } - ObstacleProjectionResult ProjectToWorld(); -} -``` - -- [ ] 构造函数统一为 `ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles)` 和 `TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input)`;计划内所有实际调用均使用这两个签名。 -- [ ] 写来源事务失败测试:人工 `Applied/Empty`、TwoLeg 两个圆、可选来源 `Unavailable/Invalid` 保留人工图层、必需来源失败导致整图失败、重复 `SourceId` 失败、输入顺序不同但结果相同。 -- [ ] 校验 `SourceId` 非空且一次请求内唯一,`SourceVersion` 非负;来源快照内容发生变化时,上层必须递增版本。 -- [ ] 写 TwoLeg 坐标测试:使用检测时车辆位姿完成车体系 mm 到世界系 mm 变换,不得使用规划开始时位姿。 -- [ ] 实现纯快照投影:`ProjectToWorld()` 不能调用 `TwoLegDetect`、定位、UI 或系统时间;过期判断由上层采集适配器在构造 DTO 前完成。 -- [ ] builder 按 `SourceId` 排序;可选失败只记诊断;必需失败不发布地图;全部成功几何交给唯一 rasterizer。 -- [ ] 重跑工厂脚本,预期来源状态、投影和事务断言通过。 - -### Task 5:生成不可变 PlanningGridMap 和保守距离场 - -**文件:** - -- Create: `Map/Planning/EuclideanDistanceTransform.cs` -- Create: `Map/Planning/ObstacleDistanceField.cs` -- Create: `Map/Planning/PlanningGridMap.cs` -- Create: `Map/Planning/PlanningMapAdapter.cs` -- Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` - -- [ ] 扩展失败测试:mm→m、占据深拷贝、地图外距离为零、障碍格距离为零、空图距离正无穷、末格裁剪,以及所有样本距离不高于暴力几何距离。 -- [ ] 实现两次一维平方距离变换,复杂度 `O(Rows×Cols)`;不得逐自由格遍历全部障碍格。 -- [ ] 对精确栅格中心距离应用: - -```csharp -Math.Max(0d, centerDistanceMeters - Math.Sqrt(2d) * resolutionMeters) -``` - -- [ ] 查询使用点所在格的保守值,不做可能抬高结果的插值;空图跳过 EDT;边界检查独立执行。 -- [ ] `PlanningGridMap` 私有保存连续占据/距离数组,不提供写入口和缓冲区引用。 -- [ ] 重跑地图适配脚本,预期栅格、深拷贝、空图和保守距离全部通过。 - -### Task 6:实现 PlanningMapFactory 与两级快照缓存 - -**文件:** - -- Create: `Map/Planning/PlanningMapCache.cs` -- Create: `Map/PlanningMapRequest.cs` -- Create: `Map/PlanningMapBuildResult.cs` -- Create: `Map/PlanningMapFactory.cs` -- Modify: `Map/Planning/PlanningGridMap.cs` -- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` - -```csharp -public sealed class PlanningMapFactory -{ - public PlanningMapBuildResult Create(PlanningMapRequest request); -} -``` - -`PlanningMapRequest` 固定包含 `MapBoundsMm Bounds`、`float ResolutionMm`、`IReadOnlyList ObstacleSources`、`bool AllowExplicitEmptyMap`。 - -- [ ] 写缓存测试:完全相同请求返回同一 `PlanningGridMap`;来源版本变化但最终占据未变化时产生新 `SnapshotId` 并复用占据/距离缓冲;占据变化时重建距离场。 -- [ ] 写规划可用性测试:至少一个成功来源提供有效障碍语义时 `PlanningReady=true`;所有来源均为空时仅 `AllowExplicitEmptyMap=true` 可用;必需来源失败或未明确空图语义时 `PlanningReady=false` 并填写 `PlanningBlockReason`,规划器随后返回 `MapNotReady`。 -- [ ] 验证失效边界:起终点、车辆、规划参数和可视化开关不进入地图指纹;边界、分辨率、空图策略、来源状态/版本或规范化几何变化必须进入。 -- [ ] 实现确定性 `InputFingerprint`:来源先按 ID 排序,字段固定顺序,浮点按 IEEE 位模式;命中后仍比较规范化结构。 -- [ ] 栅格化后计算 `OccupancyHash`;命中后仍比较地图几何、数组长度和逐字节内容,不能只信任哈希。 -- [ ] 实现容量 4 的线程安全 LRU;完整命中返回同一快照,占据命中共享不可变缓冲但生成新元数据,只有占据变化才运行 EDT。 -- [ ] `PlanningMapBuildResult` 固定返回 `Succeeded`、失败原因、来源摘要、缓存命中类型和成功时的 `PlanningGridMap`;快照固定保存 `PlanningReady`、`PlanningBlockReason`、来源版本摘要、`InputFingerprint`、`OccupancyHash`、单调 `SnapshotId`。 -- [ ] 增加 16 个并发相同请求测试和 LRU 淘汰测试,重跑脚本通过。 - -### Task 7:实现连续车体足迹和保守碰撞检查 - -**文件:** - -- Create: `CoarsePath/Vehicle/` 下目录树列出的 4 个文件 -- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1` - -**固定接口:** - -```csharp -bool IsPoseCollisionFree( - Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, - double additionalMarginMeters, out double bodyClearanceMeters); - -bool IsSweptMotionCollisionFree( - Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, - double maximumCenterStepMeters, out double minimumBodyClearanceMeters); -``` - -- [ ] 写失败测试:正交、45°、任意航向、栅格中心/亚栅格中心、边角接触、薄障碍、地图边界、距离场快速放行,以及两个无碰撞端点之间有障碍的扫掠场景。 -- [ ] 实现以几何中心为参考的扩大车体;安全余量加到长度和宽度两侧;最大曲率与最小转弯半径并存时取更保守限制。 -- [ ] 使用分离轴定理精确判断连续旋转矩形与占据格矩形相交,接触视为碰撞;不得使用离散航向模板。 -- [ ] 检查顺序:扩大车体边界 → 保守距离严格大于外接圆时快速放行 → AABB 内占据格 SAT。 -- [ ] 扫掠采样中心步长不超过 `min(configuredStep,map.Resolution/2)`;每段临时附加余量为: - -```text -0.5 × (centerDisplacement - + circumscribedRadius × abs(headingDelta)) -``` - -- [ ] 重跑碰撞脚本,预期所有亚栅格、擦边和扫掠案例通过。 - -### Task 8:实现解析恒曲率原语和内部终点截断 - -**文件:** - -- Create: `CoarsePath/Search/MotionPrimitive.cs` -- Create: `CoarsePath/Search/MotionPrimitiveGenerator.cs` -- Create: `CoarsePath/Search/GoalToleranceChecker.cs` -- Create/Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**解析积分:** - -```csharp -double signedDistance = direction == TravelDirection.Forward ? step : -step; -double nextHeading = AngleMath.NormalizeRadians( - heading + curvature * signedDistance); - -if (Math.Abs(curvature) < 1e-12) -{ - nextX = x + signedDistance * Math.Cos(heading); - nextY = y + signedDistance * Math.Sin(heading); -} -else -{ - nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature; - nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature; -} -``` - -- [ ] 写原语测试:直行、圆弧、倒车、五级曲率、相邻曲率最多变化一级、最大长度 0.50 m、实际采样步长不超过 `min(IntegrationStep,CollisionStep,MapResolution/2)`。 -- [ ] 写用户提出的案例:目标距起点 0.30 m、原语最大 0.50 m、收紧容差;断言第一个满足目标的内部点截断,后续点不生成,来源为 `GoalTruncation`。 -- [ ] 每个内部点严格按“有限值 → 扫掠碰撞 → 目标条件”检查;碰撞必须先于目标。 -- [ ] 起点已满足目标时创建零长度候选,不生成原语。 -- [ ] 重跑搜索脚本,预期几何、碰撞采样和 0.30 m 截断案例通过。 - -### Task 9:实现 Open List、统一代价和二维启发 - -**文件:** - -- Create: `CoarsePath/Search/BinaryMinHeap.cs` -- Create: `CoarsePath/Search/SearchCostCalculator.cs` -- Create: `CoarsePath/Search/GridDijkstraHeuristic.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**固定代价:** - -```text -primitiveCost = - lengthMeters - × directionMultiplier - × (1 - + CurvatureMagnitudeWeight × abs(curvature / maximumCurvature) - + ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters)) - + gearSwitchPenalty - + CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta) -``` - -- [ ] 写堆顺序测试:较小 `F`、较小 `H`、较大 `G`、较小插入序号;相同输入重复运行顺序一致。 -- [ ] 写代价测试:前进、倒车、换向、曲率幅值、曲率变化和净空项;拒绝负数及非有限权重。 -- [ ] 写 Dijkstra 测试:八邻域直/斜代价;两个正交邻格任一占据时禁止对角切角;二维不可达返回明确状态。 -- [ ] 实现专用二叉最小堆,不依赖 `PriorityQueue`;允许旧条目由搜索层惰性丢弃。 -- [ ] `HeuristicWeight=1` 使用 `F=G+H`;大于 1 时只承诺可行性。 -- [ ] 重跑搜索脚本,预期顺序、公式和切角限制通过。 - -### Task 10:实现 Hybrid A* 节点、重开和终点候选管理 - -**文件:** - -- Create: `CoarsePath/Search/HybridAStarNode.cs` -- Create: `CoarsePath/Search/HybridAStarNodeKey.cs` -- Create: `CoarsePath/Search/HybridAStarSearch.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -- [ ] 写搜索测试:空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、`±π` 容差、无解、取消、超时、节点上限、重开和确定性。 -- [ ] 写候选排序测试:先生成较大 `F` 的终点候选时不得结束;较小 `F` 普通节点先出队;候选成为最佳有效条目后才成功。 -- [ ] 离散键固定为位置格、航向格、方向和曲率等级;连续位姿保留在节点中。 -- [ ] `Dictionary` 保存普通状态最佳 `G`;更小 `G` 允许重开;旧普通堆条目惰性丢弃。 -- [ ] 终点候选放入同一堆,但不能仅因另一个连续位姿落入相同离散键且 `G` 更低而被删除;候选出队时重新验证目标和末段碰撞。 -- [ ] 搜索循环在每次扩展前按顺序检查取消、5 s 超时、200,000 节点上限,再弹出有效条目;Open List 为空返回 `NoFeasiblePath`。 -- [ ] 重跑搜索脚本,预期候选顺序、重开、限额和所有场景通过。 - -### Task 11:回溯、装配、最终复核和 HybridAStarPlanner - -**文件:** - -- Create: `CoarsePath/Output/PathBacktracker.cs` -- Create: `CoarsePath/Output/CoarsePathAssembler.cs` -- Create: `CoarsePath/Output/CoarsePathValidator.cs` -- Create: `CoarsePath/HybridAStarPlanner.cs` -- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -```csharp -public sealed class HybridAStarPlanner -{ - public PlanningResult Plan( - PlanningRequest request, - CancellationToken cancellationToken = default(CancellationToken)); -} -``` - -- [ ] 写失败状态测试:请求、地图就绪、车辆参数、曲率配置、起终点越界/碰撞,以及搜索失败到结果的无异常映射。 -- [ ] 写输出测试:首点弧长零;弧长不递减;`UnwrappedHeading` 连续;终点截断来源正确;除换向对外无相邻重复点。 -- [ ] 写换向分段测试:换向处保留两个坐标/航向/弧长相同而方向不同的点;新方向点标记 `IsGearSwitchPoint`;包含式分段完整覆盖路径。 -- [ ] 搜索节点只存父索引、方向、曲率和实际原语长度;成功后使用相同解析积分和有效步长重建内部点。 -- [ ] 最终复核有限数值、曲率、扫掠碰撞、目标容差/方向、弧长、换向对和分段;失败返回 `FinalValidationFailed` 且不发布部分路径。 -- [ ] 重跑碰撞、搜索和集成脚本,预期全部通过。 - -### Task 12:实现一次调用 CoarsePathPlanningService - -**文件:** - -- Create: `CoarsePath/Facade/` 下目录树列出的 5 个文件 -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -```csharp -public sealed class CoarsePathPlanningService -{ - public CoarsePathPlanningJobResult Plan( - CoarsePathPlanningJob job, - CancellationToken cancellationToken = default(CancellationToken)); -} -``` - -- [ ] 写一次调用测试:固定执行 `PlanningMapFactory.Create → HybridAStarPlanner.Plan → debug sink`;地图失败不启动搜索;结果同时保留地图和规划结果。 -- [ ] 写旁路隔离测试:可视化开关不改变地图指纹、占据哈希、规划状态或路径;debug sink 异常只写调试诊断。 -- [ ] 服务持有同一 `PlanningMapFactory`,多次调用共享容量 4 缓存;默认 debug sink 为空行为。 -- [ ] 重跑以下 P0 验收,预期构建成功且 6 个脚本退出码为 0。 - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -## P0-MAP 收尾与 P1:集成、迁移和性能 - -### Task 13(P0-MAP):拆分迁移 TrapMapImageExporter - -**现有来源:** - -- Read/Migrate: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` - -**新文件:** - -- Create: `Map/Test/Visualization/PlanningMapImageExportRequest.cs` -- Create: `Map/Test/Visualization/PlanningMapImageExportResult.cs` -- Create: `Map/Test/Visualization/PlanningMapImageExporter.cs` -- Create: `Map/Test/Visualization/PlanningMapImageRenderer.cs` -- Create: `Map/Test/Visualization/ValidatedPngWriter.cs` -- Create: `ClumsyPilot/tests/verify_planning_map_image.ps1` - -- [ ] 写新图片测试:输入只允许 `PlanningGridMap`;覆盖关闭导出、非法尺寸、唯一命名、临时文件清理、PNG 签名/IHDR/IEND 和每个 chunk CRC。 -- [ ] 保留现有有效限制:`PixelsPerCell=4`、`MaximumImageEdgePixels=4000`、`MaximumFileSizeBytes=50 MiB`、`OutputDpi=300`、最多 1024 次重名重试、StbImageWriteSharp 1.16.7。 -- [ ] 按职责迁移:Request/Result 只放 DTO;Renderer 生成 RGBA;Writer 负责编码/CRC;Exporter 校验、独占临时文件和原子发布。 -- [ ] 新导出器不得依赖 `GridMapData`、`TrapMapVehiclePose`、TwoLeg 状态、传感器或地图构造器;车辆、起终点和路径只作可选叠加层。 -- [ ] 运行图片脚本通过后先保留旧文件,Task 16 确认等价覆盖后再退役。 - -### Task 14:增加地图与粗路径 MovementTest - -**文件:** - -- Create: `Map/Test/MovementTest.MapTest.cs` -- Create: `CoarsePath/Test/CoarsePathScenarioFactory.cs` -- Create: `CoarsePath/Test/MovementTest.CoarsePathTest.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -- [ ] **P0-MAP 部分:** 先创建 `MovementTest.MapTest.cs`。它只调用长期持有的 `PlanningMapFactory`,显示来源状态、栅格、快照 ID 和缓存命中,并可选调用新 PNG 导出器;通过 Map Gate 后即可结束当前首要里程碑。 -- [ ] **P1 部分:** Map Gate 和 P0-PLAN 均通过后,再创建 `CoarsePathScenarioFactory.cs` 与 `MovementTest.CoarsePathTest.cs`。 -- [ ] 场景工厂提供显式空图、单矩形绕行、人工圆+矩形+TwoLeg、相同地图缓存命中、倒车换向和无解案例。 -- [ ] CoarsePath 案例使用同一 `CoarsePathPlanningService`;测试类不得直接实例化 rasterizer、碰撞器、原语生成器或搜索节点。 -- [ ] `MovementTest.CoarsePathTest` 在后台任务调用同步 `Plan`,绘制起点、目标、路径、换向点和扩大车体检查点。 -- [ ] `TestStop` 先取消专用 `CancellationTokenSource`,再清理任务和 Painter;两个入口不得发送底盘运动命令。 -- [ ] Clumsy UI 手动运行时不阻塞界面,停止后无后台规划残留,调试开关不改变结果。 - -### Task 15(P1):性能、资源和确定性验收 - -**文件:** - -- Create: `ClumsyPilot/tests/benchmark_coarse_path.ps1` -- Modify: `Map/Planning/PlanningMapCache.cs` -- Modify: `Map/Planning/EuclideanDistanceTransform.cs` -- Modify: `CoarsePath/Search/BinaryMinHeap.cs` -- Modify: `CoarsePath/Search/HybridAStarSearch.cs` -- Modify: `CoarsePath/Contracts/PlanningDiagnostics.cs` - -- [ ] 基准脚本输出地图规模、缓存命中、状态、耗时、扩展/生成/重开节点、陈旧堆条目、Open List 峰值和托管内存增量,超限返回非零。 -- [ ] 地图参考:20 m×20 m、0.05 m、160,000 格、100 障碍;20 次后完整构建 P95≤200 ms,完整缓存命中 P95≤5 ms。 -- [ ] 地图极限:4,000,000 格、100 障碍;3 s 内成功或明确失败;成功时内存增量≤160 MB,无溢出和部分快照。 -- [ ] 规划参考:12 m×8 m、0.05 m、矩形阻断直线、距离≥8 m;20 次 P95≤2 s,内存增量≤256 MB。 -- [ ] 规划压力:20 m×20 m、0.05 m;成功或无解均在 5 s、200,000 节点和 512 MB 增量内返回。 -- [ ] 优化只针对查询分配、重复 EDT、堆扩容和稠密点保存;不得降低碰撞保守性、跳过最终复核或放宽失败状态。 -- [ ] 运行: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj -c Release --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\benchmark_coarse_path.ps1 -Configuration Release -``` - -预期:全部指标达标,脚本退出码为 0。 - -### Task 16(P1):退役旧地图入口并完成文档 - -**文件:** - -- Modify/Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/MovementTest.Trapmaptest.cs` -- Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_grid.ps1` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_lifecycle.ps1` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_image.ps1` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - -- [ ] 建立覆盖表:`TrapMapBounds→MapBoundsMm`、`GridMapData→EnvironmentGridMap/PlanningGridMap`、`TrapMapLayerComposer→EnvironmentMapBuilder`、`TrapMapBuilder.Get→上层采集+CoarsePathPlanningService`、旧 exporter→五个 Visualization 文件。 -- [ ] 等价脚本全部通过后再删除旧实现;不保留车辆写图、默认 300 mm 膨胀或地图构建器直接调用 `TwoLegDetect` 的兼容开关。 -- [ ] README 写明一次调用、下层门面、mm/m-rad 边界、快照变化判断、调试开关不参与指纹、P0/P1 命令和非目标。 -- [ ] 执行旧引用搜索: - -```powershell -rg -n "GridMapData|TrapMapBuilder|TrapMapLayerComposer|TrapMapImageExporter|MarkVehicleFootprint" .\ClumsyPilot -``` - -预期:仅迁移说明或历史文档可命中;运行时代码和新测试不得命中旧类型。 - -- [ ] 运行最终回归: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -预期:Debug 构建成功,7 个功能脚本退出码均为 0;随后重跑 Task 15 Release 基准并通过。 - -## 最终验收清单 - -- [ ] `CoarsePathPlanningService.Plan(job)` 是推荐的一次调用入口。 -- [ ] `PlanningMapFactory` 和 `HybridAStarPlanner` 仅作为可独立测试的下层门面。 -- [ ] 人工、TwoLeg 和未来障碍通过同一个 `IMapObstacleSource` 进入唯一栅格化器。 -- [ ] 地图未变化时复用完整快照或占据/距离缓冲;可视化、起终点和车辆参数不参与地图变化判断。 -- [ ] 地图不包含车辆自身和安全膨胀,碰撞检查使用扩大车辆矩形。 -- [ ] 距离场是净空下界,不能因高估而跳过精确碰撞。 -- [ ] 0.50 m 原语可在任意内部采样点截断;终点候选按 Open List 顺序出队后才终止。 -- [ ] best-G、重开、陈旧条目、候选保护和堆排序均有自动化测试。 -- [ ] 输出包含稠密点、保守净空、实际曲率、换向点和完整方向分段。 -- [ ] PNG 和 MovementTest 只消费只读快照,不成为核心规划依赖。 -- [ ] 失败、取消、超时和限额均返回空路径及明确状态,不发布部分结果。 -- [ ] Debug 与 Release 验收全部通过,且未执行 Git 自检或提交。 diff --git a/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md b/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md deleted file mode 100644 index 0d05929..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md +++ /dev/null @@ -1,1194 +0,0 @@ -# AMR 非结构化道路轨迹规划技术路线(面向 Agent) - -## 1. 项目目标 - -开发一套面向非结构化道路四舵轮 AMR 的轨迹规划系统。 - -第一阶段采用车式运动模型,具体边界如下: - -- 支持前进、倒车和换向; -- 暂不支持蟹行、纯横移和原地旋转; -- 车体航向始终表示车头朝向; -- 规划结果必须满足完整车体无碰撞、最大曲率约束和目标位姿要求; -- 最终输出可供底盘控制器跟踪的带时间轨迹。 - -整体技术路线分为四个主要阶段: - -```mermaid -flowchart LR - A[输入与地图构建] --> B[Hybrid A* 粗路径规划] - B --> C[路径平滑与可行性处理] - C --> D[SQP 轨迹优化] - D --> E[输出可跟踪轨迹] -``` - ---- - -## 2. 总体模块划分 - -```text -Module 1:输入与地图构建 -Module 2:Hybrid A* 粗路径规划 -Module 3:路径平滑与可行性处理 -Module 4:SQP 轨迹优化 -Module 5:轨迹输出与底盘接口 -``` - -各模块必须通过明确的数据结构传递结果,不允许后级模块直接读取前级模块内部状态。 - ---- - -# 3. Module 1:输入与地图构建 - -## 3.1 模块目标 - -根据起点、终点、车辆参数和环境障碍物信息,构建一张供全局搜索使用的任务级二维栅格地图。 - -地图范围至少覆盖: - -- AMR 起点; -- 目标位置; -- 起点与目标之间的规划区域; -- 周围必要的安全边界。 - -当前阶段继续使用已有 `GridMapData` 作为地图数据来源,并通过适配器转换为 Hybrid A* 使用的规划地图。 - ---- - -## 3.2 输入数据 - -### 3.2.1 起点位姿 - -```text -StartPose: - X - Y - Heading - StartDirection - StartVehicleCurvature -``` - -字段定义: - -- `X、Y`:AMR 几何中心在世界坐标系中的位置; -- `Heading`:车头航向; -- `StartDirection`:`Forward`、`Reverse` 或 `Both`; -- `StartVehicleCurvature`:车辆当前转向曲率,未提供时默认为 0。 - ---- - -### 3.2.2 终点位姿 - -```text -GoalPose: - X - Y - Heading - GoalDirectionConstraint -``` - -目标进入方向约束: - -```text -Any -Forward -Reverse -``` - -其中: - -- `GoalHeading` 始终表示目标车头朝向; -- `GoalDirectionConstraint` 只约束最后一个连续运动段的运动方向。 - ---- - -### 3.2.3 车辆参数 - -```text -VehicleParameters: - VehicleLength - VehicleWidth - MaxVehicleCurvature - MinTurningRadius -``` - -第一阶段车辆参考点统一采用 AMR 几何中心。 - ---- - -### 3.2.4 环境障碍物 - -当前已接入: - -```text -TwoLegDetect 两腿检测结果 -``` - -后续扩展: - -```text -点云障碍物 -激光雷达障碍物 -静态障碍物 -动态障碍物 -禁行区域 -``` - ---- - -### 3.2.5 地图配置 - -```text -MapConfiguration: - GridResolution - MapMarginX - MapMarginY - ObstacleInflation - MaximumCellCount -``` - ---- - -## 3.3 当前地图构建逻辑 - -```mermaid -flowchart TD - A[读取 AMR 位姿与车身尺寸] --> B[读取目标位置与地图参数] - B --> C[计算任务级地图边界] - C --> D[创建 GridMapData] - D --> E[读取 TwoLegDetect 障碍物] - E --> F[车体系坐标转世界坐标] - F --> G[圆形障碍物栅格化] - G --> H[输出环境占据栅格] -``` - -当前地图边界由起点与目标位置共同决定: - -```text -XMin = min(StartX, GoalX) - MapMarginX -XMax = max(StartX, GoalX) + MapMarginX - -YMin = min(StartY, GoalY) - MapMarginY -YMax = max(StartY, GoalY) + MapMarginY -``` - -栅格尺寸: - -```text -Cols = ceil((XMax - XMin) / Resolution) -Rows = ceil((YMax - YMin) / Resolution) -``` - ---- - -## 3.4 当前地图适配要求 - -现有地图格式为: - -```text -单位:mm -数组:Cells[row, col] -状态:0 = 未标记占据,1 = Occupied -``` - -Hybrid A* 内部统一使用: - -```text -单位:m -角度:rad -曲率:1/m -``` - -因此需要增加: - -```text -PlanningMapAdapter -``` - -适配流程: - -```mermaid -flowchart TD - A[GridMapData] --> B[校验边界、尺寸与分辨率] - B --> C[深拷贝 Occupancy] - C --> D[毫米转换为米] - D --> E[生成障碍物距离场] - E --> F[输出 PlanningGridMap] -``` - ---- - -## 3.5 地图构建阶段的关键规则 - -### 3.5.1 规划地图不得包含 AMR 自身占据层 - -AMR 自身不能写入环境占据地图,否则 Hybrid A* 起点会直接被判定为碰撞。 - -正确关系: - -```text -环境地图: - 只包含外部障碍物 - -AMR 自身: - 通过 VehiclePose、VehicleLength、VehicleWidth 单独描述 -``` - ---- - -### 3.5.2 障碍物膨胀量必须明确 - -建议将: - -```text -SafetyMarginMm -``` - -改为: - -```text -ObstacleInflationMm -``` - -其唯一含义是: - -> 障碍物写入地图前向外膨胀的距离。 - -必须避免: - -```text -障碍物已经膨胀 -+ -车辆碰撞矩形再次增加同样余量 -``` - ---- - -### 3.5.3 区分地图创建成功与地图可用于规划 - -```text -Succeeded: - 地图数据结构是否创建成功 - -PlanningReady: - 当前地图是否满足规划输入要求 - -PlanningBlockReason: - 地图为何不能用于规划 -``` - -例如: - -```text -TwoLegDetect 正常: - Succeeded = true - PlanningReady = true - -TwoLegDetect 无结果: - Succeeded = true - PlanningReady = false -``` - ---- - -## 3.6 地图输出 - -```text -PlanningGridMap: - XMin - XMax - YMin - YMax - Resolution - Rows - Cols - Occupancy[row, col] - ObstacleDistance[row, col] - ObstacleInflation - InputSource - PlanningReady - MapVersion -``` - -当前第一版状态定义: - -```text -Occupancy = 0: - 当前地图层未标记为占据 - -Occupancy = 1: - 占据 - -地图外部: - 按占据处理 -``` - ---- - -# 4. Module 2:Hybrid A* 粗路径规划 - -## 4.1 模块目标 - -在任务级栅格地图中搜索一条: - -- 从起点到目标附近; -- 完整车体无碰撞; -- 满足最大曲率限制; -- 支持前进、倒车和换向; -- 具有正确绕障拓扑; -- 可作为后续平滑和 SQP 优化初值; - -的空间粗路径。 - -Hybrid A* 负责决定: - -```text -从哪里走 -绕障碍物哪一侧走 -什么时候前进 -什么时候倒车 -在哪里换向 -以什么车头航向接近目标 -``` - -Hybrid A* 不负责: - -```text -速度 -加速度 -角速度 -时间戳 -舵轮角度 -轮速 -``` - ---- - -## 4.2 搜索状态 - -连续状态: - -```text -State: - x - y - theta - direction - curvatureIndex -``` - -字段定义: - -- `x、y`:AMR 几何中心位置; -- `theta`:车头航向; -- `direction`:`Forward` 或 `Reverse`; -- `curvatureIndex`:当前离散车辆曲率等级。 - -Closed Set 键: - -```text -GridKey: - ix - iy - iHeading - direction - curvatureIndex -``` - ---- - -## 4.3 运动模型 - -定义: - -```text -directionSign = +1:Forward -directionSign = -1:Reverse -``` - -运动积分: - -```text -dx/ds = directionSign × cos(theta) -dy/ds = directionSign × sin(theta) -dtheta/ds = directionSign × VehicleCurvature -``` - -其中: - -```text -Heading: - 始终表示车头朝向 - -ArcLength: - 始终非负并单调增加 -``` - ---- - -## 4.4 运动原语 - -第一版离散曲率集合: - -```text --kappaMax --0.5 × kappaMax -0 -+0.5 × kappaMax -+kappaMax -``` - -每个曲率均支持: - -```text -Forward -Reverse -``` - -第一版建议参数: - -```text -PrimitiveLength = 0.50 m -IntegrationStep = 0.05 m -``` - -每条原语按 0.05 m 步长积分,并在每个内部积分点执行完整车体碰撞检测。 - ---- - -## 4.5 搜索约束 - -每个候选原语必须检查: - -```text -地图边界 -Occupied 栅格 -完整车辆矩形碰撞 -安全净空 -最大车辆曲率 -允许的运动方向 -曲率等级切换规则 -目标进入方向 -``` - -第一阶段暂不直接约束: - -```text -速度 -加速度 -曲率时间变化率 -舵轮转向速度 -``` - -但需要通过代价项减少极端曲率跳变。 - ---- - -## 4.6 搜索代价 - -```text -g = - 路径长度代价 - + 倒车代价 - + 换向代价 - + 曲率代价 - + 曲率变化代价 - + 障碍物距离代价 -``` - -启发函数: - -```text -h = max(h2D, hRS) -``` - -其中: - -```text -h2D: - 基于二维 Dijkstra 的绕障距离 - -hRS: - 基于 Reeds–Shepp 模型的非完整约束距离 -``` - -节点排序: - -```text -f = g + HeuristicWeight × h -``` - ---- - -## 4.7 终点附近 Reeds–Shepp 连接 - -```mermaid -flowchart TD - A[当前节点接近目标] --> B[生成 Reeds-Shepp 候选] - B --> C[按 0.05 m 采样] - C --> D[完整车体碰撞检查] - D --> E[最大曲率与目标方向检查] - E -->|成功| F[精确连接目标] - E -->|失败| G[继续 Hybrid A* 搜索] -``` - -支持模式: - -```text -Disabled -Opportunistic -RequiredNearGoal -``` - -停车、钻入和精确泊入任务优先使用: - -```text -RequiredNearGoal -``` - ---- - -## 4.8 Hybrid A* 输出 - -```text -CoarsePathPoint: - X - Y - Heading - UnwrappedHeading - ArcLength - Direction - VehicleCurvature - GeometricCurvature - BodyClearance - IsGearSwitchPoint - Source -``` - -路径按运动方向拆分: - -```text -PathSegment: - SegmentIndex - Direction - StartIndex - EndIndex - StartsAtGearSwitch - EndsAtGearSwitch -``` - ---- - -# 5. Module 3:路径平滑与可行性处理 - -## 5.1 模块目标 - -消除 Hybrid A* 粗路径中的离散折线和不连续曲率,生成更平滑的空间参考路径。 - -该阶段只优化几何形状,不进行时间参数化。 - -不得改变: - -```text -绕障侧 -前进与倒车顺序 -换向次数 -方向段拓扑 -目标进入方向 -``` - ---- - -## 5.2 处理流程 - -```mermaid -flowchart TD - A[Hybrid A* 稠密粗路径] --> B[删除重复点] - B --> C[按 Forward/Reverse 分段] - C --> D[保留关键点] - D --> E[按弧长重采样] - E --> F[几何平滑] - F --> G[完整可行性检查] - G --> H[平滑空间路径] -``` - ---- - -## 5.3 必须保留的关键点 - -```text -起点 -终点 -换向点 -曲率等级切换点 -直线与弯道切换点 -Reeds–Shepp 连接点 -局部最小净空点 -明显航向变化点 -``` - -禁止跨换向点进行单条曲线拟合。 - ---- - -## 5.4 平滑方法 - -### 5.4.1 贝塞尔曲线 - -适用于: - -```text -局部短转角 -折线拐点快速平滑 -局部连接 -``` - -特点: - -```text -实现简单 -计算速度快 -局部形状易控制 -``` - ---- - -### 5.4.2 五次多项式 - -适用于: - -```text -末端短距离连接 -具有明确起终点位置、航向和曲率的局部段 -``` - -特点: - -```text -可同时约束位置、航向和曲率 -适合末端姿态调整 -``` - ---- - -### 5.4.3 B 样条 - -适用于: - -```text -较长的普通路径段 -多点连续平滑 -降低局部折线感 -``` - -建议采用三次 B 样条作为第一版普通路径段的主平滑方法。 - ---- - -## 5.5 可行性检查 - -平滑后的路径必须重新检查: - -```text -完整车体碰撞 -地图边界 -Unknown / Occupied -最小安全净空 -最大车辆曲率 -路径位置连续性 -路径切线连续性 -换向结构 -目标进入方向 -``` - -若平滑失败: - -```text -局部重新调整 - ↓ -局部 QP 兜底 - ↓ -仍失败则回退原始可行粗路径 -``` - ---- - -## 5.6 输出 - -```text -SmoothedPathPoint: - X - Y - Heading - ArcLength - Direction - VehicleCurvature - GeometricCurvature - BodyClearance - Source -``` - -该输出仍然不包含: - -```text -速度 -加速度 -时间戳 -``` - ---- - -# 6. Module 4:SQP 轨迹优化 - -## 6.1 模块目标 - -以平滑空间路径作为参考线,通过 Frenet 坐标系和 SQP 优化,联合完成: - -- 横向几何调整; -- 曲率优化; -- 纵向速度规划; -- 加速度约束; -- 换向停车约束; -- 终点精确收敛; -- 时间参数化。 - -最终生成可供车辆控制器跟踪的带时间轨迹。 - ---- - -## 6.2 Frenet 坐标转换 - -将平滑路径作为参考线,建立 Frenet 坐标系: - -```text -s: - 沿参考线的累计弧长 - -l: - 相对于参考线的横向偏移 -``` - -转换后,将原二维路径问题分解为: - -```text -横向 l-s 问题 -纵向 s-t 问题 -``` - ---- - -## 6.3 横向 l-s 优化 - -主要优化变量: - -```text -横向偏移 l -横向一阶导数 dl/ds -横向二阶导数 d²l/ds² -曲率 -曲率变化 -``` - -优化目标: - -```text -尽量贴近参考线 -避开障碍物 -保持曲率平滑 -满足最大曲率 -满足安全走廊约束 -精确到达目标横向位置 -``` - ---- - -## 6.4 纵向 s-t 优化 - -主要优化变量: - -```text -s(t) -速度 v(t) -加速度 a(t) -加加速度 jerk(t) -``` - -优化目标: - -```text -满足速度上限 -满足加速度约束 -满足减速度约束 -提高运动平顺性 -减少总时间 -在换向点停车 -在终点停车 -``` - ---- - -## 6.5 SQP 联合优化 - -SQP 负责迭代求解: - -```text -路径位置 -航向 -曲率 -速度 -加速度 -角速度 -时间戳 -``` - -约束至少包括: - -```text -障碍物安全约束 -车辆曲率约束 -速度约束 -加速度约束 -角速度约束 -换向点速度为 0 -终点位置约束 -终点航向约束 -终点速度约束 -``` - -SQP 不负责: - -```text -重新决定绕障侧 -凭空新增换向 -改变 Hybrid A* 已确定的拓扑结构 -``` - ---- - -## 6.6 SQP 输出 - -```text -TrajectoryPoint: - X - Y - Heading - Time - ArcLength - Direction - Velocity - Acceleration - AngularVelocity - VehicleCurvature - GeometricCurvature -``` - -该轨迹可以直接提供给后续底盘控制与轨迹跟踪模块。 - ---- - -# 7. Module 5:轨迹输出与底盘接口 - -## 7.1 输出内容 - -```text -TrajectoryResult: - PlanningStatus - GoalReachType - PathProcessingResult - TrajectoryPoints - DirectionSegments - Diagnostics -``` - ---- - -## 7.2 底盘控制前的处理 - -轨迹规划器不直接输出四轮舵角和轮速。 - -底盘控制层负责: - -```text -轨迹点 - ↓ -车辆状态误差计算 - ↓ -轨迹跟踪控制 - ↓ -底盘运动学逆解 - ↓ -四轮舵角与轮速 -``` - ---- - -# 8. Agent 开发顺序 - -## 阶段 M0:数据结构与公共工具 - -实现: - -```text -Pose2D -VehicleParameters -GoalPose -GridMapData Adapter -角度归一化 -坐标变换 -单位转换 -``` - -完成标准: - -```text -单元测试通过 -毫米与米转换无误 -航向角定义统一 -``` - ---- - -## 阶段 M1:地图适配与碰撞检测 - -实现: - -```text -PlanningMapAdapter -PlanningGridMap -距离场 -旋转矩形碰撞检测 -地图边界检测 -``` - -完成标准: - -```text -AMR 自身不会出现在环境占据图中 -不同航向下完整车体碰撞检测正确 -``` - ---- - -## 阶段 M2:最小 Hybrid A* - -实现: - -```text -只前进 -无障碍 -恒曲率运动原语 -基础启发函数 -``` - -完成标准: - -```text -稳定从起点搜索到目标 -``` - ---- - -## 阶段 M3:障碍物、倒车和换向 - -实现: - -```text -Dijkstra 启发 -Forward / Reverse -换向代价 -方向分段 -``` - -完成标准: - -```text -单障碍绕行成功 -需要倒车的场景成功 -换向点正确输出 -``` - ---- - -## 阶段 M4:目标终止与 Reeds–Shepp - -实现: - -```text -目标位置容差 -目标航向容差 -目标进入方向 -Reeds–Shepp 三种模式 -``` - -完成标准: - -```text -普通目标和狭窄目标场景均能正确终止 -``` - ---- - -## 阶段 M5:路径后处理 - -实现: - -```text -父节点回溯 -内部积分点恢复 -关键点保留 -弧长重采样 -``` - -完成标准: - -```text -路径无断点 -换向结构明确 -方向段正确 -``` - ---- - -## 阶段 M6:平滑与可行性检查 - -实现: - -```text -B 样条 -贝塞尔曲线 -五次多项式 -完整碰撞复核 -失败回退 -``` - -完成标准: - -```text -路径曲率明显改善 -不改变原有拓扑 -平滑失败可回退 -``` - ---- - -## 阶段 M7:SQP 轨迹优化 - -实现: - -```text -Frenet 坐标转换 -l-s 优化 -s-t 优化 -速度和加速度约束 -换向停车约束 -终点精确约束 -``` - -完成标准: - -```text -输出带时间的可跟踪轨迹 -终点位置与航向满足要求 -换向点速度为 0 -``` - ---- - -## 阶段 M8:性能优化 - -在前述流程完整跑通后再开展: - -```text -性能剖析 -Open List 优化 -Closed Set 优化 -节点内存优化 -地图裁剪 -预计算运动原语 -预计算碰撞模板 -启发图复用 -路径热启动 -增量规划 -``` - ---- - -# 9. 核心技术边界 - -## Hybrid A* 负责 - -```text -拓扑 -绕障侧 -前进与倒车结构 -换向位置 -基本运动学可行性 -粗目标位姿接近 -``` - -## 路径平滑负责 - -```text -消除折线 -改善局部曲率 -保持拓扑不变 -输出连续空间参考路径 -``` - -## SQP 负责 - -```text -终点精确收敛 -速度规划 -加速度规划 -角速度规划 -时间参数化 -动态可跟踪性 -``` - -## 底盘控制负责 - -```text -四轮舵角 -四轮轮速 -轨迹跟踪 -执行器控制 -``` - ---- - -# 10. 最终技术路线总结 - -```text -起点位姿 + 目标位姿 + 车辆参数 + 障碍物数据 - ↓ -构建任务级环境占据地图 - ↓ -通过 PlanningMapAdapter 转换为规划地图 - ↓ -Hybrid A* 搜索满足运动学约束的粗路径 - ↓ -Reeds–Shepp 处理终点位姿连接 - ↓ -回溯、重采样和路径方向分段 - ↓ -B 样条 / 贝塞尔 / 五次多项式进行几何平滑 - ↓ -重新执行完整车体碰撞和曲率检查 - ↓ -建立 Frenet 参考坐标系 - ↓ -横向 l-s 优化路径形状和曲率 - ↓ -纵向 s-t 优化速度、加速度和停车决策 - ↓ -SQP 联合求解可跟踪轨迹 - ↓ -输出位置、航向、速度、加速度、角速度和时间戳 - ↓ -底盘控制器进行四轮舵角和轮速逆解 -``` - -该技术路线的核心原则是: - -> Hybrid A* 解决“怎么走”,路径平滑解决“走得是否顺”,SQP 解决“以什么速度和时间走”,底盘控制解决“四个舵轮和车轮如何执行”。 diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs deleted file mode 100644 index f4097e3..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 建图阶段使用的可变环境占据栅格。 -/// -/// 单位:边界、世界查询和栅格边长均为 mm。 -/// 注意:只有 可以写入占据状态;规划阶段应改用不可变的 。 -/// -public sealed class EnvironmentGridMap -{ - private readonly byte[] _cells; - private int _occupiedCount; - - /// - /// 创建空的环境占据栅格。 - /// - /// 参数:bounds 为左闭右开的世界边界,单位 mm;resolutionMm 为格边长,单位 mm。 - /// 返回:无;边界为空或分辨率不合法时抛出异常。 - /// - public EnvironmentGridMap(MapBoundsMm bounds, float resolutionMm) - { - if (bounds == null) throw new ArgumentNullException(nameof(bounds)); - bounds.GetDimensions(resolutionMm, out int rows, out int cols); - Bounds = bounds; ResolutionMm = resolutionMm; Rows = rows; Cols = cols; - _cells = new byte[checked(rows * cols)]; - } - - /// 地图世界边界,单位 mm,采用左闭右开规则。 - public MapBoundsMm Bounds { get; } - /// 单个栅格边长,单位 mm。 - public float ResolutionMm { get; } - /// 栅格行数,Y 方向从下限向上递增。 - public int Rows { get; } - /// 栅格列数,X 方向从下限向右递增。 - public int Cols { get; } - /// 当前已被标记为障碍的格数。 - public int OccupiedCount { get { return _occupiedCount; } } - - /// 判断行列索引是否有效。参数 row、col 分别为从零开始的行和列;有效时返回 true。 - public bool IsInBounds(int row, int col) { return row >= 0 && row < Rows && col >= 0 && col < Cols; } - /// 判断世界坐标是否位于地图内。参数 xMm、yMm 单位为 mm;上边界与右边界返回 false。 - public bool IsWorldInBounds(float xMm, float yMm) { return Bounds.Contains(xMm, yMm); } - - /// - /// 将世界坐标转换为栅格索引。 - /// - /// 参数:xMm、yMm 为世界坐标,单位 mm;row、col 为输出索引。 - /// 返回:坐标在地图内时为 true 并写入索引;否则返回 false,两个输出均为 -1。 - /// - public bool TryWorldToGrid(float xMm, float yMm, out int row, out int col) - { - row = -1; col = -1; - if (!IsWorldInBounds(xMm, yMm)) return false; - col = (int)Math.Floor(((double)xMm - Bounds.XMin) / ResolutionMm); - row = (int)Math.Floor(((double)yMm - Bounds.YMin) / ResolutionMm); - return IsInBounds(row, col); - } - - /// 查询栅格是否占据。越界索引按障碍处理,返回 true。 - public bool IsOccupied(int row, int col) { return !IsInBounds(row, col) || _cells[row * Cols + col] != 0; } - /// 按世界坐标查询占据状态。参数 xMm、yMm 单位为 mm;坐标越界时保守地返回 true。 - public bool IsOccupiedWorld(float xMm, float yMm) - { - return !TryWorldToGrid(xMm, yMm, out int row, out int col) || IsOccupied(row, col); - } - - /// - /// 获取一个栅格的世界坐标范围。 - /// - /// 参数:row、col 为有效索引;xMin、xMax、yMin、yMax 为输出边界,单位 mm。 - /// 返回:无;索引越界时抛出 。 - /// - public void GetCellBounds(int row, int col, out float xMin, out float xMax, out float yMin, out float yMax) - { - if (!IsInBounds(row, col)) throw new ArgumentOutOfRangeException(); - xMin = Bounds.XMin + col * ResolutionMm; - yMin = Bounds.YMin + row * ResolutionMm; - xMax = Math.Min(Bounds.XMax, xMin + ResolutionMm); - yMax = Math.Min(Bounds.YMax, yMin + ResolutionMm); - } - - internal void MarkOccupied(int row, int col) - { - if (!IsInBounds(row, col)) return; - int index = row * Cols + col; - if (_cells[index] == 0) { _cells[index] = 1; _occupiedCount++; } - } - internal byte[] CopyCells() { return (byte[])_cells.Clone(); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs deleted file mode 100644 index d084079..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 环境占据图构建结果。 -/// 返回:成功时提供可供适配的 EnvironmentGridMap;失败时提供失败原因和已处理来源状态。 -/// -public sealed class EnvironmentMapBuildResult -{ - private EnvironmentMapBuildResult(bool succeeded, EnvironmentGridMap map, IReadOnlyList sourceResults, string failureReason, PlanningOperationStopReason stopReason) - { - Succeeded = succeeded; Map = map; SourceResults = sourceResults ?? Array.Empty(); FailureReason = failureReason ?? string.Empty; StopReason = stopReason; - } - /// 构建是否成功。true 时 Map 非空;false 时读取 FailureReason。 - public bool Succeeded { get; } - /// 成功生成的构建期环境栅格;失败时为 null。 - public EnvironmentGridMap Map { get; } - /// 已尝试来源的投影结果,用于记录已应用、空或失败状态。 - public IReadOnlyList SourceResults { get; } - /// 失败原因。成功时为空字符串。 - public string FailureReason { get; } - /// 内部预算停止原因;普通构建成功或失败时为 None。 - internal PlanningOperationStopReason StopReason { get; } - /// 创建成功结果。参数 map 为已完成栅格,sourceResults 为来源投影记录。 - public static EnvironmentMapBuildResult Success(EnvironmentGridMap map, IReadOnlyList sourceResults) { return new EnvironmentMapBuildResult(true, map, sourceResults, null, PlanningOperationStopReason.None); } - /// 创建失败结果。参数 reason 为诊断文本,sourceResults 可包含失败前已处理的来源。 - public static EnvironmentMapBuildResult Failure(string reason, IReadOnlyList sourceResults) { return new EnvironmentMapBuildResult(false, null, sourceResults, reason, PlanningOperationStopReason.None); } - /// 创建已取消或超时结果;不发布构建期可写地图。 - internal static EnvironmentMapBuildResult Stopped(PlanningOperationStopReason stopReason, IReadOnlyList sourceResults) - { - if (stopReason == PlanningOperationStopReason.None) throw new ArgumentOutOfRangeException(nameof(stopReason)); - return new EnvironmentMapBuildResult(false, null, sourceResults, - stopReason == PlanningOperationStopReason.Cancelled ? "地图构建已取消。" : "地图构建已超时。", stopReason); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs deleted file mode 100644 index 31e4c0d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 从排序后的纯障碍物快照事务性构建环境占据图。 -/// -/// 注意:必需来源返回不可用或无效状态时,构建整体失败;可选来源仅记录其状态并继续构建。 -/// -public sealed class EnvironmentMapBuilder -{ - /// - /// 投影所有障碍物来源并栅格化为环境地图。 - /// - /// 参数:request 包含 mm 世界边界、分辨率和来源列表;每个来源 ID 必须唯一且版本非负。 - /// 返回:成功时包含 和全部来源状态;必需来源失败时返回失败结果而不产生可用地图。 - /// - public EnvironmentMapBuildResult Build(MapBuildRequest request) - { - return Build(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); - } - - /// 使用共享预算投影来源并栅格化;停止时不发布可写环境地图。 - internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget) - { - if (budget == null) throw new ArgumentNullException(nameof(budget)); - PlanningOperationStopReason stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, null); - if (request == null || request.Bounds == null) return EnvironmentMapBuildResult.Failure("Map request and bounds are required.", null); - if (request.ObstacleSources == null) return EnvironmentMapBuildResult.Failure("Obstacle source collection is required.", null); - var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); - var results = new List(); - string previousId = null; - for (int i = 0; i < sources.Length; i++) - { - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, results); - IMapObstacleSource source = sources[i]; - if (source == null || string.IsNullOrWhiteSpace(source.SourceId) || source.SourceVersion < 0) - return EnvironmentMapBuildResult.Failure("Each source needs a non-empty id and non-negative version.", results); - if (string.Equals(previousId, source.SourceId, StringComparison.Ordinal)) - return EnvironmentMapBuildResult.Failure("Obstacle source ids must be unique.", results); - previousId = source.SourceId; - ObstacleProjectionResult result; - try { result = source.ProjectToWorld() ?? ObstacleProjectionResult.Invalid("Source returned no projection result."); } - catch (Exception exception) { result = ObstacleProjectionResult.Invalid(exception.Message); } - results.Add(result); - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, results); - if (source.IsRequired && (result.Status == ObstacleSourceStatus.Invalid || result.Status == ObstacleSourceStatus.Unavailable)) - return EnvironmentMapBuildResult.Failure("A required obstacle source failed: " + source.SourceId, results); - } - var map = new EnvironmentGridMap(request.Bounds, request.ResolutionMm); - for (int i = 0; i < results.Count; i++) - if (results[i].Status == ObstacleSourceStatus.Applied) - for (int j = 0; j < results[i].Obstacles.Count; j++) - { - if (!MapObstacleRasterizer.TryRasterize(map, results[i].Obstacles[j], budget, out stopReason)) - return EnvironmentMapBuildResult.Stopped(stopReason, results); - } - return EnvironmentMapBuildResult.Success(map, results); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs deleted file mode 100644 index 96c707b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 有限的世界地图边界。 -/// 单位:mm;范围采用左闭右开 [XMin, XMax) × [YMin, YMax)。 -/// -public sealed class MapBoundsMm : IEquatable -{ - /// 单张地图允许的最大栅格数,超过该值会拒绝创建地图。 - public const int MaximumCellCount = 4000000; - - /// - /// 创建地图世界边界。 - /// - /// 参数: - /// - xMin、xMax:世界 X 轴下限和上限,单位 mm,且 xMax 必须大于 xMin。 - /// - yMin、yMax:世界 Y 轴下限和上限,单位 mm,且 yMax 必须大于 yMin。 - /// - /// 注意:边界采用左闭右开规则,上限坐标不属于地图。 - /// - public MapBoundsMm(float xMin, float xMax, float yMin, float yMax) - { - if (!NumericGuard.IsFinite(xMin) || !NumericGuard.IsFinite(xMax) || - !NumericGuard.IsFinite(yMin) || !NumericGuard.IsFinite(yMax) || - xMax <= xMin || yMax <= yMin) - throw new ArgumentOutOfRangeException(nameof(xMax), "Map bounds must be finite and non-degenerate."); - XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; - } - - /// 世界 X 轴下限,单位 mm,包含在地图内。 - public float XMin { get; } - /// 世界 X 轴上限,单位 mm,不包含在地图内。 - public float XMax { get; } - /// 世界 Y 轴下限,单位 mm,包含在地图内。 - public float YMin { get; } - /// 世界 Y 轴上限,单位 mm,不包含在地图内。 - public float YMax { get; } - - /// - /// 判断世界坐标是否属于地图边界。 - /// - /// 参数:xMm、yMm 为世界坐标,单位 mm。 - /// 返回:坐标位于 [XMin, XMax) × [YMin, YMax) 时为 true,否则为 false。 - /// - public bool Contains(float xMm, float yMm) - { - return xMm >= XMin && xMm < XMax && yMm >= YMin && yMm < YMax; - } - - /// - /// 根据栅格分辨率计算行列数。 - /// - /// 参数:resolutionMm 为每个方格的边长,单位 mm,取值必须在 [20, 200];rows、cols 为输出行数和列数。 - /// 返回:无;当分辨率无效或总格数超过 时抛出异常。 - /// - public void GetDimensions(float resolutionMm, out int rows, out int cols) - { - if (!NumericGuard.IsInRange(resolutionMm, 20f, 200f)) - throw new ArgumentOutOfRangeException(nameof(resolutionMm), "ResolutionMm must be within [20, 200]."); - double columnCount = Math.Ceiling(((double)XMax - XMin) / resolutionMm); - double rowCount = Math.Ceiling(((double)YMax - YMin) / resolutionMm); - if (columnCount > int.MaxValue || rowCount > int.MaxValue || columnCount <= 0d || rowCount <= 0d) - throw new ArgumentOutOfRangeException(nameof(resolutionMm), "Map dimensions are invalid."); - cols = (int)columnCount; rows = (int)rowCount; - long cellCount = checked((long)rows * cols); - if (cellCount > MaximumCellCount) - throw new ArgumentOutOfRangeException(nameof(resolutionMm), "Map cell count exceeds 4,000,000."); - } - - /// 比较两个边界的四个 mm 坐标是否完全相同。 - public bool Equals(MapBoundsMm other) - { - return other != null && XMin.Equals(other.XMin) && XMax.Equals(other.XMax) && - YMin.Equals(other.YMin) && YMax.Equals(other.YMax); - } - /// 比较当前边界与指定对象是否表示相同的世界范围。 - public override bool Equals(object obj) { return Equals(obj as MapBoundsMm); } - /// 返回由四个边界坐标组成的哈希值,用于缓存键比较。 - public override int GetHashCode() - { - unchecked { int hash = XMin.GetHashCode(); hash = hash * 31 + XMax.GetHashCode(); hash = hash * 31 + YMin.GetHashCode(); return hash * 31 + YMax.GetHashCode(); } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs deleted file mode 100644 index 5c8290e..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 环境占据图构建器的输入数据。 -/// 注意:通常由 PlanningMapFactory 从公开请求转换得到,调用者无需直接使用。 -/// -public sealed class MapBuildRequest -{ - /// 环境图世界边界。单位:mm;不能为空。 - public MapBoundsMm Bounds { get; set; } - /// 环境栅格边长。单位:mm;必须满足 MapBoundsMm 的分辨率限制。 - public float ResolutionMm { get; set; } - /// 待投影的障碍物来源列表;每个来源 ID 必须唯一。 - public IReadOnlyList ObstacleSources { get; set; } = Array.Empty(); -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs deleted file mode 100644 index e9ce924..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 边与世界 X/Y 轴平行的矩形障碍物,坐标单位为 mm。 -public sealed class AxisAlignedRectangleObstacle : IMapObstacle -{ - /// - /// 创建轴对齐矩形障碍物。 - /// - /// 参数:xMin、xMax、yMin、yMax 分别为矩形世界坐标边界,单位 mm。 - /// 注意:构造不校验边界顺序,请通过 判断后再使用。 - /// - public AxisAlignedRectangleObstacle(float xMin, float xMax, float yMin, float yMax) - { - XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; - } - /// 矩形世界 X 下边界,单位 mm。 - public float XMin { get; } - /// 矩形世界 X 上边界,单位 mm。 - public float XMax { get; } - /// 矩形世界 Y 下边界,单位 mm。 - public float YMin { get; } - /// 矩形世界 Y 上边界,单位 mm。 - public float YMax { get; } - /// 四个边界均为有限数且 XMax≥XMin、YMax≥YMin 时为 true;否则为 false。 - public bool IsValid - { - get { return NumericGuard.IsFinite(XMin) && NumericGuard.IsFinite(XMax) && NumericGuard.IsFinite(YMin) && NumericGuard.IsFinite(YMax) && XMax >= XMin && YMax >= YMin; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs deleted file mode 100644 index 55eee5f..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 以世界 mm 坐标表示的圆形障碍物。 -public sealed class CircleObstacle : IMapObstacle -{ - /// - /// 创建圆形障碍物。 - /// - /// 参数:centerX、centerY 为圆心世界坐标,单位 mm;radiusMm 为半径,单位 mm。 - /// 注意:构造不抛出几何校验异常,请通过 判断后再使用。 - /// - public CircleObstacle(float centerX, float centerY, float radiusMm) - { - CenterX = centerX; CenterY = centerY; RadiusMm = radiusMm; - } - /// 圆心世界 X 坐标,单位 mm。 - public float CenterX { get; } - /// 圆心世界 Y 坐标,单位 mm。 - public float CenterY { get; } - /// 圆的半径,单位 mm。 - public float RadiusMm { get; } - /// 圆心和半径均为有限数且半径不小于零时为 true;否则为 false。 - public bool IsValid - { - get { return NumericGuard.IsFinite(CenterX) && NumericGuard.IsFinite(CenterY) && NumericGuard.IsFinite(RadiusMm) && RadiusMm >= 0f; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs deleted file mode 100644 index 2377271..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 世界坐标中的不可变障碍物几何。 -/// -/// 单位:所有几何坐标与尺寸均为 mm。实现类型必须能由 栅格化。 -/// -public interface IMapObstacle -{ - /// 几何数据是否有效。true 表示数值有限且尺寸满足该几何类型的约束;false 表示不得投影到地图。 - bool IsValid { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs deleted file mode 100644 index 44d35de..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 将障碍物几何保守投影为栅格占据状态的唯一写入入口。 -/// -/// 注意:调用方不能直接改写 ;相交或贴边的栅格均按占据处理。 -/// -public static class MapObstacleRasterizer -{ - /// - /// 将一个有效障碍物栅格化到环境地图。 - /// - /// 参数:map 为待写入的环境栅格;obstacle 为世界 mm 坐标的圆形或轴对齐矩形障碍物。 - /// 返回:无。地图或障碍物为空、障碍物无效、几何类型不受支持时抛出异常。 - /// 注意:该方法只增加占据格,不会清除已有障碍。 - /// - public static void Rasterize(EnvironmentGridMap map, IMapObstacle obstacle) - { - if (!TryRasterize(map, obstacle, PlanningOperationBudget.Unlimited(CancellationToken.None), out _)) - throw new InvalidOperationException("Unbounded rasterization unexpectedly stopped."); - } - - /// 使用共享预算将障碍物写入环境栅格;停止时返回 false 且不发布环境地图。 - internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle, PlanningOperationBudget budget, - out PlanningOperationStopReason stopReason) - { - if (map == null) throw new ArgumentNullException(nameof(map)); - if (obstacle == null || !obstacle.IsValid) throw new ArgumentException("Obstacle must be valid.", nameof(obstacle)); - if (budget == null) throw new ArgumentNullException(nameof(budget)); - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return false; - int workItemCount = 0; - var circle = obstacle as CircleObstacle; - if (circle != null) return TryRasterizeCircle(map, circle, budget, ref workItemCount, out stopReason); - var rectangle = obstacle as AxisAlignedRectangleObstacle; - if (rectangle != null) return TryRasterizeRectangle(map, rectangle, budget, ref workItemCount, out stopReason); - throw new NotSupportedException("Unsupported map obstacle geometry."); - } - - private static bool TryRasterizeCircle(EnvironmentGridMap map, CircleObstacle circle, PlanningOperationBudget budget, - ref int workItemCount, out PlanningOperationStopReason stopReason) - { - GetCandidateRange(map, circle.CenterX - circle.RadiusMm, circle.CenterX + circle.RadiusMm, - circle.CenterY - circle.RadiusMm, circle.CenterY + circle.RadiusMm, - out int firstRow, out int lastRow, out int firstCol, out int lastCol); - double radiusSquared = (double)circle.RadiusMm * circle.RadiusMm; - for (int row = firstRow; row <= lastRow; row++) - for (int col = firstCol; col <= lastCol; col++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - map.GetCellBounds(row, col, out float xMin, out float xMax, out float yMin, out float yMax); - double nearestX = Math.Max(xMin, Math.Min(circle.CenterX, xMax)); - double nearestY = Math.Max(yMin, Math.Min(circle.CenterY, yMax)); - double dx = circle.CenterX - nearestX; - double dy = circle.CenterY - nearestY; - if (dx * dx + dy * dy <= radiusSquared) map.MarkOccupied(row, col); - } - stopReason = PlanningOperationStopReason.None; - return true; - } - - private static bool TryRasterizeRectangle(EnvironmentGridMap map, AxisAlignedRectangleObstacle rectangle, PlanningOperationBudget budget, - ref int workItemCount, out PlanningOperationStopReason stopReason) - { - GetCandidateRange(map, rectangle.XMin, rectangle.XMax, rectangle.YMin, rectangle.YMax, - out int firstRow, out int lastRow, out int firstCol, out int lastCol); - for (int row = firstRow; row <= lastRow; row++) - for (int col = firstCol; col <= lastCol; col++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - map.GetCellBounds(row, col, out float xMin, out float xMax, out float yMin, out float yMax); - if (rectangle.XMax >= xMin && rectangle.XMin <= xMax && rectangle.YMax >= yMin && rectangle.YMin <= yMax) - map.MarkOccupied(row, col); - } - stopReason = PlanningOperationStopReason.None; - return true; - } - - private static void GetCandidateRange(EnvironmentGridMap map, float xMin, float xMax, float yMin, float yMax, - out int firstRow, out int lastRow, out int firstCol, out int lastCol) - { - if (xMax < map.Bounds.XMin || xMin > map.Bounds.XMax || yMax < map.Bounds.YMin || yMin > map.Bounds.YMax) - { firstRow = 1; lastRow = 0; firstCol = 1; lastCol = 0; return; } - // Geometry is closed for conservative rasterisation. Include the cell on - // the lower side when a boundary lies exactly on a grid line. - firstCol = Clamp((int)Math.Floor(((double)xMin - map.Bounds.XMin) / map.ResolutionMm) - 1, 0, map.Cols - 1); - lastCol = Clamp((int)Math.Floor(((double)xMax - map.Bounds.XMin) / map.ResolutionMm), 0, map.Cols - 1); - firstRow = Clamp((int)Math.Floor(((double)yMin - map.Bounds.YMin) / map.ResolutionMm) - 1, 0, map.Rows - 1); - lastRow = Clamp((int)Math.Floor(((double)yMax - map.Bounds.YMin) / map.ResolutionMm), 0, map.Rows - 1); - } - private static int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs deleted file mode 100644 index 2bdba59..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 针对行主序二值栅格计算精确欧氏距离平方的内部算法。 -internal static class EuclideanDistanceTransform -{ - /// - /// 计算每个栅格到最近障碍栅格的距离平方。 - /// - /// 参数:occupied 为行主序占据数组,非零表示障碍;rows、cols 为数组尺寸。 - /// 返回:行主序距离平方数组,单位为栅格边长的平方;不含任何 mm 或 m 换算。 - /// - public static double[] ComputeSquaredDistances(byte[] occupied, int rows, int cols) - { - if (!TryComputeSquaredDistances(occupied, rows, cols, PlanningOperationBudget.Unlimited(CancellationToken.None), - out double[] squared, out _)) - throw new InvalidOperationException("Unbounded Euclidean distance transform unexpectedly stopped."); - return squared; - } - - /// 使用共享预算计算距离平方;停止时不返回部分数组。 - internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols, PlanningOperationBudget budget, - out double[] squaredDistances, out PlanningOperationStopReason stopReason) - { - if (occupied == null) throw new ArgumentNullException(nameof(occupied)); - if (budget == null) throw new ArgumentNullException(nameof(budget)); - squaredDistances = null; - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return false; - double noObstacleDistanceSquared = (double)rows * rows + (double)cols * cols + 1d; - var intermediate = new double[occupied.Length]; - var result = new double[occupied.Length]; - var input = new double[Math.Max(rows, cols)]; - var output = new double[Math.Max(rows, cols)]; - int workItemCount = 0; - for (int row = 0; row < rows; row++) - { - int offset = row * cols; - for (int col = 0; col < cols; col++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - input[col] = occupied[offset + col] == 0 ? noObstacleDistanceSquared : 0d; - } - if (!TryTransform1D(input, cols, output, budget, ref workItemCount, out stopReason)) return false; - for (int col = 0; col < cols; col++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - intermediate[offset + col] = output[col]; - } - } - for (int col = 0; col < cols; col++) - { - for (int row = 0; row < rows; row++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - input[row] = intermediate[row * cols + col]; - } - if (!TryTransform1D(input, rows, output, budget, ref workItemCount, out stopReason)) return false; - for (int row = 0; row < rows; row++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - result[row * cols + col] = output[row]; - } - } - squaredDistances = result; - stopReason = PlanningOperationStopReason.None; - return true; - } - - private static bool TryTransform1D(double[] f, int length, double[] result, PlanningOperationBudget budget, - ref int workItemCount, out PlanningOperationStopReason stopReason) - { - var locations = new int[length]; - var boundaries = new double[length + 1]; - int k = 0; - locations[0] = 0; - boundaries[0] = double.NegativeInfinity; - boundaries[1] = double.PositiveInfinity; - for (int q = 1; q < length; q++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - double intersection; - do - { - int p = locations[k]; - intersection = ((f[q] + (double)q * q) - (f[p] + (double)p * p)) / (2d * (q - p)); - if (intersection <= boundaries[k]) - { - k--; - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - } - } while (k >= 0 && intersection <= boundaries[k]); - if (k < 0) - { - k = 0; locations[0] = q; boundaries[0] = double.NegativeInfinity; boundaries[1] = double.PositiveInfinity; - } - else - { - k++; locations[k] = q; boundaries[k] = intersection; boundaries[k + 1] = double.PositiveInfinity; - } - } - k = 0; - for (int q = 0; q < length; q++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - while (boundaries[k + 1] < q) k++; - double delta = q - locations[k]; - result[q] = delta * delta + f[locations[k]]; - } - stopReason = PlanningOperationStopReason.None; - return true; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs deleted file mode 100644 index 385551d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 以 m 表示的障碍物净距离保守下界。 -internal sealed class ObstacleDistanceField -{ - private readonly double[] _conservativeDistances; - private ObstacleDistanceField(double[] conservativeDistances) { _conservativeDistances = conservativeDistances; } - /// - /// 从占据栅格创建距离场。 - /// - /// 参数:occupied 为行主序占据数组;rows、cols 为其尺寸;resolutionMeters 为格边长,单位 m。 - /// 返回:每个格到最近障碍物的保守净距离下界,单位 m;全空地图中的每项为正无穷。 - /// - public static ObstacleDistanceField Create(byte[] occupied, int rows, int cols, double resolutionMeters) - { - if (!TryCreate(occupied, rows, cols, resolutionMeters, PlanningOperationBudget.Unlimited(CancellationToken.None), - out ObstacleDistanceField field, out _)) - throw new InvalidOperationException("Unbounded distance-field creation unexpectedly stopped."); - return field; - } - - /// 使用共享预算创建距离场;停止时不返回部分距离数据。 - internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters, - PlanningOperationBudget budget, out ObstacleDistanceField field, out PlanningOperationStopReason stopReason) - { - if (occupied == null) throw new ArgumentNullException(nameof(occupied)); - if (budget == null) throw new ArgumentNullException(nameof(budget)); - field = null; - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return false; - bool hasObstacle = false; - int workItemCount = 0; - for (int i = 0; i < occupied.Length; i++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - if (occupied[i] != 0) { hasObstacle = true; break; } - } - var distances = new double[occupied.Length]; - if (!hasObstacle) - { - for (int i = 0; i < distances.Length; i++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - distances[i] = double.PositiveInfinity; - } - field = new ObstacleDistanceField(distances); - stopReason = PlanningOperationStopReason.None; - return true; - } - if (!EuclideanDistanceTransform.TryComputeSquaredDistances(occupied, rows, cols, budget, out double[] squared, out stopReason)) - return false; - double conservativeOffset = Math.Sqrt(2d) * resolutionMeters; - for (int i = 0; i < distances.Length; i++) - { - stopReason = budget.CheckEvery(ref workItemCount); - if (stopReason != PlanningOperationStopReason.None) return false; - distances[i] = Math.Max(0d, Math.Sqrt(squared[i]) * resolutionMeters - conservativeOffset); - } - field = new ObstacleDistanceField(distances); - stopReason = PlanningOperationStopReason.None; - return true; - } - internal double[] CopyDistances() { return (double[])_conservativeDistances.Clone(); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs deleted file mode 100644 index 0c014ca..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 供粗路径规划使用的不可变地图快照。 -/// -/// 单位:世界查询方法使用 m; 保留原始 mm 数据。 -/// 注意:世界坐标越界一律按占据处理,净距离为零。 -/// -public sealed class PlanningGridMap -{ - private readonly byte[] _occupied; - private readonly double[] _conservativeDistances; - - internal PlanningGridMap(MapBoundsMm bounds, float resolutionMm, int rows, int cols, byte[] occupied, double[] conservativeDistances, - long snapshotId, bool planningReady, string planningBlockReason, string inputFingerprint, string occupancyHash) - { - Bounds = bounds; ResolutionMm = resolutionMm; Rows = rows; Cols = cols; - _occupied = occupied; _conservativeDistances = conservativeDistances; - SnapshotId = snapshotId; PlanningReady = planningReady; PlanningBlockReason = planningBlockReason ?? string.Empty; - InputFingerprint = inputFingerprint ?? string.Empty; OccupancyHash = occupancyHash ?? string.Empty; - } - /// 源环境图的世界边界,单位 mm,采用左闭右开规则。 - public MapBoundsMm Bounds { get; } - /// 源环境图的栅格边长,单位 mm。 - public float ResolutionMm { get; } - /// 规划世界查询对应的栅格边长,单位 m。 - public double ResolutionMeters { get { return ResolutionMm / 1000d; } } - /// 栅格行数。 - public int Rows { get; } - /// 栅格列数。 - public int Cols { get; } - /// 工厂为本次返回快照分配的单调编号,用于区分不同构建结果。 - public long SnapshotId { get; } - /// 地图是否允许进入粗路径规划。true 时可直接查询;false 时应先处理 - public bool PlanningReady { get; } - /// 禁止规划的原因。 为 true 时为空字符串。 - public string PlanningBlockReason { get; } - /// 完整建图输入的稳定指纹,用于识别精确输入缓存命中。 - public string InputFingerprint { get; } - /// 占据栅格内容哈希,用于识别可复用的占据与距离数组。 - public string OccupancyHash { get; } - - /// 查询世界位置是否占据。参数 xMeters、yMeters 单位为 m;位置越界时保守地返回 true。 - public bool IsOccupiedWorld(double xMeters, double yMeters) - { - return !TryWorldToGrid(xMeters, yMeters, out int row, out int col) || _occupied[row * Cols + col] != 0; - } - /// - /// 查询到最近障碍物的保守净距离下界。 - /// - /// 参数:xMeters、yMeters 为世界坐标,单位 m。 - /// 返回:单位 m 的非负距离下界;地图内无障碍物时为正无穷,越界时为零。 - /// - public double GetConservativeObstacleDistanceMeters(double xMeters, double yMeters) - { - return !TryWorldToGrid(xMeters, yMeters, out int row, out int col) ? 0d : _conservativeDistances[row * Cols + col]; - } - /// - /// 将规划世界坐标转换为栅格索引。 - /// - /// 参数:xMeters、yMeters 为世界坐标,单位 m;row、col 为输出索引。 - /// 返回:位置在地图内时为 true 并写入索引;否则返回 false,两个输出均为 -1。 - /// - public bool TryWorldToGrid(double xMeters, double yMeters, out int row, out int col) - { - row = -1; col = -1; - double xMm = xMeters * 1000d, yMm = yMeters * 1000d; - if (xMm < Bounds.XMin || xMm >= Bounds.XMax || yMm < Bounds.YMin || yMm >= Bounds.YMax) return false; - col = (int)Math.Floor((xMm - Bounds.XMin) / ResolutionMm); - row = (int)Math.Floor((yMm - Bounds.YMin) / ResolutionMm); - return row >= 0 && row < Rows && col >= 0 && col < Cols; - } - /// 按行列索引查询占据状态。参数从零开始;任一索引越界时返回 true。 - public bool IsOccupied(int row, int col) { return row < 0 || row >= Rows || col < 0 || col >= Cols || _occupied[row * Cols + col] != 0; } - internal byte[] CopyOccupied() { return (byte[])_occupied.Clone(); } - internal bool OccupancyEquals(PlanningGridMap other) - { - if (other == null || Rows != other.Rows || Cols != other.Cols || ResolutionMm != other.ResolutionMm || !Bounds.Equals(other.Bounds) || _occupied.Length != other._occupied.Length) return false; - for (int i = 0; i < _occupied.Length; i++) if (_occupied[i] != other._occupied[i]) return false; - return true; - } - internal PlanningGridMap WithMetadata(long snapshotId, bool planningReady, string blockReason, string inputFingerprint, string occupancyHash) - { - return new PlanningGridMap(Bounds, ResolutionMm, Rows, Cols, _occupied, _conservativeDistances, snapshotId, planningReady, blockReason, inputFingerprint, occupancyHash); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs deleted file mode 100644 index 3c292d9..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 将建图阶段的 mm 环境栅格适配为规划阶段的不可变 m 查询快照。 -public static class PlanningMapAdapter -{ - /// - /// 创建规划地图快照及其保守距离场。 - /// - /// 参数:environmentMap 为已经完成障碍物栅格化的环境图,坐标与分辨率单位均为 mm。 - /// 返回:不可变的 ;其世界查询使用 m,初始元数据由工厂随后分配。 - /// - public static PlanningGridMap Create(EnvironmentGridMap environmentMap) - { - if (!TryCreate(environmentMap, PlanningOperationBudget.Unlimited(CancellationToken.None), out PlanningGridMap map, out _)) - throw new InvalidOperationException("Unbounded planning-map adaptation unexpectedly stopped."); - return map; - } - - /// 使用共享预算创建规划快照;停止时不返回部分地图。 - internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget, - out PlanningGridMap map, out PlanningOperationStopReason stopReason) - { - if (environmentMap == null) throw new ArgumentNullException(nameof(environmentMap)); - if (budget == null) throw new ArgumentNullException(nameof(budget)); - map = null; - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return false; - byte[] occupied = environmentMap.CopyCells(); - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return false; - if (!ObstacleDistanceField.TryCreate(occupied, environmentMap.Rows, environmentMap.Cols, - environmentMap.ResolutionMm / 1000d, budget, out ObstacleDistanceField field, out stopReason)) - return false; - map = new PlanningGridMap(environmentMap.Bounds, environmentMap.ResolutionMm, environmentMap.Rows, environmentMap.Cols, - occupied, field.CopyDistances(), 0, false, "Map metadata has not been assigned.", string.Empty, string.Empty); - stopReason = budget.GetStopReason(); - if (stopReason == PlanningOperationStopReason.None) return true; - map = null; - return false; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs deleted file mode 100644 index 06fe232..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 线程安全、容量为四的 LRU 缓存,分别复用精确输入结果和不可变占据数组。 -internal sealed class PlanningMapCache -{ - private const int Capacity = 4; - private readonly object _gate = new object(); - private readonly LinkedList _inputs = new LinkedList(); - private readonly LinkedList _occupancies = new LinkedList(); - - /// 按完整输入描述查询缓存。命中时返回原始快照与来源结果,并提升其最近使用顺序。 - public bool TryGetInput(string descriptor, out PlanningGridMap map, out IReadOnlyList sourceResults) - { - lock (_gate) - { - for (var node = _inputs.First; node != null; node = node.Next) - if (string.Equals(node.Value.Descriptor, descriptor, StringComparison.Ordinal)) - { map = node.Value.Map; sourceResults = node.Value.SourceResults; _inputs.Remove(node); _inputs.AddFirst(node); return true; } - map = null; sourceResults = null; return false; - } - } - - /// 按占据哈希及逐格比较查询缓存。命中时返回共享数组的规范快照,供工厂创建新的元数据快照。 - public bool TryGetOccupancy(string hash, PlanningGridMap candidate, out PlanningGridMap canonical) - { - lock (_gate) - { - for (var node = _occupancies.First; node != null; node = node.Next) - if (string.Equals(node.Value.Hash, hash, StringComparison.Ordinal) && node.Value.Map.OccupancyEquals(candidate)) - { canonical = node.Value.Map; _occupancies.Remove(node); _occupancies.AddFirst(node); return true; } - canonical = null; return false; - } - } - - /// 写入或更新精确输入缓存。参数 descriptor 为完整输入键,map 为不可变快照,sourceResults 为对应来源结果。 - public void AddInput(string descriptor, PlanningGridMap map, IReadOnlyList sourceResults) - { - lock (_gate) - { - for (var node = _inputs.First; node != null; node = node.Next) - if (string.Equals(node.Value.Descriptor, descriptor, StringComparison.Ordinal)) { node.Value.Map = map; node.Value.SourceResults = sourceResults; _inputs.Remove(node); _inputs.AddFirst(node); return; } - _inputs.AddFirst(new InputEntry(descriptor, map, sourceResults)); - while (_inputs.Count > Capacity) _inputs.RemoveLast(); - } - } - - /// 写入占据缓存。参数 hash 为占据内容哈希,map 为包含可复用占据与距离数组的快照。 - public void AddOccupancy(string hash, PlanningGridMap map) - { - lock (_gate) - { - for (var node = _occupancies.First; node != null; node = node.Next) - if (string.Equals(node.Value.Hash, hash, StringComparison.Ordinal) && node.Value.Map.OccupancyEquals(map)) - { _occupancies.Remove(node); _occupancies.AddFirst(node); return; } - _occupancies.AddFirst(new OccupancyEntry(hash, map)); - while (_occupancies.Count > Capacity) _occupancies.RemoveLast(); - } - } - - private sealed class InputEntry { public InputEntry(string descriptor, PlanningGridMap map, IReadOnlyList sourceResults) { Descriptor = descriptor; Map = map; SourceResults = sourceResults; } public string Descriptor { get; } public PlanningGridMap Map { get; set; } public IReadOnlyList SourceResults { get; set; } } - private sealed class OccupancyEntry { public OccupancyEntry(string hash, PlanningGridMap map) { Hash = hash; Map = map; } public string Hash { get; } public PlanningGridMap Map { get; } } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs deleted file mode 100644 index a778d3b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 规划地图创建的终止状态。 -public enum PlanningMapBuildStatus -{ - /// 已成功创建可用的不可变地图快照。 - Success, - /// 输入、来源或地图构建失败。 - Failed, - /// 调用方在地图创建期间取消了操作。 - Cancelled, - /// 地图创建消耗了整次规划操作的总超时预算。 - TimedOut, -} - -/// 本次地图创建使用的缓存层级,反映快照或占据数组的复用方式。 -public enum PlanningMapCacheHit -{ - /// 未命中缓存;本次重新创建了占据图和距离场。 - None, - /// 完整输入命中;返回与上次完全相同的不可变地图对象。 - Input, - /// 占据内容命中;复用占据和距离数组,但生成新的快照元数据。 - Occupancy, -} - -/// -/// 规划地图创建的最终结果。 -/// -/// 返回: -/// - Succeeded 为 true 时 Map 可用;false 时通过 FailureReason 获取原因。 -/// - SourceResults 始终保留已处理来源的投影状态,便于诊断。 -/// -public sealed class PlanningMapBuildResult -{ - private PlanningMapBuildResult(PlanningMapBuildStatus status, string failureReason, IReadOnlyList sourceResults, PlanningMapCacheHit cacheHit, PlanningGridMap map) - { - Status = status; FailureReason = failureReason ?? string.Empty; SourceResults = sourceResults ?? Array.Empty(); CacheHit = cacheHit; Map = map; - } - /// 本次建图的显式终止状态;取消和超时不提供地图快照。 - public PlanningMapBuildStatus Status { get; } - /// 本次建图是否成功。true 表示 Status 为 Success 且 Map 不为空;false 时读取 FailureReason。 - public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } } - /// 建图失败的可读诊断。成功时为空字符串。 - public string FailureReason { get; } - /// 每个障碍物来源的投影结果,按建图器排序后的来源顺序排列。 - public IReadOnlyList SourceResults { get; } - /// 本次调用的缓存复用层级,用于性能诊断,不影响地图正确性。 - public PlanningMapCacheHit CacheHit { get; } - /// 成功时返回的不可变规划地图;失败时为 null。 - public PlanningGridMap Map { get; } - internal static PlanningMapBuildResult Success(PlanningGridMap map, IReadOnlyList sourceResults, PlanningMapCacheHit cacheHit) { return new PlanningMapBuildResult(PlanningMapBuildStatus.Success, null, sourceResults, cacheHit, map); } - internal static PlanningMapBuildResult Failure(string reason, IReadOnlyList sourceResults) { return new PlanningMapBuildResult(PlanningMapBuildStatus.Failed, reason, sourceResults, PlanningMapCacheHit.None, null); } - internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason stopReason, IReadOnlyList sourceResults) - { - if (stopReason == PlanningOperationStopReason.Cancelled) - return new PlanningMapBuildResult(PlanningMapBuildStatus.Cancelled, "地图创建已取消。", sourceResults, PlanningMapCacheHit.None, null); - if (stopReason == PlanningOperationStopReason.TimedOut) - return new PlanningMapBuildResult(PlanningMapBuildStatus.TimedOut, "地图创建已超时。", sourceResults, PlanningMapCacheHit.None, null); - throw new ArgumentOutOfRangeException(nameof(stopReason)); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs deleted file mode 100644 index 1dc7e04..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 规划地图的唯一公开创建入口。 -/// -/// 注意: -/// - 应在规划服务生命周期内长期复用同一实例,才能命中输入和占据两级缓存。 -/// - 本类只消费请求中的纯障碍物快照,不读取传感器、定位、UI 或系统时钟。 -/// -public sealed class PlanningMapFactory -{ - private readonly EnvironmentMapBuilder _builder = new EnvironmentMapBuilder(); - private readonly PlanningMapCache _cache = new PlanningMapCache(); - private readonly object _createGate = new object(); - private long _nextSnapshotId; - - /// - /// 创建规划地图快照。 - /// - /// 参数: - /// - request:完整建图请求;包含世界范围、栅格分辨率、障碍物来源和空图策略,不能为空。 - /// - /// 返回: - /// - PlanningMapBuildResult:成功时包含不可变 PlanningGridMap、每个来源的投影结果和缓存命中类型; - /// 失败时包含失败原因,不提供地图。 - /// - /// 注意: - /// - 完全相同的输入返回同一个快照对象;版本变化但占据相同则复用底层栅格并生成新快照编号。 - /// - public PlanningMapBuildResult Create(PlanningMapRequest request) - { - return Create(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); - } - - /// 使用共享预算创建地图;取消或超时时不发布快照或缓存条目。 - internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget) - { - if (budget == null) throw new ArgumentNullException(nameof(budget)); - PlanningOperationStopReason stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); - - bool enteredCreateGate = false; - try - { - while (!Monitor.TryEnter(_createGate, 16)) - { - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); - } - enteredCreateGate = true; - - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); - if (request == null || request.Bounds == null) return PlanningMapBuildResult.Failure("Planning map request and bounds are required.", null); - if (request.ObstacleSources == null) return PlanningMapBuildResult.Failure("Obstacle source collection is required.", null); - string requestKey = BuildRequestKey(request); - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); - if (_cache.TryGetInput(requestKey, out PlanningGridMap exactMap, out IReadOnlyList cachedResults)) - { - stopReason = budget.GetStopReason(); - return stopReason == PlanningOperationStopReason.None - ? PlanningMapBuildResult.Success(exactMap, cachedResults, PlanningMapCacheHit.Input) - : PlanningMapBuildResult.Stopped(stopReason, cachedResults); - } - - EnvironmentMapBuildResult environment = _builder.Build( - new MapBuildRequest { Bounds = request.Bounds, ResolutionMm = request.ResolutionMm, ObstacleSources = request.ObstacleSources }, budget); - if (!environment.Succeeded) - return environment.StopReason == PlanningOperationStopReason.None - ? PlanningMapBuildResult.Failure(environment.FailureReason, environment.SourceResults) - : PlanningMapBuildResult.Stopped(environment.StopReason, environment.SourceResults); - - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); - string descriptor = BuildInputDescriptor(request, environment.SourceResults); - string inputFingerprint = Sha256(descriptor); - int applied = environment.SourceResults.Count(x => x.Status == ObstacleSourceStatus.Applied); - bool ready = applied > 0 || request.AllowExplicitEmptyMap; - string blockReason = ready ? string.Empty : "No source supplied obstacle geometry; set AllowExplicitEmptyMap only when an intentionally empty map is safe."; - if (!PlanningMapAdapter.TryCreate(environment.Map, budget, out PlanningGridMap candidate, out stopReason)) - return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); - if (!TryComputeOccupancyHash(candidate, budget, out string occupancyHash, out stopReason)) - return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); - - PlanningGridMap map; - PlanningMapCacheHit cacheHit; - if (_cache.TryGetOccupancy(occupancyHash, candidate, out PlanningGridMap canonical)) - { - map = canonical.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); - cacheHit = PlanningMapCacheHit.Occupancy; - } - else - { - map = candidate.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); - _cache.AddOccupancy(occupancyHash, map); - cacheHit = PlanningMapCacheHit.None; - } - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); - _cache.AddInput(requestKey, map, environment.SourceResults); - return PlanningMapBuildResult.Success(map, environment.SourceResults, cacheHit); - } - finally - { - if (enteredCreateGate) Monitor.Exit(_createGate); - } - } - - private long NextSnapshotId() { return Interlocked.Increment(ref _nextSnapshotId); } - - private static string BuildInputDescriptor(PlanningMapRequest request, IReadOnlyList results) - { - var builder = new StringBuilder(); - AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); - var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); - for (int i = 0; i < sources.Length; i++) - { - IMapObstacleSource source = sources[i]; - builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); - if (i < results.Count) AppendProjection(builder, results[i]); - } - return builder.ToString(); - } - private static string BuildRequestKey(PlanningMapRequest request) - { - var builder = new StringBuilder(); - AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); - foreach (IMapObstacleSource source in request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal)) - builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); - return builder.ToString(); - } - private static void AppendProjection(StringBuilder builder, ObstacleProjectionResult result) - { - builder.Append('|').Append((int)result.Status); - for (int index = 0; index < result.Obstacles.Count; index++) - { - var circle = result.Obstacles[index] as CircleObstacle; - if (circle != null) { builder.Append("|C"); AppendFloat(builder, circle.CenterX); AppendFloat(builder, circle.CenterY); AppendFloat(builder, circle.RadiusMm); continue; } - var rectangle = result.Obstacles[index] as AxisAlignedRectangleObstacle; - if (rectangle != null) { builder.Append("|R"); AppendFloat(builder, rectangle.XMin); AppendFloat(builder, rectangle.XMax); AppendFloat(builder, rectangle.YMin); AppendFloat(builder, rectangle.YMax); } - } - } - private static void AppendFloat(StringBuilder builder, float value) { builder.Append(BitConverter.ToInt32(BitConverter.GetBytes(value), 0).ToString("X8")); } - private static bool TryComputeOccupancyHash(PlanningGridMap map, PlanningOperationBudget budget, out string occupancyHash, - out PlanningOperationStopReason stopReason) - { - byte[] occupied = map.CopyOccupied(); - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } - using (var sha = SHA256.Create()) - { - const int blockLength = 4096; - int offset = 0; - while (occupied.Length - offset > blockLength) - { - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } - sha.TransformBlock(occupied, offset, blockLength, occupied, offset); - offset += blockLength; - } - stopReason = budget.GetStopReason(); - if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } - byte[] hash = sha.TransformFinalBlock(occupied, offset, occupied.Length - offset); - occupancyHash = BitConverter.ToString(hash).Replace("-", string.Empty); - stopReason = PlanningOperationStopReason.None; - return true; - } - } - private static string Sha256(string text) - { - using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(text))).Replace("-", string.Empty); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs deleted file mode 100644 index 26a6822..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 规划地图的完整建图输入。 -/// -/// 注意: -/// - 此对象只描述地图内容,不包含车辆、起终点、PNG 或终端日志等调试参数。 -/// - 障碍物来源快照变化时,调用方必须更新对应的 SourceVersion。 -/// -public sealed class PlanningMapRequest -{ - /// - /// 地图世界边界。 - /// 单位:mm;边界采用左闭右开范围,由 MapBoundsMm 的 XMin、XMax、YMin、YMax 定义。 - /// - public MapBoundsMm Bounds { get; set; } - /// - /// 栅格边长。 - /// 单位:mm;当前 MapBoundsMm 允许的范围为 20 至 200 mm。 - /// - public float ResolutionMm { get; set; } - /// - /// 参与建图的统一障碍物来源。 - /// 参数:列表中的每个来源必须具有唯一 ID 和非负版本号;可为空列表。 - /// - public IReadOnlyList ObstacleSources { get; set; } = Array.Empty(); - /// - /// 是否明确允许没有任何障碍物几何的地图参与规划。 - /// 返回语义:false 时,隐式空图会被标记为不可规划;true 时空图可用于规划。 - /// - public bool AllowExplicitEmptyMap { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/README.md b/.task8-sweep/ParkrobTrajplanner/Map/README.md deleted file mode 100644 index 1d1861d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/README.md +++ /dev/null @@ -1,367 +0,0 @@ -# Map 模块说明 - -`Map` 为粗路径规划提供只读、可复用的规划地图快照。它只负责把外部障碍物投影并栅格化,生成占据图和保守障碍距离场;不读取传感器、定位、UI 或系统时钟,也不写入 AMR 自身占据和安全外扩。 - -规划器应长期持有一个 `PlanningMapFactory`,通过一次 `Create` 调用取得 `PlanningGridMap`。 - -## 文件结构 - -```text -Map/ -├── README.md # 本模块说明:结构、数据流、单位和调用方式 -├── PlanningMapRequest.cs # 公开建图输入:边界、分辨率、障碍来源、空图策略 -├── PlanningMapBuildResult.cs # 公开建图输出:状态、地图、来源结果、失败原因、缓存命中类型 -├── PlanningMapFactory.cs # 唯一公开建图门面;负责两级缓存和快照编号 -├── Core/ -│ ├── MapBoundsMm.cs # 世界地图范围及行列尺寸计算 -│ ├── MapBuildRequest.cs # EnvironmentMapBuilder 的内部建图输入 -│ ├── EnvironmentMapBuildResult.cs # 环境栅格构建结果 -│ ├── EnvironmentGridMap.cs # 构建期可写的环境占据栅格 -│ └── EnvironmentMapBuilder.cs # 汇总障碍来源并事务性创建环境图 -├── Obstacles/ -│ ├── IMapObstacle.cs # 世界坐标障碍物几何契约 -│ ├── CircleObstacle.cs # 圆形障碍物几何 -│ ├── AxisAlignedRectangleObstacle.cs # 与坐标轴平行的矩形障碍物几何 -│ └── MapObstacleRasterizer.cs # 唯一允许写入环境栅格的障碍物栅格化器 -├── Sources/ -│ ├── IMapObstacleSource.cs # 统一障碍来源接口 -│ ├── ObstacleSourceStatus.cs # 来源投影状态:已应用、空、不可用、无效 -│ ├── ObstacleProjectionResult.cs # 单个来源的世界几何和诊断结果 -│ ├── ManualObstacleSource.cs # 手工输入的圆形/矩形障碍来源 -│ ├── TwoLegProjectionInput.cs # 检测时刻的 TwoLeg 纯数据快照 -│ ├── TwoLegObstacleProjector.cs # 将 TwoLeg 局部坐标投影为世界坐标圆障碍物 -│ └── TwoLegObstacleSource.cs # 将 TwoLeg 快照包装为统一障碍来源 -├── Planning/ -│ ├── PlanningGridMap.cs # 不可变规划快照;规划查询使用米 -│ ├── PlanningMapAdapter.cs # 环境图到规划快照与距离场的适配器 -│ ├── EuclideanDistanceTransform.cs # 二值栅格的精确平方欧氏距离变换 -│ ├── ObstacleDistanceField.cs # 对规划器暴露的保守障碍净距 -│ └── PlanningMapCache.cs # 容量为 4 的输入/占据两级 LRU 缓存 -└── Test/ - ├── MovementTest.MapTest.cs # Clumsy 手工建图测试入口与终端调试开关 - └── Visualization/ - ├── PlanningMapImageExportRequest.cs # PNG 导出输入:规划快照和输出目录 - ├── PlanningMapImageExportResult.cs # PNG 导出状态、路径、尺寸和诊断 - ├── PlanningMapImageExporter.cs # 可选 PNG 导出门面和输出保护 - ├── PlanningMapImageRenderer.cs # 只读快照到 RGBA 像素的渲染器 - └── ValidatedPngWriter.cs # 写入并校验 PNG 结构和 CRC -``` - -## 建图数据流 - -```text -PlanningMapRequest - │ - ▼ -IMapObstacleSource.ProjectToWorld() - │ 输出世界坐标的圆形或矩形几何 - ▼ -EnvironmentMapBuilder + MapObstacleRasterizer - │ 写入构建期 EnvironmentGridMap - ▼ -PlanningMapAdapter + ObstacleDistanceField - │ 生成占据数组和保守距离数组 - ▼ -PlanningGridMap -``` - -具体规则: - -1. 调用者准备 `PlanningMapRequest` 和一个或多个 `IMapObstacleSource`。 -2. 每个来源通过 `ProjectToWorld()` 输出世界坐标几何;Map 核心不主动读取 TwoLeg、定位或其他传感器。 -3. `EnvironmentMapBuilder` 按来源 ID 排序,必需来源失败则整个建图失败;可选来源失败只保留诊断状态。 -4. `MapObstacleRasterizer` 是唯一写入 `EnvironmentGridMap` 占据格的组件。 -5. `PlanningMapAdapter` 生成不可变 `PlanningGridMap`,并附带保守障碍距离场。 -6. `PlanningMapFactory` 返回最终快照及来源投影结果;粗路径规划只应消费这个快照。 - -## 构建状态与停止 - -`PlanningMapBuildResult.Status` 的类型为 `PlanningMapBuildStatus`: - -- `Success`:成功发布不可变 `PlanningGridMap`; -- `Failed`:输入、来源或常规建图失败,读取 `FailureReason`; -- `Cancelled`:调用方取消了带预算的建图,`Map` 为 `null`,不会写入缓存; -- `TimedOut`:建图耗尽调用方的总超时预算,`Map` 为 `null`,不会写入缓存。 - -公开的 `PlanningMapFactory.Create(request)` 保持兼容且不设置时间限制;粗规划门面使用内部预算入口,使取消和超时能够覆盖地图创建、距离场和后续搜索。 - -## 坐标与单位 - -- 环境地图边界、障碍物几何、TwoLeg 投影输入均使用世界坐标,单位为 **mm**。 -- `MapBoundsMm` 范围采用左闭右开:`[XMin, XMax) × [YMin, YMax)`;最大边界不属于地图。 -- `EnvironmentGridMap` 查询使用 mm;`PlanningGridMap` 的世界查询使用 **m**。 -- 行 `row` 对应 Y 方向,列 `col` 对应 X 方向,存储顺序为行主序 `row * Cols + col`。 -- `PlanningGridMap` 的越界位置按占据处理,障碍净距返回 0 m。 -- Map 不做车辆自身占据或安全外扩;车辆外形与安全裕度由后续碰撞检测负责。 - -## 最小调用示例 - -```csharp -var mapFactory = new PlanningMapFactory(); // 长期持有,不要每次规划重新创建 - -IMapObstacle[] obstacles = -{ - new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), - new CircleObstacle(3600f, 1200f, 180f), -}; - -var request = new PlanningMapRequest -{ - Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), - ResolutionMm = 50f, - ObstacleSources = new IMapObstacleSource[] - { - new ManualObstacleSource("manual", 1L, true, obstacles), - }, - AllowExplicitEmptyMap = false, -}; - -PlanningMapBuildResult result = mapFactory.Create(request); -if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) - throw new InvalidOperationException(result.FailureReason); - -PlanningGridMap map = result.Map; // 交给粗路径规划器 -``` - -真实 TwoLeg 数据应由上层检测模块在检测时刻构造成 `TwoLegProjectionInput`,再交给 `TwoLegObstacleSource`。不要让 Map 模块主动读取检测器或定位器。 - -## 缓存与版本 - -`PlanningMapFactory` 内部有容量为 4 的两级 LRU 缓存: - -- **输入命中(Input)**:边界、分辨率、空图策略、来源 ID、`SourceVersion` 和必需性均相同,直接返回同一个 `PlanningGridMap` 对象。 -- **占据命中(Occupancy)**:来源版本变化,但最终占据栅格相同,复用不可变的占据/距离数组,并颁发新的 `SnapshotId`。 -- **未命中(None)**:栅格内容变化,重新生成规划快照。 - -因此,障碍来源的快照内容发生变化时,调用方必须增加其 `SourceVersion`。未递增版本会错误复用旧地图;仅修改日志、PNG 开关、起终点或车辆参数不应改变地图版本。 - -## 测试与调试 - -- `Test/MovementTest.MapTest.cs` 是手工 Clumsy 测试入口。文件顶部可设置地图范围、分辨率、TwoLeg 测试快照、`EnableTerminalDebugLog` 和 `SavePng`。 -- `PlanningMapImageExporter` 仅在 `SavePng` 开启时输出 PNG;它只读取 `PlanningGridMap`,不参与建图、缓存键或规划结果。 -- 自动检查脚本位于 `ClumsyPilot/tests`:工具、工厂、适配器、PNG 和 MapTest 配置分别有独立验证脚本。 -- 旧版 `Occupancygird_Map/Map_test/TrapMapImageExporter.cs` 与 `MovementTest.Trapmaptest.cs` 仍保留作历史对照;它们不是新粗路径规划的运行时地图入口。 - -## 详细使用指南 - -本节说明调用方如何从“障碍物数据”逐步得到可交给粗路径规划器的 `PlanningGridMap`。新地图的唯一创建入口是: - -```csharp -PlanningMapBuildResult result = mapFactory.Create(request); -``` - -其中 `mapFactory` 是长期持有的 `PlanningMapFactory`,`request` 是本次建图输入,`result.Map` 是成功时的只读规划地图。 - -### 第 1 步:长期创建地图工厂 - -地图工厂内部维护容量为 4 的缓存,因此不要在每次规划前重新创建它。应把它作为规划服务或 MovementTest 的字段长期保存: - -```csharp -using MultiWheelC.TrajectoryPlanning.Mapping; - -private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); -``` - -### 第 2 步:准备手工障碍物 - -圆形和轴对齐矩形都实现 `IMapObstacle`。障碍物的坐标是**世界坐标**,单位都是 **mm**。 - -```csharp -IMapObstacle[] manualObstacles = -{ - // 参数依次为:X最小值、X最大值、Y最小值、Y最大值,单位均为 mm。 - new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), - - // 参数依次为:圆心X、圆心Y、半径,单位均为 mm。 - new CircleObstacle(3600f, 1200f, 180f), -}; -``` - -目前 Map 只负责“环境障碍物”。不要在这里添加 AMR 自身外形,也不要在圆半径或矩形尺寸中叠加安全裕度;车辆外形与安全距离由粗路径的碰撞检查处理。 - -### 第 3 步:包装为统一障碍来源 - -所有障碍物必须通过 `IMapObstacleSource` 进入地图。手工障碍使用 `ManualObstacleSource`: - -```csharp -var manualSource = new ManualObstacleSource( - sourceId: "manual", - sourceVersion: 1L, - isRequired: true, - obstacles: manualObstacles); -``` - -参数意义如下: - -- `sourceId`:来源的唯一名称;同一次请求内不能重复。 -- `sourceVersion`:来源快照版本。障碍物位置、数量、半径或尺寸变化后,必须递增。 -- `isRequired`:`true` 表示来源无效时整次建图失败;`false` 表示仅记录该来源状态并继续建图。 -- `obstacles`:当前时刻的不可变障碍物集合。 - -例如障碍物内容变化后,应创建带新版本号的来源: - -```csharp -var changedManualSource = new ManualObstacleSource( - "manual", - 2L, // 1L 变为 2L,通知工厂地图输入已改变 - true, - changedObstacles); -``` - -### 第 4 步:可选地加入 TwoLeg 障碍物 - -Map 不主动调用 TwoLeg 检测器。上层检测模块应在检测时刻取得 AMR 世界位姿和两腿局部坐标,构造成 `TwoLegProjectionInput`: - -```csharp -var twoLegInput = new TwoLegProjectionInput( - hasDetection: true, - - // 检测时 AMR 的世界位姿:位置单位 mm,航向单位 rad。 - detectionWorldX: 1000f, - detectionWorldY: 2000f, - detectionHeadingRadians: 0d, - - // 两条腿相对于检测时 AMR 位姿的局部坐标,单位 mm。 - firstLocalX: 300f, - firstLocalY: 150f, - secondLocalX: 300f, - secondLocalY: -150f, - - // 每条腿最终投影为圆形障碍物的半径,单位 mm。 - radiusMm: 80f, - diagnostic: "TwoLeg 检测快照"); - -var twoLegSource = new TwoLegObstacleSource( - sourceId: "two-leg", - sourceVersion: 5L, - isRequired: false, - input: twoLegInput); -``` - -没有检测结果时仍可传入空快照: - -```csharp -var noTwoLegInput = new TwoLegProjectionInput( - false, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, - "当前没有 TwoLeg 检测结果"); -``` - -`hasDetection` 为 `false` 时,该来源会返回“空”结果,不会将零坐标当作障碍物。若 TwoLeg 只是可选信息,建议 `isRequired` 设置为 `false`。 - -### 第 5 步:创建建图请求 - -边界与分辨率仍使用 **mm**。范围采用左闭右开,例如 `XMax = 6000f` 时,`x = 6000f` 不属于地图。 - -```csharp -var request = new PlanningMapRequest -{ - // 世界范围:[0, 6000) × [0, 4000),单位 mm。 - Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), - - // 每格 50 mm。 - ResolutionMm = 50f, - - ObstacleSources = new IMapObstacleSource[] - { - manualSource, - twoLegSource, - }, - - // false:没有任何有效障碍物时,地图不能直接交给规划器。 - // true:调用方明确确认空地图安全时,才允许空图参与规划。 - AllowExplicitEmptyMap = false, -}; -``` - -### 第 6 步:创建地图并处理失败 - -```csharp -PlanningMapBuildResult result = _mapFactory.Create(request); - -if (!result.Succeeded) -{ - Console.WriteLine("建图失败:" + result.FailureReason); - return; -} - -if (result.Map == null || !result.Map.PlanningReady) -{ - Console.WriteLine("地图不能用于规划:" + result.Map?.PlanningBlockReason); - return; -} - -PlanningGridMap planningMap = result.Map; -``` - -此处必须同时检查: - -- `Succeeded`:请求、来源和栅格构建过程是否成功; -- `Map != null`:是否产出了规划快照; -- `PlanningReady`:是否允许把该快照交给规划器。隐式空图会在这一项被拦截。 - -需要查看每个来源是否成功投影时,读取 `result.SourceResults`;需要观察缓存效果时,读取 `result.CacheHit`。 - -```csharp -Console.WriteLine( - "快照编号=" + planningMap.SnapshotId - + ",缓存=" + result.CacheHit - + ",栅格=" + planningMap.Cols + "×" + planningMap.Rows); -``` - -### 第 7 步:交给粗路径规划器查询 - -`PlanningGridMap` 是不可变对象,可以安全地作为一次规划任务的输入。注意:它的世界查询坐标单位已经变成 **m**,而不是 mm。 - -```csharp -// 查询 (2.5 m, 1.0 m) 是否位于占据格;越界也会返回 true。 -bool occupied = planningMap.IsOccupiedWorld(2.5d, 1.0d); - -// 查询该位置到最近障碍物的保守净距,单位 m;越界返回 0 m。 -double clearanceMeters = - planningMap.GetConservativeObstacleDistanceMeters(2.5d, 1.0d); -``` - -粗路径规划器应只使用 `PlanningGridMap` 的占据与距离查询,不应直接修改或重建其中的栅格。 - -### 完整实例 - -```csharp -private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); - -private PlanningGridMap CreateMapForCoarsePlanning() -{ - IMapObstacle[] obstacles = - { - new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), - new CircleObstacle(3600f, 1200f, 180f), - }; - - var manualSource = new ManualObstacleSource("manual", 1L, true, obstacles); - - var request = new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), - ResolutionMm = 50f, - ObstacleSources = new IMapObstacleSource[] { manualSource }, - AllowExplicitEmptyMap = false, - }; - - PlanningMapBuildResult result = _mapFactory.Create(request); - if (!result.Succeeded) - throw new InvalidOperationException("建图失败:" + result.FailureReason); - if (result.Map == null || !result.Map.PlanningReady) - throw new InvalidOperationException("地图不可规划:" + result.Map?.PlanningBlockReason); - - return result.Map; -} -``` - -### 常见错误 - -| 情况 | 原因 | 处理方式 | -| --- | --- | --- | -| 改了障碍物但地图仍复用旧快照 | 未递增 `SourceVersion` | 障碍内容每次变化后增加该来源版本号 | -| 规划查询位置总是越界 | 将 mm 坐标传给了 `PlanningGridMap` | 规划查询前将 mm 除以 1000 转为 m | -| 地图创建成功但不能规划 | 未明确允许空图且没有有效障碍物 | 补充有效来源,或确认安全后设置 `AllowExplicitEmptyMap = true` | -| TwoLeg 出现在错误位置 | 未使用检测时刻位姿,或混用了 mm 与 m | 用检测时刻的世界位姿和局部 mm 坐标创建快照 | -| 缓存总是未命中 | 每次都新建 `PlanningMapFactory`,或版本号无意义变化 | 长期复用工厂;仅在来源内容实际变化时递增版本 | diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs deleted file mode 100644 index ca0312f..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// 提供一次性障碍物快照的纯数据来源。 -/// -/// 注意: 不得读取传感器、定位、UI 或时钟;调用前应由上层采集并封装完整快照。 -/// -public interface IMapObstacleSource -{ - /// 来源的稳定唯一标识。不能为空;同一次建图请求中不得重复。 - string SourceId { get; } - /// 来源快照版本号,必须非负。快照几何或有效性变化时必须递增,以使输入缓存失效。 - long SourceVersion { get; } - /// 该来源是否必需。true 时不可用或无效会使整张地图构建失败;false 时只记录状态。 - bool IsRequired { get; } - /// - /// 将已采集的快照投影为世界障碍物几何。 - /// - /// 返回:世界坐标、单位 mm 的障碍物及其状态;不得访问任何实时外部状态。 - /// - ObstacleProjectionResult ProjectToWorld(); -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs deleted file mode 100644 index e4b9334..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 直接提供世界 mm 几何列表的手工障碍物快照来源。 -public sealed class ManualObstacleSource : IMapObstacleSource -{ - private readonly IReadOnlyList _obstacles; - /// - /// 创建手工障碍物来源。 - /// - /// 参数:sourceId 为唯一来源标识;sourceVersion 为非负快照版本;isRequired 指示失败是否阻断建图;obstacles 为世界 mm 几何列表。 - /// 注意:几何内容变化时,调用方必须同步提高 sourceVersion。 - /// - public ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles) - { - SourceId = sourceId; SourceVersion = sourceVersion; IsRequired = isRequired; _obstacles = obstacles; - } - /// 该快照的唯一来源标识。 - public string SourceId { get; } - /// 该快照的版本号;内容变化时必须递增。 - public long SourceVersion { get; } - /// true 表示无效来源会阻断建图;false 表示只记录来源状态。 - public bool IsRequired { get; } - /// - /// 返回手工障碍物的世界几何。 - /// - /// 返回:列表为空时为 Empty;列表为空引用或含无效几何时为 Invalid;其余情况为 Applied。 - /// - public ObstacleProjectionResult ProjectToWorld() - { - if (_obstacles == null) return ObstacleProjectionResult.Invalid("Manual obstacle collection is null."); - if (_obstacles.Count == 0) return ObstacleProjectionResult.Empty("Manual obstacle collection is empty."); - for (int i = 0; i < _obstacles.Count; i++) - if (_obstacles[i] == null || !_obstacles[i].IsValid) return ObstacleProjectionResult.Invalid("Manual obstacle geometry is invalid."); - return ObstacleProjectionResult.Applied(_obstacles, "Manual obstacle snapshot applied."); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs deleted file mode 100644 index b429f26..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 障碍物来源的一次投影结果,包含状态、世界 mm 几何和可选诊断信息。 -public sealed class ObstacleProjectionResult -{ - private ObstacleProjectionResult(ObstacleSourceStatus status, IReadOnlyList obstacles, string diagnostic) - { - Status = status; Obstacles = obstacles ?? Array.Empty(); Diagnostic = diagnostic ?? string.Empty; - } - /// 投影状态。只有 的几何会被栅格化。 - public ObstacleSourceStatus Status { get; } - /// 世界 mm 坐标的障碍物列表。非 Applied 状态时为空列表,不为 null。 - public IReadOnlyList Obstacles { get; } - /// 用于日志和诊断的说明文本;未提供时为空字符串。 - public string Diagnostic { get; } - /// 创建成功投影结果。参数 obstacles 为有效世界 mm 几何;diagnostic 为可选诊断文本。返回的状态为 Applied。 - public static ObstacleProjectionResult Applied(IReadOnlyList obstacles, string diagnostic = null) - { - return new ObstacleProjectionResult(ObstacleSourceStatus.Applied, obstacles, diagnostic); - } - /// 创建空投影结果。参数 diagnostic 为可选原因;返回状态为 Empty,障碍物列表为空。 - public static ObstacleProjectionResult Empty(string diagnostic = null) { return new ObstacleProjectionResult(ObstacleSourceStatus.Empty, null, diagnostic); } - /// 创建不可用投影结果。参数 diagnostic 应说明快照缺失原因;返回状态为 Unavailable。 - public static ObstacleProjectionResult Unavailable(string diagnostic) { return new ObstacleProjectionResult(ObstacleSourceStatus.Unavailable, null, diagnostic); } - /// 创建无效投影结果。参数 diagnostic 应说明数据或几何错误;返回状态为 Invalid。 - public static ObstacleProjectionResult Invalid(string diagnostic) { return new ObstacleProjectionResult(ObstacleSourceStatus.Invalid, null, diagnostic); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs deleted file mode 100644 index f71161a..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 一个障碍物来源投影后的状态,供建图结果和调试日志判断。 -public enum ObstacleSourceStatus -{ - /// 成功得到一个或多个有效的世界 mm 障碍物,并会被栅格化。 - Applied, - /// 来源有效但本次没有障碍物;不会写入任何栅格。 - Empty, - /// 来源快照不可取得;若来源必需则整图构建失败。 - Unavailable, - /// 来源数据或几何无效;若来源必需则整图构建失败。 - Invalid, -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs deleted file mode 100644 index 50afab3..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 将检测局部坐标中的两条腿转换为世界 mm 坐标圆形障碍物。 -public static class TwoLegObstacleProjector -{ - /// - /// 投影一份 TwoLeg 快照。 - /// - /// 参数:input 为检测时刻采集的纯数据快照,可为 null。 - /// 返回:null 为 Unavailable;未检测到目标为 Empty;数值无效为 Invalid;成功时为含两个圆形障碍物的 Applied。 - /// 注意:本方法只读取 input,不访问传感器、定位、UI 或时钟。 - /// - public static ObstacleProjectionResult Project(TwoLegProjectionInput input) - { - if (input == null) return ObstacleProjectionResult.Unavailable("TwoLeg snapshot is unavailable."); - if (!input.HasDetection) return ObstacleProjectionResult.Empty(input.Diagnostic); - if (!input.IsValid) return ObstacleProjectionResult.Invalid("TwoLeg snapshot contains invalid values."); - CoordinateTransform.LocalToWorld(input.DetectionWorldX, input.DetectionWorldY, input.DetectionHeadingRadians, - input.FirstLocalX, input.FirstLocalY, out double firstX, out double firstY); - CoordinateTransform.LocalToWorld(input.DetectionWorldX, input.DetectionWorldY, input.DetectionHeadingRadians, - input.SecondLocalX, input.SecondLocalY, out double secondX, out double secondY); - var obstacles = new List - { - new CircleObstacle((float)firstX, (float)firstY, input.RadiusMm), - new CircleObstacle((float)secondX, (float)secondY, input.RadiusMm), - }; - return ObstacleProjectionResult.Applied(obstacles, input.Diagnostic); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs deleted file mode 100644 index 206fd4b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 将一个 快照包装为统一障碍物来源。 -public sealed class TwoLegObstacleSource : IMapObstacleSource -{ - private readonly TwoLegProjectionInput _input; - /// - /// 创建 TwoLeg 障碍物来源。 - /// - /// 参数:sourceId 为唯一标识;sourceVersion 为快照版本,输入内容改变时必须递增;isRequired 指示失败是否阻断建图;input 为检测时刻快照,可为空。 - /// - public TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input) - { - SourceId = sourceId; SourceVersion = sourceVersion; IsRequired = isRequired; _input = input; - } - /// 该快照来源的唯一标识。 - public string SourceId { get; } - /// 快照版本号;输入内容变化时必须递增。 - public long SourceVersion { get; } - /// true 时投影不可用或无效会阻断建图。 - public bool IsRequired { get; } - /// 投影保存的 TwoLeg 快照。返回世界 mm 圆形障碍物,且不访问实时检测、定位、UI 或时钟。 - public ObstacleProjectionResult ProjectToWorld() { return TwoLegObstacleProjector.Project(_input); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs deleted file mode 100644 index bb1ed94..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs +++ /dev/null @@ -1,51 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// -/// TwoLeg 检测时刻的纯输入快照。 -/// -/// 单位:位置和两腿局部坐标均为 mm,航向为弧度。该对象不读取检测器或定位模块,确保投影可重复。 -/// -public sealed class TwoLegProjectionInput -{ - /// - /// 创建 TwoLeg 投影快照。 - /// - /// 参数:hasDetection 表示当前是否检测到目标;detectionWorldX/Y 为检测坐标系原点的世界 mm 坐标;detectionHeadingRadians 为其相对世界坐标系的航向;firstLocalX/Y、secondLocalX/Y 为两腿在检测局部坐标系中的 mm 坐标;radiusMm 为每条腿投影圆半径;diagnostic 为可选诊断文本。 - /// 注意:hasDetection 为 false 时其余几何参数不会参与有效性校验。 - /// - public TwoLegProjectionInput(bool hasDetection, float detectionWorldX, float detectionWorldY, - double detectionHeadingRadians, float firstLocalX, float firstLocalY, - float secondLocalX, float secondLocalY, float radiusMm, string diagnostic = null) - { - HasDetection = hasDetection; DetectionWorldX = detectionWorldX; DetectionWorldY = detectionWorldY; - DetectionHeadingRadians = detectionHeadingRadians; FirstLocalX = firstLocalX; FirstLocalY = firstLocalY; - SecondLocalX = secondLocalX; SecondLocalY = secondLocalY; RadiusMm = radiusMm; Diagnostic = diagnostic ?? string.Empty; - } - /// 检测是否存在。false 时投影结果为 Empty。 - public bool HasDetection { get; } - /// 检测局部坐标系原点的世界 X 坐标,单位 mm。 - public float DetectionWorldX { get; } - /// 检测局部坐标系原点的世界 Y 坐标,单位 mm。 - public float DetectionWorldY { get; } - /// 检测局部坐标系相对世界坐标系的航向,单位弧度。 - public double DetectionHeadingRadians { get; } - /// 第一条腿在检测局部坐标系的 X 坐标,单位 mm。 - public float FirstLocalX { get; } - /// 第一条腿在检测局部坐标系的 Y 坐标,单位 mm。 - public float FirstLocalY { get; } - /// 第二条腿在检测局部坐标系的 X 坐标,单位 mm。 - public float SecondLocalX { get; } - /// 第二条腿在检测局部坐标系的 Y 坐标,单位 mm。 - public float SecondLocalY { get; } - /// 投影为圆形障碍物时的半径,单位 mm。 - public float RadiusMm { get; } - /// 上游检测阶段提供的可选诊断文本;未提供时为空字符串。 - public string Diagnostic { get; } - /// 未检测到目标时为 true;检测到目标时,所有数值有限且半径非负才为 true。 - public bool IsValid - { - get { return !HasDetection || (NumericGuard.IsFinite(DetectionWorldX) && NumericGuard.IsFinite(DetectionWorldY) && NumericGuard.IsFinite(DetectionHeadingRadians) && NumericGuard.IsFinite(FirstLocalX) && NumericGuard.IsFinite(FirstLocalY) && NumericGuard.IsFinite(SecondLocalX) && NumericGuard.IsFinite(SecondLocalY) && NumericGuard.IsFinite(RadiusMm) && RadiusMm >= 0f); } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs deleted file mode 100644 index 5a8eb2e..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Numerics; -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using CommonUsage.Mathematics; -using FundamentalLib; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC; - -/// -/// 用于手工调试的 Clumsy 地图测试入口,只通过公开的 创建快照。 -/// -/// 注意:终端日志和 PNG 开关只影响调试输出,不参与建图输入、地图内容或缓存键。 -/// -[MovementTest(name = "规划地图快照测试V1")] -public sealed class PlanningMapTest : MovementTest -{ - private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); - private const bool EnableTerminalDebugLog = true; - private const bool SavePng = true; - private const float MapXMinMm = 0f; - private const float MapXMaxMm = 6000f; - private const float MapYMinMm = 0f; - private const float MapYMaxMm = 4000f; - private const float GridResolutionMm = 50f; - private const bool AllowExplicitEmptyMap = false; - private const bool EnableTwoLegSnapshot = false; - - /// - /// 执行一次配置好的地图创建与调试输出。 - /// - /// 返回:无。测试使用本文件顶部的地图范围、分辨率、障碍物来源、日志和 PNG 开关;不会接入实时 TwoLeg 检测。 - /// - public override void Test() - { - IMapObstacleSource[] sources = CreateObstacleSources(); - Log("========== 规划地图创建开始 =========="); - Log("参数:地图范围=(" + MapXMinMm + "," + MapXMaxMm + ")x(" + MapYMinMm + "," + MapYMaxMm - + ")mm,分辨率=" + GridResolutionMm + "mm,允许空图=" + AllowExplicitEmptyMap + ",导出图片=" + SavePng); - Log("障碍物来源:" + FormatSources(sources)); - var result = _mapFactory.Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(MapXMinMm, MapXMaxMm, MapYMinMm, MapYMaxMm), - ResolutionMm = GridResolutionMm, - ObstacleSources = sources, - AllowExplicitEmptyMap = AllowExplicitEmptyMap, - }); - Log("投影结果:" + FormatProjectionResults(sources, result.SourceResults)); - if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) - { - Log("规划地图创建失败:" + result.FailureReason); - Log("========== 规划地图创建结束 =========="); - return; - } - Log("地图快照:编号=" + result.Map.SnapshotId + ",缓存=" + FormatCacheHit(result.CacheHit) + ",栅格=" + result.Map.Cols + "x" + result.Map.Rows - + ",可规划=" + result.Map.PlanningReady + ",指纹=" + result.Map.InputFingerprint); - PlanningMapImageExportResult image = PlanningMapImageExporter.ExportIfEnabled(SavePng, - new PlanningMapImageExportRequest { Map = result.Map, OutputRootDirectory = Environment.CurrentDirectory }); - Log("图片导出:" + FormatImageResult(image)); - Log("========== 规划地图创建完成 =========="); - } - - private static IMapObstacleSource[] CreateObstacleSources() - { - IMapObstacle[] manualObstacles = - { - new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), - new CircleObstacle(3600f, 1200f, 180f), - }; - return new IMapObstacleSource[] - { - new ManualObstacleSource("manual", 1L, true, manualObstacles), - new TwoLegObstacleSource("two-leg", EnableTwoLegSnapshot ? 1L : 0L, false, - new TwoLegProjectionInput(EnableTwoLegSnapshot, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, "No current TwoLeg snapshot.")), - }; - } - - private static string FormatSources(IReadOnlyList sources) - { - return string.Join(";", sources.Select(source => source.SourceId + "(版本=" + source.SourceVersion + ",必需=" + source.IsRequired + ")")); - } - - private static string FormatProjectionResults(IReadOnlyList sources, IReadOnlyList results) - { - IMapObstacleSource[] sortedSources = sources.OrderBy(source => source.SourceId, StringComparer.Ordinal).ToArray(); - return string.Join(";", results.Select((result, index) => sortedSources[index].SourceId + "=" + FormatSourceStatus(result.Status) + "(" + result.Obstacles.Count + " 个障碍物)")); - } - - private static string FormatSourceStatus(ObstacleSourceStatus status) - { - switch (status) - { - case ObstacleSourceStatus.Applied: return "已应用"; - case ObstacleSourceStatus.Empty: return "空"; - case ObstacleSourceStatus.Unavailable: return "不可用"; - case ObstacleSourceStatus.Invalid: return "无效"; - default: return "未知"; - } - } - - private static string FormatCacheHit(PlanningMapCacheHit cacheHit) - { - switch (cacheHit) - { - case PlanningMapCacheHit.Input: return "输入命中"; - case PlanningMapCacheHit.Occupancy: return "占据图命中"; - default: return "未命中"; - } - } - - private static string FormatImageResult(PlanningMapImageExportResult image) - { - if (image.Skipped) return "未启用"; - if (image.Saved) return "已保存:" + image.FilePath; - return "失败:" + image.Message; - } - - private static void Log(string message) - { - if (EnableTerminalDebugLog) Console.WriteLine("[PlanningMapTest] " + message); - } - - /// 停止测试。当前测试不启动持续任务,因此无需额外清理资源。 - public override void TestStop() { } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs deleted file mode 100644 index 3abe48b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 可选 PNG 调试图导出请求,只读取不可变的规划地图快照。 -public sealed class PlanningMapImageExportRequest -{ - /// 待渲染的规划地图快照。不能为空;PNG 导出不会改动该地图或其缓存。 - public PlanningGridMap Map { get; set; } - /// PNG 输出根目录。导出器会在其中创建 PlanningMapExports 子目录;不能为空。 - public string OutputRootDirectory { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs deleted file mode 100644 index 7d09fce..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 一次可选 PNG 导出的处理结果,包含状态、输出路径、像素尺寸和诊断信息。 -public sealed class PlanningMapImageExportResult -{ - /// 是否已经成功写入并完成 PNG 文件。 - public bool Saved { get; set; } - /// 是否因导出开关关闭而跳过。true 时不创建文件,也不影响地图构建或缓存。 - public bool Skipped { get; set; } - /// 成功保存时的 PNG 完整路径;未保存时通常为 null。 - public string FilePath { get; set; } - /// 保存、跳过或失败的诊断信息。 - public string Message { get; set; } - /// 成功 PNG 的文件字节数;未保存时为零。 - public long FileSizeBytes { get; set; } - /// 输出图像宽度,单位为像素。 - public int PixelWidth { get; set; } - /// 输出图像高度,单位为像素。 - public int PixelHeight { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs deleted file mode 100644 index be32cd4..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.IO; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 可选 PNG 导出器;它永远不参与地图构建、障碍物投影或缓存指纹计算。 -public static class PlanningMapImageExporter -{ - /// 每个规划栅格在输出图中占用的边长,单位为像素。 - public const int PixelsPerCell = 4; - /// 输出 PNG 单边允许的最大像素数,超过时拒绝导出。 - public const int MaximumImageEdgePixels = 4000; - /// 输出 PNG 允许的最大文件大小,单位为字节(50 MiB)。 - public const long MaximumFileSizeBytes = 50L * 1024L * 1024L; - /// 写入 PNG 的物理分辨率元数据,单位 DPI。 - public const float OutputDpi = 300f; - private const int MaximumFilenameAttempts = 1024; - - /// - /// 在开关开启时导出规划地图 PNG。 - /// - /// 参数:enabled 为导出开关;request 包含不可变地图和输出根目录。 - /// 返回:开关关闭时返回 Skipped;成功时返回 Saved、路径、字节数与像素尺寸;输入或尺寸不合法时返回失败信息。 - /// 注意:该方法仅用于调试,绝不会影响地图创建和缓存结果。 - /// - public static PlanningMapImageExportResult ExportIfEnabled(bool enabled, PlanningMapImageExportRequest request) - { - if (!enabled) return new PlanningMapImageExportResult { Skipped = true, Message = "Planning map image export is disabled." }; - if (request == null || request.Map == null) return Rejected("Planning map image export requires a PlanningGridMap."); - if (string.IsNullOrWhiteSpace(request.OutputRootDirectory)) return Rejected("Planning map image export requires an output root directory."); - long width = (long)request.Map.Cols * PixelsPerCell, height = (long)request.Map.Rows * PixelsPerCell; - if (width <= 0 || height <= 0 || width > MaximumImageEdgePixels || height > MaximumImageEdgePixels) return Rejected("Planning map image dimensions exceed the permitted edge.", width, height); - string temporary = null; - try - { - string directory = Path.Combine(request.OutputRootDirectory, "PlanningMapExports"); - Directory.CreateDirectory(directory); - using (FileStream stream = CreateTemporaryFile(directory, out string finalPath, out temporary)) - { - byte[] rgba = PlanningMapImageRenderer.Render(request.Map, PixelsPerCell, out int pixelWidth, out int pixelHeight); - ValidatedPngWriter.Write(rgba, pixelWidth, pixelHeight, stream); - } - long length = new FileInfo(temporary).Length; - if (length > MaximumFileSizeBytes) return Rejected("Planning map image PNG exceeds 50 MiB.", width, height); - string completed = temporary.Substring(0, temporary.Length - 4); - File.Move(temporary, completed); temporary = null; - return new PlanningMapImageExportResult { Saved = true, FilePath = completed, FileSizeBytes = length, PixelWidth = (int)width, PixelHeight = (int)height, Message = "Planning map image export saved." }; - } - catch (Exception exception) { return Rejected("Planning map image export failed: " + exception.Message, width, height); } - finally { if (temporary != null && File.Exists(temporary)) File.Delete(temporary); } - } - - private static PlanningMapImageExportResult Rejected(string message, long width = 0, long height = 0) { return new PlanningMapImageExportResult { Message = message, PixelWidth = width > int.MaxValue ? int.MaxValue : (int)width, PixelHeight = height > int.MaxValue ? int.MaxValue : (int)height }; } - private static FileStream CreateTemporaryFile(string directory, out string finalPath, out string temporaryPath) - { - string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); - for (int index = 0; index < MaximumFilenameAttempts; index++) - { - string suffix = index == 0 ? string.Empty : "_" + index; - finalPath = Path.Combine(directory, "PlanningMap_" + timestamp + suffix + ".png"); temporaryPath = finalPath + ".tmp"; - if (File.Exists(finalPath)) continue; - try { return new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); } - catch (IOException) { } - } - finalPath = null; temporaryPath = null; throw new IOException("Could not reserve a unique PlanningMap PNG filename."); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs deleted file mode 100644 index fbdbdbf..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 从只读规划地图生成简单的 RGBA 占据图字节数组。 -internal static class PlanningMapImageRenderer -{ - /// - /// 渲染规划地图为行主序 RGBA 像素。 - /// - /// 参数:map 为只读规划快照;pixelsPerCell 为每个栅格的像素边长;width、height 返回图像像素尺寸。 - /// 返回:长度为 width × height × 4 的 RGBA 字节数组;不会修改地图。 - /// - public static byte[] Render(PlanningGridMap map, int pixelsPerCell, out int width, out int height) - { - if (map == null) throw new ArgumentNullException(nameof(map)); - width = checked(map.Cols * pixelsPerCell); - height = checked(map.Rows * pixelsPerCell); - var rgba = new byte[checked(width * height * 4)]; - for (int row = 0; row < map.Rows; row++) - for (int col = 0; col < map.Cols; col++) - { - bool occupied = map.IsOccupied(row, col); - byte red = occupied ? (byte)220 : (byte)245; - byte green = occupied ? (byte)45 : (byte)245; - byte blue = occupied ? (byte)45 : (byte)245; - int displayRow = map.Rows - 1 - row; - for (int py = 0; py < pixelsPerCell; py++) - for (int px = 0; px < pixelsPerCell; px++) - { - int index = ((displayRow * pixelsPerCell + py) * width + col * pixelsPerCell + px) * 4; - rgba[index] = red; rgba[index + 1] = green; rgba[index + 2] = blue; rgba[index + 3] = 255; - } - } - return rgba; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs deleted file mode 100644 index d2a6371..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.IO; -using StbImageWriteSharp; - -namespace MultiWheelC.TrajectoryPlanning.Mapping; - -/// 编码 RGBA 像素并校验 PNG 文件头、必要块和全部 CRC 的内部写入器。 -internal static class ValidatedPngWriter -{ - private static readonly byte[] Signature = { 137, 80, 78, 71, 13, 10, 26, 10 }; - private static readonly byte[] PhysType = { 112, 72, 89, 115 }; - - /// - /// 将 RGBA 像素写入经过校验的 PNG 流。 - /// - /// 参数:rgba 为行主序、每像素四字节的红绿蓝透明度数组;width、height 为像素尺寸;output 为可写目标流。 - /// 返回:无。输入长度不等于 width × height × 4 或 PNG 校验失败时抛出异常。 - /// - public static void Write(byte[] rgba, int width, int height, Stream output) - { - Write(rgba, width, height, output, 11811u); - } - - /// 将 RGBA 像素写入带指定物理分辨率元数据的经过校验的 PNG 流。 - internal static void Write(byte[] rgba, int width, int height, Stream output, uint pixelsPerMeter) - { - if (rgba == null || output == null || width <= 0 || height <= 0 || pixelsPerMeter == 0u || rgba.Length != checked(width * height * 4)) - throw new ArgumentException("Invalid RGBA PNG input."); - byte[] encoded; - using (var memory = new MemoryStream()) - { - new ImageWriter().WritePng(rgba, width, height, ColorComponents.RedGreenBlueAlpha, memory); - encoded = memory.ToArray(); - } - Validate(encoded); - const int ihdrEndOffset = 8 + 4 + 4 + 13 + 4; - using (var outputMemory = new MemoryStream()) - { - outputMemory.Write(encoded, 0, ihdrEndOffset); - var phys = new byte[9]; - WriteUInt32BigEndian(phys, 0, pixelsPerMeter); - WriteUInt32BigEndian(phys, 4, pixelsPerMeter); - phys[8] = 1; - WriteChunk(outputMemory, PhysType, phys); - outputMemory.Write(encoded, ihdrEndOffset, encoded.Length - ihdrEndOffset); - encoded = outputMemory.ToArray(); - } - Validate(encoded); - output.Write(encoded, 0, encoded.Length); - } - - private static void Validate(byte[] png) - { - if (png == null || png.Length < 45) throw new InvalidDataException("PNG is truncated."); - for (int i = 0; i < Signature.Length; i++) if (png[i] != Signature[i]) throw new InvalidDataException("PNG signature is invalid."); - int offset = Signature.Length, ihdr = 0, iend = 0; - while (offset < png.Length) - { - if (png.Length - offset < 12) throw new InvalidDataException("PNG chunk header is truncated."); - uint length = ReadUInt32BigEndian(png, offset); - long crcOffset = (long)offset + 8L + length; - if (crcOffset + 4L > png.Length) throw new InvalidDataException("PNG chunk is truncated."); - int typeOffset = offset + 4; - uint expected = ReadUInt32BigEndian(png, (int)crcOffset); - uint actual = ComputeCrc32(png, typeOffset, checked((int)length + 4)); - if (expected != actual) throw new InvalidDataException("PNG chunk CRC is invalid."); - bool isIhdr = IsType(png, typeOffset, 73, 72, 68, 82); - bool isIend = IsType(png, typeOffset, 73, 69, 78, 68); - if (offset == Signature.Length && !isIhdr) throw new InvalidDataException("PNG must start with IHDR."); - if (isIhdr) ihdr++; - if (isIend) { iend++; if (crcOffset + 4L != png.Length) throw new InvalidDataException("PNG data follows IEND."); } - offset = checked((int)crcOffset + 4); - } - if (ihdr != 1 || iend != 1) throw new InvalidDataException("PNG must contain exactly one IHDR and IEND."); - } - private static bool IsType(byte[] bytes, int offset, byte a, byte b, byte c, byte d) { return bytes[offset] == a && bytes[offset + 1] == b && bytes[offset + 2] == c && bytes[offset + 3] == d; } - private static uint ReadUInt32BigEndian(byte[] bytes, int offset) { return ((uint)bytes[offset] << 24) | ((uint)bytes[offset + 1] << 16) | ((uint)bytes[offset + 2] << 8) | bytes[offset + 3]; } - private static void WriteUInt32BigEndian(byte[] bytes, int offset, uint value) { bytes[offset] = (byte)(value >> 24); bytes[offset + 1] = (byte)(value >> 16); bytes[offset + 2] = (byte)(value >> 8); bytes[offset + 3] = (byte)value; } - private static void WriteChunk(Stream output, byte[] type, byte[] data) - { - var length = new byte[4]; WriteUInt32BigEndian(length, 0, (uint)data.Length); output.Write(length, 0, 4); output.Write(type, 0, 4); output.Write(data, 0, data.Length); - var crcBytes = new byte[4]; WriteUInt32BigEndian(crcBytes, 0, ComputeCrc32(type, 0, type.Length, data)); output.Write(crcBytes, 0, 4); - } - private static uint ComputeCrc32(byte[] data, int offset, int count) { return ComputeCrc32(data, offset, count, null); } - private static uint ComputeCrc32(byte[] first, int offset, int count, byte[] second) - { - uint crc = 0xffffffffu; - for (int i = 0; i < count; i++) crc = UpdateCrc(crc, first[offset + i]); - if (second != null) for (int i = 0; i < second.Length; i++) crc = UpdateCrc(crc, second[i]); - return crc ^ 0xffffffffu; - } - private static uint UpdateCrc(uint crc, byte value) - { - crc ^= value; - for (int bit = 0; bit < 8; bit++) crc = (crc & 1u) == 0u ? crc >> 1 : 0xedb88320u ^ (crc >> 1); - return crc; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs deleted file mode 100644 index 275fdc3..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs +++ /dev/null @@ -1,412 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 按方向段独立生成夹持三次 B 样条原始几何候选。 -internal sealed class CubicBSplineSmoother : IPathSmoother -{ - private const int Degree = 3; - private const int SamplesPerSpan = 64; - private const double StraightToleranceMeters = 1e-9d; - private const double EndpointProbeParameter = 1e-6d; - private const double MinimumTangentHandleLengthMeters = 1e-10d; - - /// - public SmoothingMethod Method => SmoothingMethod.CubicBSpline; - - /// - public SmoothingCandidate Smooth( - SmoothingAlgorithmInput input, - double effectiveStrength, - CancellationToken cancellationToken) - { - if (input == null || input.OriginalPath == null || - input.Options == null || - !NumericGuard.IsPositiveFinite(effectiveStrength) || - !NumericGuard.IsFinite(input.MinimumClearanceReserveMeters) || - input.MinimumClearanceReserveMeters < 0d) - { - return SmoothingCandidate.Failed("B 样条输入、强度或净空预留无效。"); - } - - var candidateSegments = new List(input.OriginalPath.Segments.Count); - for (int segmentIndex = 0; segmentIndex < input.OriginalPath.Segments.Count; segmentIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - PreparedDirectionSegment sourceSegment = input.OriginalPath.Segments[segmentIndex]; - if (!TrySmoothSegment( - sourceSegment, - effectiveStrength, - input.MinimumClearanceReserveMeters, - input.Options.CubicBSplineEndpointTangentScale, - cancellationToken, - out IReadOnlyList points, - out string reason, - out SmoothingCandidateStatus status)) - { - return status == SmoothingCandidateStatus.RetryableInfeasible - ? SmoothingCandidate.RetryableInfeasible(reason) - : SmoothingCandidate.Failed(reason); - } - - candidateSegments.Add(new PreparedDirectionSegment( - sourceSegment.SegmentIndex, - sourceSegment.Direction, - points, - sourceSegment.StartsAtGearSwitch, - sourceSegment.EndsAtGearSwitch, - sourceSegment.StartVehicleCurvaturePerMeter)); - } - - return SmoothingCandidate.Success(candidateSegments); - } - - private static bool TrySmoothSegment( - PreparedDirectionSegment sourceSegment, - double strength, - double reserveMeters, - double endpointTangentScale, - CancellationToken cancellationToken, - out IReadOnlyList result, - out string reason, - out SmoothingCandidateStatus status) - { - result = null; - reason = string.Empty; - status = SmoothingCandidateStatus.Failed; - if (sourceSegment == null || sourceSegment.Points == null || sourceSegment.Points.Count == 0) - { - reason = "B 样条方向段为空。"; - return false; - } - - IReadOnlyList anchors = sourceSegment.Points; - for (int index = 0; index < anchors.Count; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - if (!IsValidAnchor(anchors[index])) - { - reason = "B 样条方向段包含非法锚点。"; - return false; - } - } - - if (anchors.Count <= Degree || IsStraight(anchors)) - { - result = anchors; - return true; - } - - if (!TryCreateControls(anchors, sourceSegment.Direction, strength, reserveMeters, endpointTangentScale, - cancellationToken, out Point2D[] controls, out reason)) - { - return false; - } - - double[] knots = CreateClampedKnots(controls.Length); - var sampled = new List(); - int spanCount = controls.Length - Degree; - int uniformIntervals = spanCount * SamplesPerSpan; - if (!TryAddSample(0d, anchors, controls, knots, reserveMeters, sampled, cancellationToken, out reason, out status)) - return false; - if (!TryAddSample(EndpointProbeParameter, anchors, controls, knots, reserveMeters, - sampled, cancellationToken, out reason, out status)) - return false; - for (int index = 1; index < uniformIntervals; index++) - { - if (!TryAddSample((double)index / uniformIntervals, anchors, controls, knots, reserveMeters, - sampled, cancellationToken, out reason, out status)) - { - return false; - } - } - if (!TryAddSample(1d - EndpointProbeParameter, anchors, controls, knots, reserveMeters, - sampled, cancellationToken, out reason, out status)) - return false; - if (!TryAddSample(1d, anchors, controls, knots, reserveMeters, sampled, cancellationToken, out reason, out status)) - return false; - - result = sampled; - return true; - } - - private static bool TryCreateControls( - IReadOnlyList anchors, - TravelDirection direction, - double strength, - double reserveMeters, - double endpointTangentScale, - CancellationToken cancellationToken, - out Point2D[] controls, - out string reason) - { - reason = string.Empty; - controls = new Point2D[anchors.Count]; - controls[0] = Point2D.FromAnchor(anchors[0]); - controls[controls.Length - 1] = Point2D.FromAnchor(anchors[anchors.Count - 1]); - - double startHandleLength = Distance(anchors[0], anchors[1]) * endpointTangentScale * strength; - double startTravelHeading = GetTravelHeading(anchors[0], direction); - if (!TryConstrainTangentHandle( - Point2D.FromAnchor(anchors[0]), - Math.Cos(startTravelHeading), - Math.Sin(startTravelHeading), - startHandleLength, - anchors[1], - GetAllowedRadius(anchors[1], reserveMeters), - out controls[1])) - { - reason = "B 样条起点切向手柄无法同时满足相邻锚点移动范围。"; - return false; - } - - int finalIndex = anchors.Count - 1; - double endHandleLength = Distance(anchors[finalIndex - 1], anchors[finalIndex]) * endpointTangentScale * strength; - double endTravelHeading = GetTravelHeading(anchors[finalIndex], direction); - if (!TryConstrainTangentHandle( - Point2D.FromAnchor(anchors[finalIndex]), - -Math.Cos(endTravelHeading), - -Math.Sin(endTravelHeading), - endHandleLength, - anchors[finalIndex - 1], - GetAllowedRadius(anchors[finalIndex - 1], reserveMeters), - out controls[finalIndex - 1])) - { - reason = "B 样条终点切向手柄无法同时满足相邻锚点移动范围。"; - return false; - } - - for (int index = 2; index < finalIndex - 1; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - SmoothingPoint2D previous = anchors[index - 1]; - SmoothingPoint2D current = anchors[index]; - SmoothingPoint2D next = anchors[index + 1]; - Point2D target = new Point2D( - (previous.X + current.X + next.X) / 3d, - (previous.Y + current.Y + next.Y) / 3d); - Point2D proposed = new Point2D( - current.X + strength * (target.X - current.X), - current.Y + strength * (target.Y - current.Y)); - controls[index] = ClampDisplacement(current, proposed, GetAllowedRadius(current, reserveMeters)); - } - - for (int index = 0; index < controls.Length; index++) - { - if (!NumericGuard.IsFinite(controls[index].X) || !NumericGuard.IsFinite(controls[index].Y)) - { - reason = "B 样条控制点构造产生非法数值。"; - return false; - } - } - return true; - } - - private static bool TryAddSample( - double parameter, - IReadOnlyList anchors, - Point2D[] controls, - double[] knots, - double reserveMeters, - List output, - CancellationToken cancellationToken, - out string reason, - out SmoothingCandidateStatus status) - { - reason = string.Empty; - status = SmoothingCandidateStatus.Failed; - cancellationToken.ThrowIfCancellationRequested(); - Point2D evaluated = Evaluate(controls, knots, parameter); - if (!NumericGuard.IsFinite(evaluated.X) || !NumericGuard.IsFinite(evaluated.Y)) - { - reason = "B 样条评估产生非法数值。"; - return false; - } - - double targetArcLength = parameter * anchors[anchors.Count - 1].ArcLength; - if (!PathReferenceInterpolator.TryInterpolateByArcLength(anchors, targetArcLength, - out SmoothingPoint2D reference, out reason)) - { - return false; - } - double displacement = Distance(evaluated, reference); - if (!NumericGuard.IsFinite(displacement)) - { - reason = "B 样条评估点位移产生非法数值。"; - return false; - } - if (displacement > GetAllowedRadius(reference, reserveMeters)) - { - reason = "B 样条评估点超过对应原始参考点的允许移动范围。"; - status = SmoothingCandidateStatus.RetryableInfeasible; - return false; - } - bool endpoint = parameter == 0d || parameter == 1d; - output.Add(new SmoothingPoint2D( - evaluated.X, - evaluated.Y, - reference.ArcLength, - reference.Heading, - reference.UnwrappedHeading, - reference.BodyClearance, - endpoint && reference.IsGearSwitchPoint, - endpoint ? reference.Source : SmoothedPathPointSource.Interpolated)); - return true; - } - - private static bool TryConstrainTangentHandle( - Point2D endpoint, - double rayDirectionX, - double rayDirectionY, - double desiredLength, - SmoothingPoint2D adjacentAnchor, - double allowedRadius, - out Point2D control) - { - control = default; - double offsetX = adjacentAnchor.X - endpoint.X; - double offsetY = adjacentAnchor.Y - endpoint.Y; - double projectedLength = offsetX * rayDirectionX + offsetY * rayDirectionY; - double perpendicularX = offsetX - projectedLength * rayDirectionX; - double perpendicularY = offsetY - projectedLength * rayDirectionY; - double discriminant = allowedRadius * allowedRadius - - (perpendicularX * perpendicularX + perpendicularY * perpendicularY); - if (!NumericGuard.IsFinite(discriminant) || discriminant < 0d) return false; - - double halfInterval = Math.Sqrt(discriminant); - double minimumLength = Math.Max(MinimumTangentHandleLengthMeters, projectedLength - halfInterval); - double maximumLength = projectedLength + halfInterval; - if (!NumericGuard.IsFinite(maximumLength) || maximumLength < minimumLength) return false; - - double constrainedLength = Math.Max(minimumLength, Math.Min(desiredLength, maximumLength)); - control = new Point2D( - endpoint.X + constrainedLength * rayDirectionX, - endpoint.Y + constrainedLength * rayDirectionY); - return NumericGuard.IsFinite(control.X) && NumericGuard.IsFinite(control.Y); - } - - private static Point2D Evaluate(Point2D[] controls, double[] knots, double parameter) - { - if (parameter <= 0d) return controls[0]; - if (parameter >= 1d) return controls[controls.Length - 1]; - - var point = new Point2D(0d, 0d); - for (int index = 0; index < controls.Length; index++) - { - double basis = EvaluateBasis(index, Degree, parameter, knots); - point = new Point2D(point.X + basis * controls[index].X, point.Y + basis * controls[index].Y); - } - return point; - } - - private static double EvaluateBasis(int index, int degree, double parameter, double[] knots) - { - if (degree == 0) - return knots[index] <= parameter && parameter < knots[index + 1] ? 1d : 0d; - - double left = 0d; - double leftDenominator = knots[index + degree] - knots[index]; - if (leftDenominator > 0d) - left = (parameter - knots[index]) / leftDenominator * EvaluateBasis(index, degree - 1, parameter, knots); - - double right = 0d; - double rightDenominator = knots[index + degree + 1] - knots[index + 1]; - if (rightDenominator > 0d) - right = (knots[index + degree + 1] - parameter) / rightDenominator * - EvaluateBasis(index + 1, degree - 1, parameter, knots); - return left + right; - } - - private static double[] CreateClampedKnots(int controlCount) - { - var knots = new double[controlCount + Degree + 1]; - for (int index = Degree + 1; index < controlCount; index++) - knots[index] = (double)(index - Degree) / (controlCount - Degree); - for (int index = controlCount; index < knots.Length; index++) knots[index] = 1d; - return knots; - } - - private static Point2D ClampDisplacement(SmoothingPoint2D anchor, Point2D proposed, double allowedRadius) - { - double deltaX = proposed.X - anchor.X; - double deltaY = proposed.Y - anchor.Y; - double distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - if (!NumericGuard.IsFinite(distance) || distance <= allowedRadius) return proposed; - if (distance == 0d || allowedRadius == 0d) return Point2D.FromAnchor(anchor); - double scale = allowedRadius / distance; - return new Point2D(anchor.X + deltaX * scale, anchor.Y + deltaY * scale); - } - - private static bool IsStraight(IReadOnlyList anchors) - { - if (anchors.Count < 3) return true; - SmoothingPoint2D first = anchors[0]; - SmoothingPoint2D last = anchors[anchors.Count - 1]; - double directionX = last.X - first.X; - double directionY = last.Y - first.Y; - double length = Math.Sqrt(directionX * directionX + directionY * directionY); - if (!NumericGuard.IsFinite(length) || length <= StraightToleranceMeters) return false; - for (int index = 1; index < anchors.Count - 1; index++) - { - double offsetX = anchors[index].X - first.X; - double offsetY = anchors[index].Y - first.Y; - double perpendicularDeviation = Math.Abs(directionX * offsetY - directionY * offsetX) / length; - if (perpendicularDeviation >= StraightToleranceMeters) return false; - } - return true; - } - - private static bool IsValidAnchor(SmoothingPoint2D point) - { - return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && - NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.Heading) && - NumericGuard.IsFinite(point.UnwrappedHeading) && NumericGuard.IsFinite(point.BodyClearance) && - point.BodyClearance >= 0d; - } - - private static double GetAllowedRadius(SmoothingPoint2D anchor, double reserveMeters) - { - return Math.Max(0d, anchor.BodyClearance - reserveMeters); - } - - private static double GetTravelHeading(SmoothingPoint2D point, TravelDirection direction) - { - return direction == TravelDirection.Forward ? point.Heading : point.Heading - Math.PI; - } - - private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) - { - double deltaX = right.X - left.X; - double deltaY = right.Y - left.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - - private static double Distance(Point2D left, SmoothingPoint2D right) - { - double deltaX = right.X - left.X; - double deltaY = right.Y - left.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - - private readonly struct Point2D - { - internal Point2D(double x, double y) - { - X = x; - Y = y; - } - - internal double X { get; } - internal double Y { get; } - - internal static Point2D FromAnchor(SmoothingPoint2D anchor) - { - return new Point2D(anchor.X, anchor.Y); - } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs deleted file mode 100644 index 1a9c251..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Threading; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 单一平滑方法生成原始几何候选的内部契约。 -internal interface IPathSmoother -{ - SmoothingMethod Method { get; } - - SmoothingCandidate Smooth( - SmoothingAlgorithmInput input, - double effectiveStrength, - CancellationToken cancellationToken); -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs deleted file mode 100644 index eee05ec..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs +++ /dev/null @@ -1,427 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 在方向段内以局部三次 Bézier 连接替换明显转角。 -internal sealed class LocalCubicBezierSmoother : IPathSmoother -{ - private const double WindowToleranceMeters = 1e-9d; - - /// - public SmoothingMethod Method => SmoothingMethod.LocalCubicBezier; - - /// - public SmoothingCandidate Smooth( - SmoothingAlgorithmInput input, - double effectiveStrength, - CancellationToken cancellationToken) - { - if (input == null || input.OriginalPath == null || input.Options == null || - !NumericGuard.IsPositiveFinite(effectiveStrength) || - !NumericGuard.IsFinite(input.MinimumClearanceReserveMeters) || - input.MinimumClearanceReserveMeters < 0d) - { - return SmoothingCandidate.Failed("Bézier 输入、强度或净空预留无效。"); - } - - var candidateSegments = new List(input.OriginalPath.Segments.Count); - for (int segmentIndex = 0; segmentIndex < input.OriginalPath.Segments.Count; segmentIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - PreparedDirectionSegment sourceSegment = input.OriginalPath.Segments[segmentIndex]; - if (!TrySmoothSegment( - sourceSegment, - input.Options.BezierCornerHeadingThresholdRadians, - input.Options.BezierMaximumWindowLengthMeters, - input.Options.BezierHandleLengthRatio, - effectiveStrength, - input.MinimumClearanceReserveMeters, - cancellationToken, - out IReadOnlyList points, - out string reason, - out SmoothingCandidateStatus status)) - { - return status == SmoothingCandidateStatus.RetryableInfeasible - ? SmoothingCandidate.RetryableInfeasible(reason) - : SmoothingCandidate.Failed(reason); - } - - candidateSegments.Add(new PreparedDirectionSegment( - sourceSegment.SegmentIndex, - sourceSegment.Direction, - points, - sourceSegment.StartsAtGearSwitch, - sourceSegment.EndsAtGearSwitch, - sourceSegment.StartVehicleCurvaturePerMeter)); - } - - return SmoothingCandidate.Success(candidateSegments); - } - - private static bool TrySmoothSegment( - PreparedDirectionSegment sourceSegment, - double cornerThresholdRadians, - double maximumWindowLengthMeters, - double handleLengthRatio, - double strength, - double reserveMeters, - CancellationToken cancellationToken, - out IReadOnlyList result, - out string reason, - out SmoothingCandidateStatus status) - { - result = null; - reason = string.Empty; - status = SmoothingCandidateStatus.Failed; - if (sourceSegment == null || sourceSegment.Points == null || sourceSegment.Points.Count == 0) - { - reason = "Bézier 方向段为空。"; - return false; - } - - IReadOnlyList anchors = sourceSegment.Points; - if (!ValidateAnchors(anchors, cancellationToken, out reason)) return false; - if (anchors.Count < 3) - { - result = anchors; - return true; - } - - if (!TryCreateMergedWindows( - anchors, - cornerThresholdRadians, - maximumWindowLengthMeters, - cancellationToken, - out List windows, - out reason)) - { - return false; - } - if (windows.Count == 0) - { - result = anchors; - return true; - } - - var output = new List(anchors.Count); - int anchorIndex = 0; - for (int windowIndex = 0; windowIndex < windows.Count; windowIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - Window window = windows[windowIndex]; - while (anchorIndex <= window.StartIndex) - { - output.Add(anchors[anchorIndex]); - anchorIndex++; - } - - if (!TryAppendWindowInterior( - anchors, - window, - handleLengthRatio, - strength, - reserveMeters, - output, - cancellationToken, - out reason, - out status)) - { - return false; - } - - output.Add(anchors[window.EndIndex]); - anchorIndex = window.EndIndex + 1; - } - - while (anchorIndex < anchors.Count) - { - output.Add(anchors[anchorIndex]); - anchorIndex++; - } - - result = output; - return true; - } - - private static bool ValidateAnchors( - IReadOnlyList anchors, - CancellationToken cancellationToken, - out string reason) - { - reason = string.Empty; - for (int index = 0; index < anchors.Count; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - SmoothingPoint2D point = anchors[index]; - if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || - !NumericGuard.IsFinite(point.ArcLength) || point.ArcLength < 0d || - !NumericGuard.IsFinite(point.Heading) || !NumericGuard.IsFinite(point.UnwrappedHeading) || - !NumericGuard.IsFinite(point.BodyClearance) || point.BodyClearance < 0d || - (index > 0 && point.ArcLength <= anchors[index - 1].ArcLength)) - { - reason = "Bézier 方向段包含非有限、非递增弧长或无效净空的锚点。"; - return false; - } - } - return true; - } - - private static bool TryCreateMergedWindows( - IReadOnlyList anchors, - double cornerThresholdRadians, - double maximumWindowLengthMeters, - CancellationToken cancellationToken, - out List windows, - out string reason) - { - windows = new List(); - var candidates = new List(); - reason = string.Empty; - for (int cornerIndex = 1; cornerIndex < anchors.Count - 1; cornerIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - if (!TryGetTravelTangent(anchors[cornerIndex - 1], anchors[cornerIndex], out Point2D entryTangent) || - !TryGetTravelTangent(anchors[cornerIndex], anchors[cornerIndex + 1], out Point2D exitTangent)) - { - reason = "Bézier 转角包含零长度或非有限行进切向。"; - return false; - } - - double cross = entryTangent.X * exitTangent.Y - entryTangent.Y * exitTangent.X; - double dot = entryTangent.X * exitTangent.X + entryTangent.Y * exitTangent.Y; - double turnRadians = Math.Atan2(Math.Abs(cross), dot); - if (!NumericGuard.IsFinite(turnRadians)) - { - reason = "Bézier 转角计算产生非有限数值。"; - return false; - } - if (turnRadians < cornerThresholdRadians) continue; - - int startIndex = cornerIndex - 1; - int endIndex = cornerIndex + 1; - double windowLength = anchors[endIndex].ArcLength - anchors[startIndex].ArcLength; - if (!NumericGuard.IsPositiveFinite(windowLength)) - { - reason = "Bézier 局部窗口弧长无效。"; - return false; - } - if (windowLength > maximumWindowLengthMeters + WindowToleranceMeters) continue; - if (ContainsGearSwitch(anchors, startIndex, endIndex)) continue; - - candidates.Add(new Window(startIndex, endIndex)); - } - - MergeBoundedConnectedWindows(anchors, candidates, maximumWindowLengthMeters, windows); - return true; - } - - private static void MergeBoundedConnectedWindows( - IReadOnlyList anchors, - IReadOnlyList candidates, - double maximumWindowLengthMeters, - List windows) - { - int candidateIndex = 0; - while (candidateIndex < candidates.Count) - { - Window merged = candidates[candidateIndex]; - candidateIndex++; - while (candidateIndex < candidates.Count && - candidates[candidateIndex].StartIndex <= merged.EndIndex + 1) - { - merged = new Window(merged.StartIndex, - Math.Max(merged.EndIndex, candidates[candidateIndex].EndIndex)); - candidateIndex++; - } - - double mergedLength = anchors[merged.EndIndex].ArcLength - anchors[merged.StartIndex].ArcLength; - if (mergedLength <= maximumWindowLengthMeters + WindowToleranceMeters) - { - windows.Add(merged); - } - // A connected group that exceeds the cap is declined as a whole. Splitting it into - // adjacent local curves would introduce unrequested joins; accepting it would violate - // the maximum-window contract. Its original anchors therefore remain unchanged. - } - } - - private static bool TryAppendWindowInterior( - IReadOnlyList anchors, - Window window, - double handleLengthRatio, - double strength, - double reserveMeters, - List output, - CancellationToken cancellationToken, - out string reason, - out SmoothingCandidateStatus status) - { - reason = string.Empty; - status = SmoothingCandidateStatus.Failed; - SmoothingPoint2D p0 = anchors[window.StartIndex]; - SmoothingPoint2D p3 = anchors[window.EndIndex]; - if (!TryGetTravelTangent(p0, anchors[window.StartIndex + 1], out Point2D entryTangent) || - !TryGetTravelTangent(anchors[window.EndIndex - 1], p3, out Point2D exitTangent)) - { - reason = "Bézier 窗口端点包含无效行进切向。"; - return false; - } - - double arcLength = p3.ArcLength - p0.ArcLength; - double chordLength = Distance(p0, p3); - double handleLength = chordLength * handleLengthRatio * strength; - if (!NumericGuard.IsPositiveFinite(arcLength) || !NumericGuard.IsPositiveFinite(chordLength) || - !NumericGuard.IsPositiveFinite(handleLength)) - { - reason = "Bézier 窗口弧长、端点弦长或控制柄长度无效。"; - return false; - } - - Point2D control1 = new Point2D( - p0.X + entryTangent.X * handleLength, - p0.Y + entryTangent.Y * handleLength); - Point2D control2 = new Point2D( - p3.X - exitTangent.X * handleLength, - p3.Y - exitTangent.Y * handleLength); - if (!IsFinite(control1) || !IsFinite(control2)) - { - reason = "Bézier 控制点产生非有限数值。"; - return false; - } - - for (int index = window.StartIndex + 1; index < window.EndIndex; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - SmoothingPoint2D anchor = anchors[index]; - double parameter = (anchor.ArcLength - p0.ArcLength) / arcLength; - if (!NumericGuard.IsFinite(parameter) || parameter <= 0d || parameter >= 1d) - { - reason = "Bézier 窗口参数无效。"; - return false; - } - - Point2D evaluated = Evaluate(p0, control1, control2, p3, parameter); - if (!IsFinite(evaluated)) - { - reason = "Bézier 评估产生非有限数值。"; - return false; - } - - double referenceArcLength = p0.ArcLength + parameter * arcLength; - if (!PathReferenceInterpolator.TryInterpolateByArcLength( - anchors, - referenceArcLength, - out SmoothingPoint2D reference, - out reason)) - { - return false; - } - - double displacement = Distance(evaluated, reference); - if (!NumericGuard.IsFinite(displacement)) - { - reason = "Bézier 评估点位移产生非有限数值。"; - return false; - } - double allowedDisplacement = Math.Max(0d, reference.BodyClearance - reserveMeters); - if (displacement > allowedDisplacement) - { - reason = "Bézier 评估点超过对应原始弧长参考点的允许移动范围。"; - status = SmoothingCandidateStatus.RetryableInfeasible; - return false; - } - - output.Add(new SmoothingPoint2D( - evaluated.X, - evaluated.Y, - reference.ArcLength, - reference.Heading, - reference.UnwrappedHeading, - reference.BodyClearance, - false, - SmoothedPathPointSource.Interpolated)); - } - return true; - } - - private static bool ContainsGearSwitch(IReadOnlyList anchors, int startIndex, int endIndex) - { - for (int index = startIndex; index <= endIndex; index++) - { - if (anchors[index].IsGearSwitchPoint) return true; - } - return false; - } - - private static bool TryGetTravelTangent(SmoothingPoint2D start, SmoothingPoint2D end, out Point2D tangent) - { - tangent = default; - double deltaX = end.X - start.X; - double deltaY = end.Y - start.Y; - double length = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - if (!NumericGuard.IsPositiveFinite(length)) return false; - tangent = new Point2D(deltaX / length, deltaY / length); - return IsFinite(tangent); - } - - private static Point2D Evaluate(SmoothingPoint2D p0, Point2D p1, Point2D p2, SmoothingPoint2D p3, double parameter) - { - double oneMinusParameter = 1d - parameter; - double p0Weight = oneMinusParameter * oneMinusParameter * oneMinusParameter; - double p1Weight = 3d * oneMinusParameter * oneMinusParameter * parameter; - double p2Weight = 3d * oneMinusParameter * parameter * parameter; - double p3Weight = parameter * parameter * parameter; - return new Point2D( - p0Weight * p0.X + p1Weight * p1.X + p2Weight * p2.X + p3Weight * p3.X, - p0Weight * p0.Y + p1Weight * p1.Y + p2Weight * p2.Y + p3Weight * p3.Y); - } - - private static bool IsFinite(Point2D point) - { - return NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y); - } - - private static double Distance(Point2D point, SmoothingPoint2D reference) - { - double deltaX = point.X - reference.X; - double deltaY = point.Y - reference.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - - private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) - { - double deltaX = left.X - right.X; - double deltaY = left.Y - right.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - - private readonly struct Window - { - internal Window(int startIndex, int endIndex) - { - StartIndex = startIndex; - EndIndex = endIndex; - } - - internal int StartIndex { get; } - - internal int EndIndex { get; } - } - - private readonly struct Point2D - { - internal Point2D(double x, double y) - { - X = x; - Y = y; - } - - internal double X { get; } - - internal double Y { get; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs deleted file mode 100644 index f6abfe4..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs +++ /dev/null @@ -1,557 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 按方向段局部弧长构造 C2 连续的分段五次 Hermite 原始几何候选。 -internal sealed class PiecewiseQuinticSmoother : IPathSmoother -{ - private const int SamplesPerInterval = 8; - private const double DoubleMachineEpsilon = 2.2204460492503131e-16d; - private const double EndpointNormalizationUlps = 32d; - - /// - public SmoothingMethod Method => SmoothingMethod.PiecewiseQuintic; - - /// - public SmoothingCandidate Smooth( - SmoothingAlgorithmInput input, - double effectiveStrength, - CancellationToken cancellationToken) - { - if (input == null || input.OriginalPath == null || input.Options == null || - !NumericGuard.IsPositiveFinite(effectiveStrength) || - !NumericGuard.IsFinite(input.MinimumClearanceReserveMeters) || - input.MinimumClearanceReserveMeters < 0d || - !NumericGuard.IsPositiveFinite(input.Options.QuinticKnotSpacingMeters) || - !NumericGuard.IsPositiveFinite(input.Options.QuinticMinimumKnotSpacingMeters) || - input.Options.QuinticKnotSpacingMeters < input.Options.QuinticMinimumKnotSpacingMeters) - { - return SmoothingCandidate.Failed("五次 Hermite 输入、强度、净空预留或结点间距无效。"); - } - - var candidateSegments = new List(input.OriginalPath.Segments.Count); - for (int segmentIndex = 0; segmentIndex < input.OriginalPath.Segments.Count; segmentIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - PreparedDirectionSegment sourceSegment = input.OriginalPath.Segments[segmentIndex]; - if (!TrySmoothSegment( - sourceSegment, - effectiveStrength, - input.MinimumClearanceReserveMeters, - input.Options.QuinticKnotSpacingMeters, - input.Options.QuinticMinimumKnotSpacingMeters, - cancellationToken, - out IReadOnlyList points, - out string reason, - out SmoothingCandidateStatus status)) - { - return status == SmoothingCandidateStatus.RetryableInfeasible - ? SmoothingCandidate.RetryableInfeasible(reason) - : SmoothingCandidate.Failed(reason); - } - - candidateSegments.Add(new PreparedDirectionSegment( - sourceSegment.SegmentIndex, - sourceSegment.Direction, - points, - sourceSegment.StartsAtGearSwitch, - sourceSegment.EndsAtGearSwitch, - sourceSegment.StartVehicleCurvaturePerMeter)); - } - - return SmoothingCandidate.Success(candidateSegments); - } - - private static bool TrySmoothSegment( - PreparedDirectionSegment sourceSegment, - double effectiveStrength, - double reserveMeters, - double knotSpacingMeters, - double minimumKnotSpacingMeters, - CancellationToken cancellationToken, - out IReadOnlyList result, - out string reason, - out SmoothingCandidateStatus status) - { - result = null; - reason = string.Empty; - status = SmoothingCandidateStatus.Failed; - if (sourceSegment == null || sourceSegment.Points == null || sourceSegment.Points.Count < 2) - { - reason = "五次 Hermite 方向段至少需要两个锚点。"; - return false; - } - - IReadOnlyList anchors = sourceSegment.Points; - if (!ValidateAnchors(anchors, cancellationToken, out reason)) return false; - - if (!TryCreateKnots( - anchors, - sourceSegment.Direction, - effectiveStrength, - knotSpacingMeters, - minimumKnotSpacingMeters, - cancellationToken, - out List knots, - out reason)) - { - return false; - } - - // Each physical acceleration is blended once into its shared knot and then scaled by - // the left/right local interval independently. Reusing this value is what makes the - // curve C2 with respect to local arc length, even for nonuniform final intervals. - if (!TryAssignSharedAccelerations(knots, out reason)) return false; - if (!TryCreateIntervals(knots, out List intervals, out reason)) return false; - - var sampled = new List(1 + intervals.Count * SamplesPerInterval); - for (int intervalIndex = 0; intervalIndex < intervals.Count; intervalIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - QuinticInterval interval = intervals[intervalIndex]; - int firstSample = intervalIndex == 0 ? 0 : 1; - for (int sampleIndex = firstSample; sampleIndex <= SamplesPerInterval; sampleIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - double parameter = (double)sampleIndex / SamplesPerInterval; - double referenceArcLength = interval.Start.ArcLength + parameter * interval.Length; - if (!NumericGuard.IsFinite(referenceArcLength)) - { - reason = "五次 Hermite 采样参考弧长无效。"; - return false; - } - - if (!PathReferenceInterpolator.TryInterpolateByArcLength( - anchors, - referenceArcLength, - out SmoothingPoint2D reference, - out reason)) - { - return false; - } - - Point2D evaluated; - if (sampleIndex == 0) - evaluated = interval.Start.Position; - else if (sampleIndex == SamplesPerInterval) - evaluated = interval.End.Position; - else if (!interval.TryEvaluate(parameter, out evaluated, out Point2D derivative, out Point2D secondDerivative)) - { - reason = "五次 Hermite 采样产生非有限位置或导数。"; - return false; - } - - double displacement = Distance(evaluated, reference); - if (!NumericGuard.IsFinite(displacement)) - { - reason = "五次 Hermite 采样位移无效。"; - return false; - } - double allowedDisplacement = Math.Max(0d, reference.BodyClearance - reserveMeters); - if (displacement > allowedDisplacement) - { - reason = "五次 Hermite 采样点超过对应局部弧长参考点的允许移动范围。"; - status = SmoothingCandidateStatus.RetryableInfeasible; - return false; - } - - bool firstEndpoint = intervalIndex == 0 && sampleIndex == 0; - bool lastEndpoint = intervalIndex == intervals.Count - 1 && sampleIndex == SamplesPerInterval; - if (firstEndpoint) - { - sampled.Add(anchors[0]); - } - else if (lastEndpoint) - { - sampled.Add(anchors[anchors.Count - 1]); - } - else - { - sampled.Add(new SmoothingPoint2D( - evaluated.X, - evaluated.Y, - reference.ArcLength, - reference.Heading, - reference.UnwrappedHeading, - reference.BodyClearance, - false, - SmoothedPathPointSource.Interpolated)); - } - } - } - - result = sampled; - return true; - } - - private static bool ValidateAnchors( - IReadOnlyList anchors, - CancellationToken cancellationToken, - out string reason) - { - reason = string.Empty; - for (int index = 0; index < anchors.Count; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - SmoothingPoint2D point = anchors[index]; - if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || - !NumericGuard.IsFinite(point.ArcLength) || !NumericGuard.IsFinite(point.Heading) || - !NumericGuard.IsFinite(point.UnwrappedHeading) || !NumericGuard.IsFinite(point.BodyClearance) || - point.ArcLength < 0d || point.BodyClearance < 0d || - (index > 0 && point.ArcLength <= anchors[index - 1].ArcLength)) - { - reason = "五次 Hermite 方向段包含非有限、非递增弧长或无效净空的锚点。"; - return false; - } - } - return true; - } - - private static bool TryCreateKnots( - IReadOnlyList anchors, - TravelDirection direction, - double effectiveStrength, - double knotSpacingMeters, - double minimumKnotSpacingMeters, - CancellationToken cancellationToken, - out List knots, - out string reason) - { - knots = new List(); - reason = string.Empty; - double startArcLength = anchors[0].ArcLength; - double endArcLength = anchors[anchors.Count - 1].ArcLength; - double totalLength = endArcLength - startArcLength; - if (!NumericGuard.IsPositiveFinite(totalLength) || totalLength < minimumKnotSpacingMeters) - { - reason = "五次 Hermite 方向段短于配置的最小结点间距。"; - return false; - } - - if (!TryCreateKnot(anchors[0], direction, effectiveStrength, out Knot first)) - { - reason = "五次 Hermite 起点结点或行进切向无效。"; - return false; - } - knots.Add(first); - - double endpointTolerance = GetEndpointNormalizationTolerance( - startArcLength, - endArcLength, - knotSpacingMeters); - double previousArcLength = startArcLength; - for (long knotOrdinal = 1L; ; knotOrdinal++) - { - cancellationToken.ThrowIfCancellationRequested(); - double targetArcLength = startArcLength + knotOrdinal * knotSpacingMeters; - if (!NumericGuard.IsFinite(targetArcLength)) - { - reason = "五次 Hermite 内部结点弧长无效。"; - return false; - } - if (targetArcLength >= endArcLength - endpointTolerance) break; - if (targetArcLength <= previousArcLength) - { - reason = "五次 Hermite 内部结点无法在浮点弧长尺度上保持递增。"; - return false; - } - if (!PathReferenceInterpolator.TryInterpolateByArcLength( - anchors, - targetArcLength, - out SmoothingPoint2D reference, - out reason) || - !TryCreateKnot(reference, direction, effectiveStrength, out Knot knot)) - { - if (string.IsNullOrEmpty(reason)) reason = "五次 Hermite 内部结点或行进切向无效。"; - return false; - } - knots.Add(knot); - previousArcLength = targetArcLength; - } - - if (!TryCreateKnot(anchors[anchors.Count - 1], direction, effectiveStrength, out Knot last)) - { - reason = "五次 Hermite 终点结点或行进切向无效。"; - return false; - } - knots.Add(last); - - for (int index = 1; index < knots.Count; index++) - { - double intervalLength = knots[index].ArcLength - knots[index - 1].ArcLength; - if (!NumericGuard.IsPositiveFinite(intervalLength) || intervalLength < minimumKnotSpacingMeters) - { - reason = "五次 Hermite 结点间隔无效或短于配置的最小间距。"; - return false; - } - } - return true; - } - - private static double GetEndpointNormalizationTolerance( - double startArcLength, - double endArcLength, - double knotSpacingMeters) - { - double magnitude = Math.Max( - Math.Abs(startArcLength), - Math.Max(Math.Abs(endArcLength), Math.Abs(knotSpacingMeters))); - return EndpointNormalizationUlps * DoubleMachineEpsilon * magnitude; - } - - private static bool TryCreateKnot( - SmoothingPoint2D reference, - TravelDirection direction, - double effectiveStrength, - out Knot knot) - { - knot = default; - if (reference == null || !NumericGuard.IsFinite(reference.X) || !NumericGuard.IsFinite(reference.Y) || - !NumericGuard.IsFinite(reference.ArcLength) || !NumericGuard.IsFinite(reference.Heading)) - { - return false; - } - - double travelHeading = direction == TravelDirection.Forward - ? reference.Heading - : reference.Heading - Math.PI; - double tangentX = Math.Cos(travelHeading); - double tangentY = Math.Sin(travelHeading); - if (!NumericGuard.IsFinite(tangentX) || !NumericGuard.IsFinite(tangentY)) return false; - - var velocity = new Point2D(tangentX * effectiveStrength, tangentY * effectiveStrength); - if (!velocity.IsFinite) return false; - knot = new Knot(reference.ArcLength, new Point2D(reference.X, reference.Y), velocity); - return true; - } - - private static bool TryAssignSharedAccelerations(List knots, out string reason) - { - reason = string.Empty; - for (int index = 0; index < knots.Count; index++) - { - Point2D acceleration; - if (index == 0) - { - if (!TryAcceleration(knots[0], knots[1], out acceleration)) - { - reason = "五次 Hermite 起点加速度无效。"; - return false; - } - } - else if (index == knots.Count - 1) - { - if (!TryAcceleration(knots[index - 1], knots[index], out acceleration)) - { - reason = "五次 Hermite 终点加速度无效。"; - return false; - } - } - else - { - if (!TryAcceleration(knots[index - 1], knots[index], out Point2D left) || - !TryAcceleration(knots[index], knots[index + 1], out Point2D right)) - { - reason = "五次 Hermite 共享结点加速度无效。"; - return false; - } - acceleration = new Point2D((left.X + right.X) / 2d, (left.Y + right.Y) / 2d); - if (!acceleration.IsFinite) - { - reason = "五次 Hermite 共享结点加速度混合产生非有限数值。"; - return false; - } - } - - knots[index] = knots[index].WithAcceleration(acceleration); - } - return true; - } - - private static bool TryAcceleration(Knot start, Knot end, out Point2D acceleration) - { - acceleration = default; - double intervalLength = end.ArcLength - start.ArcLength; - if (!NumericGuard.IsPositiveFinite(intervalLength)) return false; - acceleration = new Point2D( - (end.Velocity.X - start.Velocity.X) / intervalLength, - (end.Velocity.Y - start.Velocity.Y) / intervalLength); - return acceleration.IsFinite; - } - - private static bool TryCreateIntervals( - IReadOnlyList knots, - out List intervals, - out string reason) - { - intervals = new List(knots.Count - 1); - reason = string.Empty; - for (int index = 1; index < knots.Count; index++) - { - if (!QuinticInterval.TryCreate(knots[index - 1], knots[index], out QuinticInterval interval)) - { - reason = "五次 Hermite 系数、端点导数或结点区间无效。"; - return false; - } - intervals.Add(interval); - } - return true; - } - - private static double Distance(Point2D point, SmoothingPoint2D reference) - { - double deltaX = point.X - reference.X; - double deltaY = point.Y - reference.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } - - private readonly struct Knot - { - internal Knot(double arcLength, Point2D position, Point2D velocity) - { - ArcLength = arcLength; - Position = position; - Velocity = velocity; - Acceleration = default; - } - - internal double ArcLength { get; } - - internal Point2D Position { get; } - - internal Point2D Velocity { get; } - - internal Point2D Acceleration { get; } - - internal Knot WithAcceleration(Point2D acceleration) - { - return new Knot(ArcLength, Position, Velocity, acceleration); - } - - private Knot(double arcLength, Point2D position, Point2D velocity, Point2D acceleration) - { - ArcLength = arcLength; - Position = position; - Velocity = velocity; - Acceleration = acceleration; - } - } - - private readonly struct QuinticInterval - { - private QuinticInterval(Knot start, Knot end, Point2D c0, Point2D c1, Point2D c2, Point2D c3, Point2D c4, Point2D c5) - { - Start = start; - End = end; - Length = end.ArcLength - start.ArcLength; - _c0 = c0; - _c1 = c1; - _c2 = c2; - _c3 = c3; - _c4 = c4; - _c5 = c5; - } - - private readonly Point2D _c0; - private readonly Point2D _c1; - private readonly Point2D _c2; - private readonly Point2D _c3; - private readonly Point2D _c4; - private readonly Point2D _c5; - - internal Knot Start { get; } - - internal Knot End { get; } - - internal double Length { get; } - - internal static bool TryCreate(Knot start, Knot end, out QuinticInterval interval) - { - interval = default; - double length = end.ArcLength - start.ArcLength; - if (!NumericGuard.IsPositiveFinite(length) || !start.Position.IsFinite || !end.Position.IsFinite || - !start.Velocity.IsFinite || !end.Velocity.IsFinite || - !start.Acceleration.IsFinite || !end.Acceleration.IsFinite) - { - return false; - } - - Point2D c0 = start.Position; - Point2D c1 = Scale(start.Velocity, length); - Point2D c2 = Scale(start.Acceleration, length * length / 2d); - Point2D difference = Subtract(end.Position, start.Position); - Point2D endVelocity = Scale(end.Velocity, length); - Point2D startAcceleration = Scale(start.Acceleration, length * length); - Point2D endAcceleration = Scale(end.Acceleration, length * length); - Point2D c3 = Add( - Add(Scale(difference, 10d), Scale(c1, -6d)), - Add(Scale(endVelocity, -4d), Add(Scale(startAcceleration, -1.5d), Scale(endAcceleration, 0.5d)))); - Point2D c4 = Add( - Add(Scale(difference, -15d), Scale(c1, 8d)), - Add(Scale(endVelocity, 7d), Add(Scale(startAcceleration, 1.5d), Scale(endAcceleration, -1d)))); - Point2D c5 = Add( - Add(Scale(difference, 6d), Add(Scale(c1, -3d), Scale(endVelocity, -3d))), - Add(Scale(startAcceleration, -0.5d), Scale(endAcceleration, 0.5d))); - if (!c0.IsFinite || !c1.IsFinite || !c2.IsFinite || !c3.IsFinite || !c4.IsFinite || !c5.IsFinite) - return false; - - interval = new QuinticInterval(start, end, c0, c1, c2, c3, c4, c5); - return interval.TryEvaluate(0d, out _, out _, out _) && interval.TryEvaluate(1d, out _, out _, out _); - } - - internal bool TryEvaluate(double parameter, out Point2D position, out Point2D derivative, out Point2D secondDerivative) - { - position = default; - derivative = default; - secondDerivative = default; - if (!NumericGuard.IsFinite(parameter) || parameter < 0d || parameter > 1d) return false; - - double t2 = parameter * parameter; - double t3 = t2 * parameter; - double t4 = t3 * parameter; - double t5 = t4 * parameter; - position = Add(Add(Add(_c0, Scale(_c1, parameter)), Add(Scale(_c2, t2), Scale(_c3, t3))), - Add(Scale(_c4, t4), Scale(_c5, t5))); - derivative = Add(Add(_c1, Scale(_c2, 2d * parameter)), - Add(Scale(_c3, 3d * t2), Add(Scale(_c4, 4d * t3), Scale(_c5, 5d * t4)))); - secondDerivative = Add(Scale(_c2, 2d), - Add(Scale(_c3, 6d * parameter), Add(Scale(_c4, 12d * t2), Scale(_c5, 20d * t3)))); - return position.IsFinite && derivative.IsFinite && secondDerivative.IsFinite; - } - } - - private readonly struct Point2D - { - internal Point2D(double x, double y) - { - X = x; - Y = y; - } - - internal double X { get; } - - internal double Y { get; } - - internal bool IsFinite => NumericGuard.IsFinite(X) && NumericGuard.IsFinite(Y); - } - - private static Point2D Add(Point2D left, Point2D right) - { - return new Point2D(left.X + right.X, left.Y + right.Y); - } - - private static Point2D Subtract(Point2D left, Point2D right) - { - return new Point2D(left.X - right.X, left.Y - right.Y); - } - - private static Point2D Scale(Point2D point, double scale) - { - return new Point2D(point.X * scale, point.Y * scale); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs deleted file mode 100644 index 1a22b83..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 单次算法运行共享的已预处理路径和独立复核上下文。 -internal sealed class SmoothingAlgorithmInput -{ - internal SmoothingAlgorithmInput( - PreparedPath originalPath, - PlanningGridMap map, - VehicleParameters vehicle, - double maximumCollisionCheckStepMeters, - double minimumClearanceReserveMeters, - SmoothingOptionsSnapshot options) - { - OriginalPath = originalPath ?? throw new ArgumentNullException(nameof(originalPath)); - Map = map ?? throw new ArgumentNullException(nameof(map)); - Vehicle = vehicle ?? throw new ArgumentNullException(nameof(vehicle)); - MaximumCollisionCheckStepMeters = maximumCollisionCheckStepMeters; - MinimumClearanceReserveMeters = minimumClearanceReserveMeters; - Options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// 已校验并按方向分段的原始路径。 - internal PreparedPath OriginalPath { get; } - - /// 用于完整车体复核的不可变规划地图。 - internal PlanningGridMap Map { get; } - - /// 用于曲率和足迹复核的车辆参数快照。 - internal VehicleParameters Vehicle { get; } - - /// 连续车体碰撞检查的最大步长,单位 m。 - internal double MaximumCollisionCheckStepMeters { get; } - - /// 候选几何必须从原始保守净空中预留的最小安全余量,单位 m。 - internal double MinimumClearanceReserveMeters { get; } - - /// 本次算法运行使用的已验证方法选项快照。 - internal SmoothingOptionsSnapshot Options { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs deleted file mode 100644 index 55282ca..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 在有限强度计划内运行单一算法,并以共享分析和安全复核决定是否接受候选。 -internal sealed class SmoothingAlgorithmRunner -{ - private static readonly double[] RetryStrengthScales = { 1d, 0.75d, 0.50d, 0.25d }; - private readonly PathGeometryAnalyzer _analyzer; - private readonly SmoothedPathValidator _validator; - - internal SmoothingAlgorithmRunner() - : this(new PathGeometryAnalyzer(), new SmoothedPathValidator()) - { - } - - internal SmoothingAlgorithmRunner(PathGeometryAnalyzer analyzer, SmoothedPathValidator validator) - { - _analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer)); - _validator = validator ?? throw new ArgumentNullException(nameof(validator)); - } - - /// - /// 依次尝试配置的有限强度比例。取消直接向上传播,由门面转换为最终状态; - /// 只有算法明确标记为可重试的不可行性才会使用较低强度;终止失败和统一复核失败均不重试。 - /// - internal AlgorithmRunResult Run( - IPathSmoother smoother, - SmoothingAlgorithmInput input, - PathSmoothingConfiguration configuration, - CancellationToken cancellationToken) - { - var attemptedStrengths = new List(); - var failureReasons = new List(); - if (smoother == null || input == null || input.Options == null || configuration == null) - return AlgorithmRunResult.Failed("平滑算法、输入或配置无效。", attemptedStrengths, failureReasons); - - foreach (double scale in RetryStrengthScales) - { - cancellationToken.ThrowIfCancellationRequested(); - double effectiveStrength = configuration.SmoothingStrength * scale; - if (!IsPositiveFinite(effectiveStrength)) - return AlgorithmRunResult.Failed("平滑强度或重试比例无效。", attemptedStrengths, failureReasons); - - attemptedStrengths.Add(effectiveStrength); - SmoothingCandidate candidate = smoother.Smooth(input, effectiveStrength, cancellationToken); - if (candidate == null) - return AlgorithmRunResult.Failed("平滑算法未返回候选。", attemptedStrengths, failureReasons); - if (candidate.Status == SmoothingCandidateStatus.RetryableInfeasible) - { - failureReasons.Add(candidate.Reason); - continue; - } - if (candidate.Status == SmoothingCandidateStatus.Failed) - { - failureReasons.Add(candidate.Reason); - return AlgorithmRunResult.Failed(candidate.Reason, attemptedStrengths, failureReasons); - } - if (candidate.Status != SmoothingCandidateStatus.Success) - { - const string unknownStatusReason = "平滑算法返回未知候选状态。"; - failureReasons.Add(unknownStatusReason); - return AlgorithmRunResult.Failed(unknownStatusReason, attemptedStrengths, failureReasons); - } - - if (!_analyzer.TryAnalyze(candidate.Segments, configuration.OutputSpacingMeters, - out PathGeometryAnalysis analysis, out string reason)) - { - failureReasons.Add(reason); - return AlgorithmRunResult.Failed(reason, attemptedStrengths, failureReasons); - } - - if (_validator.TryValidate(analysis.Path, analysis.Segments, input.OriginalPath, input.Map, input.Vehicle, - input.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath, - out double minimumClearanceMeters, out reason)) - { - return AlgorithmRunResult.Success(safePath, analysis.Segments, - CreateMetrics(analysis, minimumClearanceMeters), effectiveStrength, - attemptedStrengths, failureReasons); - } - - failureReasons.Add(reason); - return AlgorithmRunResult.Failed(reason, attemptedStrengths, failureReasons); - } - - return AlgorithmRunResult.Infeasible(null, attemptedStrengths, failureReasons); - } - - private static PathQualityMetrics CreateMetrics(PathGeometryAnalysis analysis, double minimumClearanceMeters) - { - return new PathQualityMetrics( - true, - analysis.PathLengthMeters, - analysis.MaximumAbsoluteVehicleCurvaturePerMeter, - analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - analysis.RootMeanSquareVehicleCurvaturePerMeter, - analysis.TotalAbsoluteCurvatureVariationPerMeter, - analysis.CurvatureVariationEnergy, - minimumClearanceMeters, - 0d, - 0d, - 0d, - 0d); - } - - private static bool IsPositiveFinite(double value) - { - return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0d; - } - - /// - /// Reflection-only deterministic coverage seam. It is nested in an internal runner and intentionally - /// does not construct or register a production smoothing method. - /// - public static class TestHooks - { - /// 执行一个固定的内部假平滑器场景并返回可反射读取的快照。 - public static RunnerTestSnapshot Execute(string scenario) - { - if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario)); - - var cancellationSource = new CancellationTokenSource(); - var smoother = new DeterministicTestSmoother(ParseScenario(scenario), cancellationSource); - var runner = new SmoothingAlgorithmRunner(); - SmoothingAlgorithmInput input = CreateTestInput(); - var configuration = new PathSmoothingConfiguration(); - try - { - AlgorithmRunResult result = runner.Run(smoother, input, configuration, cancellationSource.Token); - return RunnerTestSnapshot.FromResult(result, false); - } - catch (OperationCanceledException) - { - return new RunnerTestSnapshot( - "OperationCanceledException", - smoother.AttemptedStrengths, - 0, - 0, - 0, - true); - } - finally - { - cancellationSource.Dispose(); - } - } - - /// 供 PowerShell 断言使用的不可变执行摘要。 - public sealed class RunnerTestSnapshot - { - internal RunnerTestSnapshot( - string status, - IReadOnlyList attemptedStrengths, - int acceptedPathPointCount, - int rejectedComparisonCandidatePointCount, - int failureCount, - bool cancellationPropagated) - { - Status = status ?? string.Empty; - AttemptedStrengths = CopyReadOnly(attemptedStrengths); - AcceptedPathPointCount = acceptedPathPointCount; - RejectedComparisonCandidatePointCount = rejectedComparisonCandidatePointCount; - FailureCount = failureCount; - CancellationPropagated = cancellationPropagated; - } - - public string Status { get; } - public IReadOnlyList AttemptedStrengths { get; } - public int AcceptedPathPointCount { get; } - public int RejectedComparisonCandidatePointCount { get; } - public int FailureCount { get; } - public bool CancellationPropagated { get; } - - internal static RunnerTestSnapshot FromResult(AlgorithmRunResult result, bool cancellationPropagated) - { - int rejectedPointCount = result.RejectedComparisonCandidate == null - ? 0 - : CountPoints(result.RejectedComparisonCandidate.Segments); - return new RunnerTestSnapshot( - result.Status.ToString(), - result.AttemptedStrengths, - result.Path.Count, - rejectedPointCount, - result.FailureReasons.Count, - cancellationPropagated); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } - } - - private enum TestScenario - { - RetryableInfeasible, - AcceptFirst, - TerminalFailed, - CancelBeforeNextAttempt, - } - - private sealed class DeterministicTestSmoother : IPathSmoother - { - private readonly TestScenario _scenario; - private readonly CancellationTokenSource _cancellationSource; - - internal DeterministicTestSmoother(TestScenario scenario, CancellationTokenSource cancellationSource) - { - _scenario = scenario; - _cancellationSource = cancellationSource; - AttemptedStrengths = new List(); - } - - public SmoothingMethod Method => SmoothingMethod.CubicBSpline; - - internal List AttemptedStrengths { get; } - - public SmoothingCandidate Smooth( - SmoothingAlgorithmInput input, - double effectiveStrength, - CancellationToken cancellationToken) - { - AttemptedStrengths.Add(effectiveStrength); - if (_scenario == TestScenario.TerminalFailed) - return SmoothingCandidate.Failed("确定性数值退化。"); - if (_scenario == TestScenario.AcceptFirst) - return CreateAcceptedCandidate(); - - if (_scenario == TestScenario.CancelBeforeNextAttempt) - _cancellationSource.Cancel(); - return SmoothingCandidate.RetryableInfeasible("确定性可重试不可行。" ); - } - } - - private static TestScenario ParseScenario(string scenario) - { - if (string.Equals(scenario, nameof(TestScenario.RetryableInfeasible), StringComparison.Ordinal)) return TestScenario.RetryableInfeasible; - if (string.Equals(scenario, nameof(TestScenario.AcceptFirst), StringComparison.Ordinal)) return TestScenario.AcceptFirst; - if (string.Equals(scenario, nameof(TestScenario.TerminalFailed), StringComparison.Ordinal)) return TestScenario.TerminalFailed; - if (string.Equals(scenario, nameof(TestScenario.CancelBeforeNextAttempt), StringComparison.Ordinal)) return TestScenario.CancelBeforeNextAttempt; - throw new ArgumentOutOfRangeException(nameof(scenario)); - } - - private static SmoothingAlgorithmInput CreateTestInput() - { - var mapRequest = new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f), - ResolutionMm = 50f, - AllowExplicitEmptyMap = true, - }; - PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(mapRequest); - if (!mapResult.Succeeded || mapResult.Map == null) - throw new InvalidOperationException("The runner test hook could not create its empty map."); - - var originalSegments = new List - { - new PreparedDirectionSegment( - 0, - TravelDirection.Forward, - new List - { - CreatePoint(0.5d, 0.5d, 0d), - CreatePoint(1.5d, 0.5d, 1d), - }, - false, - false), - }; - var vehicle = new VehicleParameters - { - LengthMeters = 0.20d, - WidthMeters = 0.20d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 100d, - MinimumTurningRadiusMeters = 0.01d, - }; - return new SmoothingAlgorithmInput( - new PreparedPath(originalSegments), - mapResult.Map, - vehicle, - 0.05d, - 0.02d, - new SmoothingOptionsSnapshot(new PathSmoothingConfiguration())); - } - - private static SmoothingCandidate CreateAcceptedCandidate() - { - return SmoothingCandidate.Success(new List - { - new PreparedDirectionSegment( - 0, - TravelDirection.Forward, - new List - { - CreatePoint(0.5d, 0.5d, 0d), - CreatePoint(1.5d, 0.5d, 1d), - }, - false, - false), - }); - } - - private static SmoothingPoint2D CreatePoint(double x, double y, double arcLength) - { - return new SmoothingPoint2D( - x, - y, - arcLength, - 0d, - 0d, - 1d, - false, - SmoothedPathPointSource.Anchor); - } - - private static int CountPoints(IReadOnlyList segments) - { - int count = 0; - for (int index = 0; index < segments.Count; index++) count += segments[index].Points.Count; - return count; - } - } - - /// 内部运行结果;只有成功路径可被正式门面发布,拒绝候选仅供比较诊断读取。 - internal sealed class AlgorithmRunResult - { - private static readonly IReadOnlyList EmptyPath = - new ReadOnlyCollection(new List()); - private static readonly IReadOnlyList EmptySegments = - new ReadOnlyCollection(new List()); - - private AlgorithmRunResult( - PathSmoothingStatus status, - IReadOnlyList path, - IReadOnlyList segments, - PathQualityMetrics metrics, - double acceptedStrength, - SmoothingCandidate rejectedComparisonCandidate, - IReadOnlyList attemptedStrengths, - IReadOnlyList failureReasons, - string reason) - { - Status = status; - Path = path ?? EmptyPath; - Segments = segments ?? EmptySegments; - Metrics = metrics ?? new PathQualityMetrics(); - AcceptedStrength = acceptedStrength; - RejectedComparisonCandidate = rejectedComparisonCandidate; - AttemptedStrengths = CopyReadOnly(attemptedStrengths); - FailureReasons = CopyReadOnly(failureReasons); - Reason = reason ?? string.Empty; - } - - internal PathSmoothingStatus Status { get; } - internal IReadOnlyList Path { get; } - internal IReadOnlyList Segments { get; } - internal PathQualityMetrics Metrics { get; } - internal double AcceptedStrength { get; } - internal SmoothingCandidate RejectedComparisonCandidate { get; } - internal IReadOnlyList AttemptedStrengths { get; } - internal IReadOnlyList FailureReasons { get; } - internal string Reason { get; } - - internal static AlgorithmRunResult Success( - IReadOnlyList path, - IReadOnlyList segments, - PathQualityMetrics metrics, - double acceptedStrength, - IReadOnlyList attemptedStrengths, - IReadOnlyList failureReasons) - { - return new AlgorithmRunResult( - PathSmoothingStatus.Success, - CopyReadOnly(path), - CopyReadOnly(segments), - metrics, - acceptedStrength, - null, - attemptedStrengths, - failureReasons, - string.Empty); - } - - internal static AlgorithmRunResult Infeasible( - SmoothingCandidate rejectedComparisonCandidate, - IReadOnlyList attemptedStrengths, - IReadOnlyList failureReasons) - { - string reason = failureReasons == null || failureReasons.Count == 0 - ? "所有有限平滑尝试均未通过复核。" - : failureReasons[failureReasons.Count - 1]; - return new AlgorithmRunResult( - PathSmoothingStatus.Infeasible, - null, - null, - null, - 0d, - rejectedComparisonCandidate, - attemptedStrengths, - failureReasons, - reason); - } - - internal static AlgorithmRunResult Failed( - string reason, - IReadOnlyList attemptedStrengths, - IReadOnlyList failureReasons) - { - return new AlgorithmRunResult( - PathSmoothingStatus.Failed, - null, - null, - null, - 0d, - null, - attemptedStrengths, - failureReasons, - reason); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs deleted file mode 100644 index 0e35ad9..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -internal enum SmoothingCandidateStatus -{ - Success, - RetryableInfeasible, - Failed, -} - -/// 平滑方法产生的原始方向段候选,尚未经过统一几何或安全复核。 -internal sealed class SmoothingCandidate -{ - private SmoothingCandidate( - SmoothingCandidateStatus status, - IReadOnlyList segments, - string reason) - { - Status = status; - if (status == SmoothingCandidateStatus.Success) - { - if (segments == null || segments.Count == 0) - throw new ArgumentException("A successful smoothing candidate requires direction segments.", nameof(segments)); - for (int index = 0; index < segments.Count; index++) - { - if (segments[index] == null || segments[index].Points == null || segments[index].Points.Count == 0) - throw new ArgumentException("A successful smoothing candidate requires complete direction segments.", nameof(segments)); - } - Segments = CopyReadOnly(segments); - Reason = string.Empty; - return; - } - - if (string.IsNullOrWhiteSpace(reason)) - throw new ArgumentException("An unsuccessful smoothing candidate requires a reason.", nameof(reason)); - Segments = CopyReadOnly(null); - Reason = reason; - } - - /// 候选是否成功产生有限的原始几何。 - internal bool Succeeded => Status == SmoothingCandidateStatus.Success; - - /// 候选的可重试性和终止性状态。 - internal SmoothingCandidateStatus Status { get; } - - /// 候选方向段;失败候选始终为空。 - internal IReadOnlyList Segments { get; } - - /// 失败或退化时的稳定说明;成功时为空。 - internal string Reason { get; } - - /// 创建待统一分析和验证的成功候选。 - internal static SmoothingCandidate Success(IReadOnlyList segments) - { - return new SmoothingCandidate(SmoothingCandidateStatus.Success, segments, string.Empty); - } - - /// 创建可由较低平滑强度重新尝试的不可行候选。 - internal static SmoothingCandidate RetryableInfeasible(string reason) - { - return new SmoothingCandidate(SmoothingCandidateStatus.RetryableInfeasible, null, reason); - } - - /// 创建不应重试的数值或构造失败候选。 - internal static SmoothingCandidate Failed(string reason) - { - return new SmoothingCandidate(SmoothingCandidateStatus.Failed, null, reason); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs deleted file mode 100644 index 974ed9d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; - -/// 供单次平滑算法运行使用的、已校验的不可变方法选项快照。 -internal sealed class SmoothingOptionsSnapshot -{ - internal SmoothingOptionsSnapshot(PathSmoothingConfiguration configuration) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - CubicBSplineEndpointTangentScale = configuration.CubicBSpline.EndpointTangentScale; - BezierCornerHeadingThresholdRadians = configuration.LocalCubicBezier.CornerHeadingThresholdRadians; - BezierMaximumWindowLengthMeters = configuration.LocalCubicBezier.MaximumWindowLengthMeters; - BezierHandleLengthRatio = configuration.LocalCubicBezier.HandleLengthRatio; - QuinticKnotSpacingMeters = configuration.PiecewiseQuintic.KnotSpacingMeters; - QuinticMinimumKnotSpacingMeters = configuration.PiecewiseQuintic.MinimumKnotSpacingMeters; - - ValidatePositiveFinite(CubicBSplineEndpointTangentScale, nameof(CubicBSplineEndpointTangentScale)); - if (!NumericGuard.IsFinite(BezierCornerHeadingThresholdRadians) || - BezierCornerHeadingThresholdRadians <= 0d || BezierCornerHeadingThresholdRadians > Math.PI) - { - throw new ArgumentOutOfRangeException(nameof(BezierCornerHeadingThresholdRadians)); - } - ValidatePositiveFinite(BezierMaximumWindowLengthMeters, nameof(BezierMaximumWindowLengthMeters)); - ValidatePositiveFinite(BezierHandleLengthRatio, nameof(BezierHandleLengthRatio)); - ValidatePositiveFinite(QuinticKnotSpacingMeters, nameof(QuinticKnotSpacingMeters)); - ValidatePositiveFinite(QuinticMinimumKnotSpacingMeters, nameof(QuinticMinimumKnotSpacingMeters)); - if (QuinticKnotSpacingMeters < QuinticMinimumKnotSpacingMeters) - throw new ArgumentOutOfRangeException(nameof(QuinticKnotSpacingMeters)); - } - - internal double CubicBSplineEndpointTangentScale { get; } - - internal double BezierCornerHeadingThresholdRadians { get; } - - internal double BezierMaximumWindowLengthMeters { get; } - - internal double BezierHandleLengthRatio { get; } - - internal double QuinticKnotSpacingMeters { get; } - - internal double QuinticMinimumKnotSpacingMeters { get; } - - private static void ValidatePositiveFinite(double value, string name) - { - if (!NumericGuard.IsPositiveFinite(value)) throw new ArgumentOutOfRangeException(name); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs deleted file mode 100644 index 2e50fb6..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -/// 原始基线或一种平滑方法的不可变比较条目。 -public sealed class PathSmoothingComparisonEntry -{ - /// 为测试、离线分析和排序创建不携带路径几何的候选条目。 - public PathSmoothingComparisonEntry( - SmoothingMethod method, - PathSmoothingStatus status, - PathQualityMetrics metrics, - SmoothingTimingSummary timing, - string stableGeometryDigest, - string diagnostic) - : this(method, false, status, metrics, timing, stableGeometryDigest, diagnostic, 0, 0d, Empty(), Empty()) - { - } - - private PathSmoothingComparisonEntry( - SmoothingMethod? method, - bool isRawPathBaseline, - PathSmoothingStatus status, - PathQualityMetrics metrics, - SmoothingTimingSummary timing, - string stableGeometryDigest, - string diagnostic, - int retryCount, - double acceptedStrength, - IReadOnlyList path, - IReadOnlyList segments) - { - Method = method; - IsRawPathBaseline = isRawPathBaseline; - Status = status; - Metrics = metrics ?? new PathQualityMetrics(); - Timing = timing ?? new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, "未提供计时结果。"); - StableGeometryDigest = stableGeometryDigest ?? string.Empty; - Diagnostic = diagnostic ?? string.Empty; - RetryCount = retryCount < 0 ? 0 : retryCount; - AcceptedStrength = acceptedStrength; - Path = CopyReadOnly(path); - Segments = CopyReadOnly(segments); - } - - /// 候选所代表的方法;原始粗路径基线为空。 - public SmoothingMethod? Method { get; } - - /// 是否为单独分析的原始粗路径基线。 - public bool IsRawPathBaseline { get; } - - /// 本条目的最终状态。 - public PathSmoothingStatus Status { get; } - - /// 使用原始基线规范化后的质量指标。 - public PathQualityMetrics Metrics { get; } - - /// 方法的五次测量计时;基线不参与计时排名。 - public SmoothingTimingSummary Timing { get; } - - /// 由状态、分段元数据和完整路径 IEEE 754 位模式生成的 SHA-256 摘要。 - public string StableGeometryDigest { get; } - - /// 面向报告和诊断的稳定说明。 - public string Diagnostic { get; } - - /// 规范平滑调用记录的重试次数;原始粗路径基线为零。 - public int RetryCount { get; } - - /// 规范平滑调用接受的强度;未接受候选和原始粗路径基线为零。 - public double AcceptedStrength { get; } - - /// 仅供比较与报告读取的正式路径;失败时为空。 - public IReadOnlyList Path { get; } - - /// 覆盖 的方向段;失败时为空。 - public IReadOnlyList Segments { get; } - - /// 条目能否参与方法推荐。 - public bool IsEligibleForRecommendation => - !IsRawPathBaseline && - Status == PathSmoothingStatus.Success && - Metrics.IsFeasible && - Timing.IsDeterministic; - - internal static PathSmoothingComparisonEntry CreateCandidate( - SmoothingMethod method, - PathSmoothingStatus status, - PathQualityMetrics metrics, - SmoothingTimingSummary timing, - string stableGeometryDigest, - string diagnostic, - int retryCount, - double acceptedStrength, - IReadOnlyList path, - IReadOnlyList segments) - { - return new PathSmoothingComparisonEntry( - method, false, status, metrics, timing, stableGeometryDigest, diagnostic, retryCount, acceptedStrength, path, segments); - } - - internal static PathSmoothingComparisonEntry CreateRawPathBaseline( - PathSmoothingStatus status, - PathQualityMetrics metrics, - string stableGeometryDigest, - string diagnostic, - IReadOnlyList path, - IReadOnlyList segments) - { - return new PathSmoothingComparisonEntry( - null, true, status, metrics, - new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, true, string.Empty), - stableGeometryDigest, diagnostic, 0, 0d, path, segments); - } - - private static IReadOnlyList Empty() - { - return new ReadOnlyCollection(new List()); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs deleted file mode 100644 index b7ee33e..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -/// 同一粗路径的离线平滑比较请求。 -public sealed class PathSmoothingComparisonRequest -{ - private static readonly SmoothingMethod[] DefaultMethods = - { - SmoothingMethod.CubicBSpline, - SmoothingMethod.LocalCubicBezier, - SmoothingMethod.PiecewiseQuintic, - }; - - /// 创建比较请求,并固定原始输入与方法顺序。 - public PathSmoothingComparisonRequest( - PathSmoothingRequest smoothingRequest, - IReadOnlyList methods = null) - { - SmoothingRequest = CopyRequest(smoothingRequest); - Methods = CopyMethods(methods ?? DefaultMethods); - } - - /// 所有方法共享的不可变粗路径、地图、车辆和配置快照。 - public PathSmoothingRequest SmoothingRequest { get; } - - /// 按调用方指定稳定顺序运行的方法集合。 - public IReadOnlyList Methods { get; } - - private static PathSmoothingRequest CopyRequest(PathSmoothingRequest source) - { - if (source == null) return null; - return new PathSmoothingRequest( - source.CoarsePath, - source.Segments, - source.Map, - source.Vehicle, - source.Configuration); - } - - private static IReadOnlyList CopyMethods(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) - { - SmoothingMethod method = source[index]; - if (!Enum.IsDefined(typeof(SmoothingMethod), method)) - throw new ArgumentOutOfRangeException(nameof(source), "比较方法无效。"); - if (copy.Contains(method)) - throw new ArgumentException("比较方法不能重复。", nameof(source)); - copy.Add(method); - } - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs deleted file mode 100644 index 94ed422..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -/// 一次离线比较的不可变基线、方法条目和推荐结论。 -public sealed class PathSmoothingComparisonResult -{ - internal PathSmoothingComparisonResult( - PathSmoothingComparisonEntry rawPathBaseline, - IReadOnlyList entries, - SmoothingMethod? recommendedMethod, - bool isCancelled, - string diagnostic) - { - RawPathBaseline = rawPathBaseline ?? throw new ArgumentNullException(nameof(rawPathBaseline)); - Entries = CopyReadOnly(entries); - RecommendedMethod = recommendedMethod; - IsCancelled = isCancelled; - Diagnostic = diagnostic ?? string.Empty; - } - - /// 独立分析的原始粗路径;不属于任何候选方法。 - public PathSmoothingComparisonEntry RawPathBaseline { get; } - - /// 每个请求方法恰有一个条目;取消时可能只包含已完成的方法。 - public IReadOnlyList Entries { get; } - - /// 按公开字典序选择的方法;没有合格方法或取消时为空。 - public SmoothingMethod? RecommendedMethod { get; } - - /// 比较是否在启动后续方法前被取消。 - public bool IsCancelled { get; } - - /// 整个比较的稳定状态说明。 - public string Diagnostic { get; } - - private static IReadOnlyList CopyReadOnly( - IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs deleted file mode 100644 index 9844037..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -/// 按公开字典序选择唯一的推荐平滑方法。 -public static class SmoothingMethodRanker -{ - /// 从当前场景的可行且确定性候选中选择最佳方法;没有合格候选时返回空。 - public static SmoothingMethod? Rank(IReadOnlyList entries) - { - PathSmoothingComparisonEntry best = null; - if (entries == null) return null; - - for (int index = 0; index < entries.Count; index++) - { - PathSmoothingComparisonEntry candidate = entries[index]; - if (candidate == null || !candidate.IsEligibleForRecommendation) continue; - if (best == null || Compare(candidate, best) < 0) best = candidate; - } - return best == null ? (SmoothingMethod?)null : best.Method; - } - - private static int Compare(PathSmoothingComparisonEntry left, PathSmoothingComparisonEntry right) - { - int comparison = CompareAscending(left.Metrics.CurvatureVariationEnergy, right.Metrics.CurvatureVariationEnergy); - if (comparison != 0) return comparison; - - comparison = CompareAscending( - left.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, - right.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter); - if (comparison != 0) return comparison; - - comparison = CompareDescending(left.Metrics.MinimumBodyClearanceMeters, right.Metrics.MinimumBodyClearanceMeters); - if (comparison != 0) return comparison; - - comparison = CompareAscending(left.Metrics.LengthChangePercent, right.Metrics.LengthChangePercent); - if (comparison != 0) return comparison; - - comparison = CompareAscending(left.Timing.MedianElapsedMilliseconds, right.Timing.MedianElapsedMilliseconds); - if (comparison != 0) return comparison; - - return ((int)left.Method.Value).CompareTo((int)right.Method.Value); - } - - private static int CompareAscending(double left, double right) - { - return Normalize(left).CompareTo(Normalize(right)); - } - - private static int CompareDescending(double left, double right) - { - return Normalize(right).CompareTo(Normalize(left)); - } - - private static double Normalize(double value) - { - return double.IsNaN(value) || double.IsInfinity(value) ? double.PositiveInfinity : value; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs deleted file mode 100644 index ae4a635..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -/// 一个方法的固定五次计时样本和确定性结论。 -public sealed class SmoothingTimingSummary -{ - /// 创建一个只接受五个已测量样本的计时汇总。 - public SmoothingTimingSummary( - IReadOnlyList measuredElapsedMilliseconds, - bool isDeterministic, - string diagnostic) - { - if (measuredElapsedMilliseconds == null || measuredElapsedMilliseconds.Count != 5) - throw new ArgumentException("计时汇总必须包含五个已测量样本。", nameof(measuredElapsedMilliseconds)); - - var copy = new List(measuredElapsedMilliseconds.Count); - for (int index = 0; index < measuredElapsedMilliseconds.Count; index++) - { - double value = measuredElapsedMilliseconds[index]; - if (double.IsNaN(value) || double.IsInfinity(value) || value < 0d) - throw new ArgumentOutOfRangeException(nameof(measuredElapsedMilliseconds), "计时样本必须为有限非负数。"); - copy.Add(value); - } - - MeasuredElapsedMilliseconds = new ReadOnlyCollection(copy); - MedianElapsedMilliseconds = Median(copy); - IsDeterministic = isDeterministic; - Diagnostic = diagnostic ?? string.Empty; - } - - /// 不含预热执行的五个实测耗时,单位 ms。 - public IReadOnlyList MeasuredElapsedMilliseconds { get; } - - /// 五个实测耗时的稳定中位数,单位 ms。 - public double MedianElapsedMilliseconds { get; } - - /// 五次输出是否具有相同的状态和稳定几何摘要。 - public bool IsDeterministic { get; } - - /// 非确定性或测量失败的稳定诊断说明。 - public string Diagnostic { get; } - - /// 从五次测量结果中生成计时汇总,并拒绝状态或几何不稳定的输出。 - public static SmoothingTimingSummary FromMeasurements( - IReadOnlyList measuredElapsedMilliseconds, - IReadOnlyList measuredResults) - { - if (measuredResults == null || measuredResults.Count != 5) - throw new ArgumentException("确定性检查必须包含五个测量结果。", nameof(measuredResults)); - for (int index = 0; index < measuredResults.Count; index++) - { - if (measuredResults[index] == null) - throw new ArgumentException("确定性检查不能包含空测量结果。", nameof(measuredResults)); - } - - PathSmoothingResult canonical = measuredResults[0]; - string canonicalDigest = StableGeometryDigest.Compute(canonical); - for (int index = 1; index < measuredResults.Count; index++) - { - PathSmoothingResult measured = measuredResults[index]; - string digest = StableGeometryDigest.Compute(measured); - if (measured.Status != canonical.Status || - measured.Path.Count != canonical.Path.Count || - measured.Segments.Count != canonical.Segments.Count || - !string.Equals(digest, canonicalDigest, StringComparison.Ordinal)) - { - return new SmoothingTimingSummary( - measuredElapsedMilliseconds, - false, - "五次测量的状态、点数、分段数或稳定几何摘要不一致。"); - } - } - - return new SmoothingTimingSummary(measuredElapsedMilliseconds, true, string.Empty); - } - - internal static double Median(IReadOnlyList values) - { - var sorted = new double[values.Count]; - for (int index = 0; index < values.Count; index++) sorted[index] = values[index]; - Array.Sort(sorted); - int middle = sorted.Length / 2; - return sorted.Length % 2 == 1 - ? sorted[middle] - : (sorted[middle - 1] + sorted[middle]) / 2d; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs deleted file mode 100644 index ee9d105..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Text; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -/// 为重复执行结果生成与进程无关的稳定几何 SHA-256 摘要。 -public static class StableGeometryDigest -{ - /// 计算正式平滑结果的状态、方法、分段和路径位模式摘要。 - public static string Compute(PathSmoothingResult result) - { - if (result == null) throw new ArgumentNullException(nameof(result)); - return Compute(result.Status, result.Method, result.Path, result.Segments); - } - - /// 计算任意已分析路径的稳定摘要。 - public static string Compute( - PathSmoothingStatus status, - SmoothingMethod? method, - IReadOnlyList path, - IReadOnlyList segments) - { - using (var stream = new MemoryStream()) - { - WriteInt32(stream, 1); - WriteInt32(stream, (int)status); - WriteBoolean(stream, method.HasValue); - if (method.HasValue) WriteInt32(stream, (int)method.Value); - - WriteInt32(stream, path == null ? 0 : path.Count); - if (path != null) - { - for (int index = 0; index < path.Count; index++) WritePoint(stream, path[index]); - } - - WriteInt32(stream, segments == null ? 0 : segments.Count); - if (segments != null) - { - for (int index = 0; index < segments.Count; index++) WriteSegment(stream, segments[index]); - } - - using (SHA256 sha256 = SHA256.Create()) - { - byte[] hash = sha256.ComputeHash(stream.ToArray()); - var builder = new StringBuilder(hash.Length * 2); - for (int index = 0; index < hash.Length; index++) builder.Append(hash[index].ToString("x2")); - return builder.ToString(); - } - } - } - - private static void WritePoint(Stream stream, SmoothedPathPoint point) - { - if (point == null) throw new ArgumentException("稳定路径摘要不能包含空点。", nameof(point)); - WriteDouble(stream, point.X); - WriteDouble(stream, point.Y); - WriteDouble(stream, point.Heading); - WriteDouble(stream, point.UnwrappedHeading); - WriteDouble(stream, point.ArcLength); - WriteInt32(stream, (int)point.Direction); - WriteDouble(stream, point.GeometricCurvature); - WriteDouble(stream, point.VehicleCurvature); - WriteDouble(stream, point.BodyClearance); - WriteBoolean(stream, point.IsGearSwitchPoint); - WriteInt32(stream, (int)point.Source); - } - - private static void WriteSegment(Stream stream, SmoothedPathSegment segment) - { - if (segment == null) throw new ArgumentException("稳定路径摘要不能包含空方向段。", nameof(segment)); - WriteInt32(stream, segment.SegmentIndex); - WriteInt32(stream, (int)segment.Direction); - WriteInt32(stream, segment.StartIndex); - WriteInt32(stream, segment.EndIndex); - WriteBoolean(stream, segment.StartsAtGearSwitch); - WriteBoolean(stream, segment.EndsAtGearSwitch); - } - - private static void WriteDouble(Stream stream, double value) - { - WriteInt64(stream, BitConverter.DoubleToInt64Bits(value)); - } - - private static void WriteBoolean(Stream stream, bool value) - { - stream.WriteByte(value ? (byte)1 : (byte)0); - } - - private static void WriteInt32(Stream stream, int value) - { - unchecked - { - stream.WriteByte((byte)value); - stream.WriteByte((byte)(value >> 8)); - stream.WriteByte((byte)(value >> 16)); - stream.WriteByte((byte)(value >> 24)); - } - } - - private static void WriteInt64(Stream stream, long value) - { - unchecked - { - ulong bits = (ulong)value; - for (int index = 0; index < 8; index++) stream.WriteByte((byte)(bits >> (index * 8))); - } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs deleted file mode 100644 index 07b2615..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 三次 B 样条平滑参数。 -public sealed class CubicBSplineOptions -{ - /// 端点切向控制柄相对于相邻弦长的比例。 - public double EndpointTangentScale { get; set; } = 1d / 3d; -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs deleted file mode 100644 index bd99eec..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 局部三次 Bézier 平滑参数。 -public sealed class LocalCubicBezierOptions -{ - /// 判定为明显转角的最小航向变化,单位 rad。 - public double CornerHeadingThresholdRadians { get; set; } = Math.PI / 18d; - - /// 单个局部平滑窗口的最大弧长,单位 m。 - public double MaximumWindowLengthMeters { get; set; } = 0.60d; - - /// 控制柄相对于窗口局部弦长的比例。 - public double HandleLengthRatio { get; set; } = 1d / 3d; -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs deleted file mode 100644 index 7bc1e64..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 局部 G2 五次过渡的可配置阈值。 -public sealed class LocalG2QuinticOptions -{ - public double MinimumWindowLengthMeters { get; set; } = 0.20d; - public double PreferredWindowLengthMeters { get; set; } = 0.50d; - public double MaximumWindowLengthMeters { get; set; } = 0.80d; - public double MaximumDeviationMeters { get; set; } = 0.10d; - public double AbsoluteCurvatureJumpFloorPerMeter { get; set; } = 0.001d; - public double CurvatureJumpRatioOfMaximum { get; set; } = 0.05d; - public double MinimumPeakGradientImprovementRatio { get; set; } = 0.20d; - public double MaximumVariationCostRegressionRatio { get; set; } = 0.02d; - public int MaximumCandidatesPerRegion { get; set; } = 12; -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs deleted file mode 100644 index 251b387..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 一条原始或平滑路径的不可变质量指标。 -public sealed class PathQualityMetrics -{ - /// 创建全零、不可行的质量指标。 - public PathQualityMetrics() - : this(false, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d) - { - } - - /// 创建完整的质量指标快照。 - public PathQualityMetrics( - bool isFeasible, - double pathLengthMeters, - double maximumAbsoluteVehicleCurvaturePerMeter, - double rootMeanSquareVehicleCurvaturePerMeter, - double totalAbsoluteCurvatureVariationPerMeter, - double curvatureVariationEnergy, - double minimumBodyClearanceMeters, - double lengthChangePercent, - double peakCurvatureChangePercent, - double curvatureVariationChangePercent, - double minimumClearanceChangeMeters) - : this( - isFeasible, - pathLengthMeters, - maximumAbsoluteVehicleCurvaturePerMeter, - 0d, - rootMeanSquareVehicleCurvaturePerMeter, - totalAbsoluteCurvatureVariationPerMeter, - curvatureVariationEnergy, - minimumBodyClearanceMeters, - lengthChangePercent, - peakCurvatureChangePercent, - curvatureVariationChangePercent, - minimumClearanceChangeMeters) - { - } - - /// 创建带有曲率导数峰值的完整质量指标快照。 - public PathQualityMetrics( - bool isFeasible, - double pathLengthMeters, - double maximumAbsoluteVehicleCurvaturePerMeter, - double maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - double rootMeanSquareVehicleCurvaturePerMeter, - double totalAbsoluteCurvatureVariationPerMeter, - double curvatureVariationEnergy, - double minimumBodyClearanceMeters, - double lengthChangePercent, - double peakCurvatureChangePercent, - double curvatureVariationChangePercent, - double minimumClearanceChangeMeters) - { - IsFeasible = isFeasible; - PathLengthMeters = pathLengthMeters; - MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; - MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter = maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter; - RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter; - TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter; - CurvatureVariationEnergy = curvatureVariationEnergy; - MinimumBodyClearanceMeters = minimumBodyClearanceMeters; - LengthChangePercent = lengthChangePercent; - PeakCurvatureChangePercent = peakCurvatureChangePercent; - CurvatureVariationChangePercent = curvatureVariationChangePercent; - MinimumClearanceChangeMeters = minimumClearanceChangeMeters; - } - - /// 该路径是否通过完整安全和运动学复核。 - public bool IsFeasible { get; } - - /// 路径总弧长,单位 m。 - public double PathLengthMeters { get; } - - /// 绝对车辆曲率峰值,单位 1/m。 - public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } - - /// 绝对车辆曲率导数峰值,单位 1/m²。 - public double MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter { get; } - - /// 车辆曲率均方根,单位 1/m。 - public double RootMeanSquareVehicleCurvaturePerMeter { get; } - - /// 逐方向段累加的绝对曲率变化,单位 1/m。 - public double TotalAbsoluteCurvatureVariationPerMeter { get; } - - /// 逐方向段计算的曲率变化能量。 - public double CurvatureVariationEnergy { get; } - - /// 曲率变化代价的兼容名称。 - public double CurvatureVariationCost => CurvatureVariationEnergy; - - /// 完整扩大车体的最小保守净空,单位 m。 - public double MinimumBodyClearanceMeters { get; } - - /// 相对原始粗路径的长度变化百分比。 - public double LengthChangePercent { get; } - - /// 相对原始粗路径的峰值曲率变化百分比。 - public double PeakCurvatureChangePercent { get; } - - /// 相对原始粗路径的曲率变化百分比。 - public double CurvatureVariationChangePercent { get; } - - /// 相对原始粗路径的最小净空变化,单位 m。 - public double MinimumClearanceChangeMeters { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs deleted file mode 100644 index 013980b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 路径平滑的公共配置;所有距离使用 m。 -public sealed class PathSmoothingConfiguration -{ - /// 创建带有安全默认值的平滑配置。 - public PathSmoothingConfiguration() - { - OutputSpacingMeters = 0.025d; - MaximumCollisionCheckStepMeters = 0.025d; - MinimumClearanceReserveMeters = 0.02d; - SmoothingStrength = 1d; - AllowFallbackToCoarsePath = true; - RetryStrengthScales = new ReadOnlyCollection( - new List { 1d, 0.75d, 0.50d, 0.25d }); - } - - /// 正式单算法入口使用的方法。 - public SmoothingMethod Method { get; set; } - - /// 输出路径的目标弧长采样间距,单位 m。 - public double OutputSpacingMeters { get; set; } - - /// 扫掠碰撞检查的最大步长,单位 m。 - public double MaximumCollisionCheckStepMeters { get; set; } - - /// 平滑候选必须在最小净空之外保留的额外余量,单位 m。 - public double MinimumClearanceReserveMeters { get; set; } - - /// 算法初始平滑强度。 - public double SmoothingStrength { get; set; } - - /// 所有平滑尝试失败时是否允许发布经过复核的原粗路径。 - public bool AllowFallbackToCoarsePath { get; set; } - - /// 有限且严格递减的平滑强度重试比例。 - public IReadOnlyList RetryStrengthScales { get; } - - /// 三次 B 样条专用参数。 - public CubicBSplineOptions CubicBSpline { get; } = new CubicBSplineOptions(); - - /// 局部三次 Bézier 专用参数。 - public LocalCubicBezierOptions LocalCubicBezier { get; } = new LocalCubicBezierOptions(); - - /// 分段五次多项式专用参数。 - public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions(); - - /// 局部 G2 五次过渡专用参数。 - public LocalG2QuinticOptions LocalG2Quintic { get; } = new LocalG2QuinticOptions(); -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs deleted file mode 100644 index 7f49d76..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 一次平滑尝试的不可变诊断信息。 -public sealed class PathSmoothingDiagnostics -{ - /// 创建不含路径指标的默认诊断信息。 - public PathSmoothingDiagnostics() - : this(new PathQualityMetrics(), TimeSpan.Zero, 0, 0d, string.Empty) - { - } - - /// 创建完整的平滑诊断快照。 - public PathSmoothingDiagnostics( - PathQualityMetrics metrics, - TimeSpan elapsed, - int retryCount, - double acceptedStrength, - string terminationReason = null) - { - Metrics = metrics ?? new PathQualityMetrics(); - Elapsed = elapsed; - RetryCount = retryCount; - AcceptedStrength = acceptedStrength; - TerminationReason = terminationReason ?? string.Empty; - } - - /// 使用所有测量量创建平滑诊断快照。 - public PathSmoothingDiagnostics( - bool isFeasible, - double pathLengthMeters, - double maximumAbsoluteVehicleCurvaturePerMeter, - double rootMeanSquareVehicleCurvaturePerMeter, - double totalAbsoluteCurvatureVariationPerMeter, - double curvatureVariationEnergy, - double minimumBodyClearanceMeters, - double lengthChangePercent, - double peakCurvatureChangePercent, - double curvatureVariationChangePercent, - double minimumClearanceChangeMeters, - TimeSpan elapsed, - int retryCount, - double acceptedStrength, - string terminationReason = null) - : this( - new PathQualityMetrics( - isFeasible, - pathLengthMeters, - maximumAbsoluteVehicleCurvaturePerMeter, - rootMeanSquareVehicleCurvaturePerMeter, - totalAbsoluteCurvatureVariationPerMeter, - curvatureVariationEnergy, - minimumBodyClearanceMeters, - lengthChangePercent, - peakCurvatureChangePercent, - curvatureVariationChangePercent, - minimumClearanceChangeMeters), - elapsed, - retryCount, - acceptedStrength, - terminationReason) - { - } - - /// 路径质量指标;始终非空。 - public PathQualityMetrics Metrics { get; } - - /// 从算法入口到返回诊断的耗时。 - public TimeSpan Elapsed { get; } - - /// 已执行的安全强度重试次数。 - public int RetryCount { get; } - - /// 通过复核的平滑强度;未接受候选时为零。 - public double AcceptedStrength { get; } - - /// 面向调用方的稳定终止说明。 - public string TerminationReason { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs deleted file mode 100644 index 91b5bf2..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 局部 G2 区域未替换原始路径的稳定原因。 -public enum PathSmoothingRegionFailureReason -{ - None, - WindowUnavailable, - CandidateGenerationFailed, - Collision, - InsufficientClearance, - CurvatureExceeded, - CurvatureOvershoot, - DeviationExceeded, - InsufficientImprovement, - VariationCostRegression, - GlobalValidationRollback, -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs deleted file mode 100644 index 4004314..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 单个局部 G2 平滑区域的不可变发布报告。 -public sealed class PathSmoothingRegionReport -{ - public PathSmoothingRegionReport( - int segmentIndex, - double startArcLengthMeters, - double endArcLengthMeters, - IReadOnlyList curvatureJumpsPerMeter, - double plannedWindowLengthMeters, - double actualWindowLengthMeters, - double leftWindowLengthMeters, - double rightWindowLengthMeters, - int candidateCount, - int selectedCandidateIndex, - PathSmoothingRegionStatus status, - PathSmoothingRegionFailureReason failureReason, - double rawPeakCurvatureDerivativePerSquareMeter, - double resultPeakCurvatureDerivativePerSquareMeter, - double rawCurvatureVariationCost, - double resultCurvatureVariationCost, - double maximumDeviationMeters, - double minimumBodyClearanceMeters, - double maximumAbsoluteVehicleCurvaturePerMeter) - { - SegmentIndex = segmentIndex; - StartArcLengthMeters = startArcLengthMeters; - EndArcLengthMeters = endArcLengthMeters; - CurvatureJumpsPerMeter = CopyReadOnly(curvatureJumpsPerMeter); - PlannedWindowLengthMeters = plannedWindowLengthMeters; - ActualWindowLengthMeters = actualWindowLengthMeters; - LeftWindowLengthMeters = leftWindowLengthMeters; - RightWindowLengthMeters = rightWindowLengthMeters; - CandidateCount = candidateCount; - SelectedCandidateIndex = status == PathSmoothingRegionStatus.Improved - ? selectedCandidateIndex - : -1; - Status = status; - FailureReason = failureReason; - RawPeakCurvatureDerivativePerSquareMeter = rawPeakCurvatureDerivativePerSquareMeter; - ResultPeakCurvatureDerivativePerSquareMeter = resultPeakCurvatureDerivativePerSquareMeter; - RawCurvatureVariationCost = rawCurvatureVariationCost; - ResultCurvatureVariationCost = resultCurvatureVariationCost; - MaximumDeviationMeters = maximumDeviationMeters; - MinimumBodyClearanceMeters = minimumBodyClearanceMeters; - MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; - } - - public int SegmentIndex { get; } - public double StartArcLengthMeters { get; } - public double EndArcLengthMeters { get; } - public IReadOnlyList CurvatureJumpsPerMeter { get; } - public double PlannedWindowLengthMeters { get; } - public double ActualWindowLengthMeters { get; } - public double LeftWindowLengthMeters { get; } - public double RightWindowLengthMeters { get; } - public int CandidateCount { get; } - public int SelectedCandidateIndex { get; } - public PathSmoothingRegionStatus Status { get; } - public PathSmoothingRegionFailureReason FailureReason { get; } - public double RawPeakCurvatureDerivativePerSquareMeter { get; } - public double ResultPeakCurvatureDerivativePerSquareMeter { get; } - public double RawCurvatureVariationCost { get; } - public double ResultCurvatureVariationCost { get; } - public double MaximumDeviationMeters { get; } - public double MinimumBodyClearanceMeters { get; } - public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) - copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs deleted file mode 100644 index 752d3ef..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 单个局部 G2 平滑区域的处理结果。 -public enum PathSmoothingRegionStatus -{ - Improved, - RetainedOriginal, -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs deleted file mode 100644 index ebe8106..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 路径平滑所需的原始粗路径、复核上下文和配置。 -public sealed class PathSmoothingRequest -{ - private readonly VehicleParameters _vehicle; - private readonly PathSmoothingConfiguration _configuration; - - /// 创建路径平滑请求,并复制粗路径和方向分段集合。 - public PathSmoothingRequest( - IReadOnlyList coarsePath, - IReadOnlyList segments, - PlanningGridMap map, - VehicleParameters vehicle, - PathSmoothingConfiguration configuration) - { - CoarsePath = CopyReadOnly(coarsePath); - Segments = CopyReadOnly(segments); - Map = map; - _vehicle = CopyVehicle(vehicle); - _configuration = CopyConfiguration(configuration); - } - - /// 原始粗路径的不可变快照。 - public IReadOnlyList CoarsePath { get; } - - /// 原始粗路径方向分段的不可变快照。 - public IReadOnlyList Segments { get; } - - /// 用于平滑后完整车体复核的规划栅格地图。 - public PlanningGridMap Map { get; } - - /// 车辆几何与最大曲率约束的不可变快照副本。 - public VehicleParameters Vehicle => CopyVehicle(_vehicle); - - /// 本次平滑配置的不可变快照副本。 - public PathSmoothingConfiguration Configuration => CopyConfiguration(_configuration); - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) - copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } - - private static VehicleParameters CopyVehicle(VehicleParameters source) - { - if (source == null) return null; - return new VehicleParameters - { - LengthMeters = source.LengthMeters, - WidthMeters = source.WidthMeters, - SafetyMarginMeters = source.SafetyMarginMeters, - MaximumCurvaturePerMeter = source.MaximumCurvaturePerMeter, - MinimumTurningRadiusMeters = source.MinimumTurningRadiusMeters, - }; - } - - private static PathSmoothingConfiguration CopyConfiguration(PathSmoothingConfiguration source) - { - if (source == null) return null; - var copy = new PathSmoothingConfiguration - { - Method = source.Method, - OutputSpacingMeters = source.OutputSpacingMeters, - MaximumCollisionCheckStepMeters = source.MaximumCollisionCheckStepMeters, - MinimumClearanceReserveMeters = source.MinimumClearanceReserveMeters, - SmoothingStrength = source.SmoothingStrength, - AllowFallbackToCoarsePath = source.AllowFallbackToCoarsePath, - }; - copy.CubicBSpline.EndpointTangentScale = source.CubicBSpline.EndpointTangentScale; - copy.LocalCubicBezier.CornerHeadingThresholdRadians = source.LocalCubicBezier.CornerHeadingThresholdRadians; - copy.LocalCubicBezier.MaximumWindowLengthMeters = source.LocalCubicBezier.MaximumWindowLengthMeters; - copy.LocalCubicBezier.HandleLengthRatio = source.LocalCubicBezier.HandleLengthRatio; - copy.PiecewiseQuintic.KnotSpacingMeters = source.PiecewiseQuintic.KnotSpacingMeters; - copy.PiecewiseQuintic.MinimumKnotSpacingMeters = source.PiecewiseQuintic.MinimumKnotSpacingMeters; - copy.LocalG2Quintic.MinimumWindowLengthMeters = source.LocalG2Quintic.MinimumWindowLengthMeters; - copy.LocalG2Quintic.PreferredWindowLengthMeters = source.LocalG2Quintic.PreferredWindowLengthMeters; - copy.LocalG2Quintic.MaximumWindowLengthMeters = source.LocalG2Quintic.MaximumWindowLengthMeters; - copy.LocalG2Quintic.MaximumDeviationMeters = source.LocalG2Quintic.MaximumDeviationMeters; - copy.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = source.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter; - copy.LocalG2Quintic.CurvatureJumpRatioOfMaximum = source.LocalG2Quintic.CurvatureJumpRatioOfMaximum; - copy.LocalG2Quintic.MinimumPeakGradientImprovementRatio = source.LocalG2Quintic.MinimumPeakGradientImprovementRatio; - copy.LocalG2Quintic.MaximumVariationCostRegressionRatio = source.LocalG2Quintic.MaximumVariationCostRegressionRatio; - copy.LocalG2Quintic.MaximumCandidatesPerRegion = source.LocalG2Quintic.MaximumCandidatesPerRegion; - return copy; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs deleted file mode 100644 index d964caa..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 路径平滑的最终不可变结果。 -public sealed class PathSmoothingResult -{ - private static readonly IReadOnlyList EmptyPath = - new ReadOnlyCollection(new List()); - private static readonly IReadOnlyList EmptySegments = - new ReadOnlyCollection(new List()); - private static readonly IReadOnlyList EmptyRegionReports = - new ReadOnlyCollection(new List()); - - private PathSmoothingResult( - PathSmoothingStatus status, - SmoothingMethod? method, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics, - IReadOnlyList regionReports) - { - Status = status; - Method = method; - Path = path; - Segments = segments; - RegionReports = regionReports; - Diagnostics = diagnostics ?? new PathSmoothingDiagnostics( - new PathQualityMetrics(), - TimeSpan.Zero, - 0, - 0d, - "No smoothing diagnostics were supplied."); - } - - /// 最终发布状态。 - public PathSmoothingStatus Status { get; } - - /// 成功或回退时的实际(或尝试)平滑方法;失败时为空。 - public SmoothingMethod? Method { get; } - - /// 成功或经过复核的回退路径;其他状态始终为空且不可变。 - public IReadOnlyList Path { get; } - - /// 覆盖 的方向分段;其他状态始终为空且不可变。 - public IReadOnlyList Segments { get; } - - /// 局部 G2 各检测区域的不可变报告;传统算法结果为空。 - public IReadOnlyList RegionReports { get; } - - /// 本次平滑的质量和终止诊断;始终非空。 - public PathSmoothingDiagnostics Diagnostics { get; } - - /// 创建已通过所有复核的平滑结果。 - public static PathSmoothingResult Success( - SmoothingMethod method, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics) - { - ValidatePublishedResult(method, path, segments, diagnostics); - return new PathSmoothingResult( - PathSmoothingStatus.Success, - method, - CopyReadOnly(path), - CopyReadOnly(segments), - diagnostics, - EmptyRegionReports); - } - - /// 创建经过完整复核的原始粗路径回退结果。 - public static PathSmoothingResult Fallback( - SmoothingMethod attemptedMethod, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics) - { - ValidatePublishedResult(attemptedMethod, path, segments, diagnostics); - return new PathSmoothingResult( - PathSmoothingStatus.FallbackToCoarsePath, - attemptedMethod, - CopyReadOnly(path), - CopyReadOnly(segments), - diagnostics, - EmptyRegionReports); - } - - /// 发布经过完整复核的局部 G2 预平滑结果。 - public static PathSmoothingResult PublishLocalG2( - PathSmoothingStatus status, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics, - IReadOnlyList regionReports) - { - if (status != PathSmoothingStatus.Complete && - status != PathSmoothingStatus.PartialImprovement && - status != PathSmoothingStatus.NotNeeded && - status != PathSmoothingStatus.Unchanged) - throw new ArgumentException("Use a Local G2 publication status.", nameof(status)); - if (regionReports == null) - throw new ArgumentNullException(nameof(regionReports)); - - ValidatePublishedResult(SmoothingMethod.LocalG2Quintic, path, segments, diagnostics); - return new PathSmoothingResult( - status, - SmoothingMethod.LocalG2Quintic, - CopyReadOnly(path), - CopyReadOnly(segments), - diagnostics, - CopyReadOnly(regionReports)); - } - - /// 创建不发布路径的失败、不可行、取消或输入无效结果。 - public static PathSmoothingResult Failure(PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics) - { - if (status == PathSmoothingStatus.Success || - status == PathSmoothingStatus.FallbackToCoarsePath || - status == PathSmoothingStatus.Complete || - status == PathSmoothingStatus.PartialImprovement || - status == PathSmoothingStatus.NotNeeded || - status == PathSmoothingStatus.Unchanged) - throw new ArgumentException("Use Success or Fallback to publish a path.", nameof(status)); - if (!Enum.IsDefined(typeof(PathSmoothingStatus), status)) - throw new ArgumentOutOfRangeException(nameof(status)); - return new PathSmoothingResult(status, null, EmptyPath, EmptySegments, diagnostics, EmptyRegionReports); - } - - private static void ValidatePublishedResult( - SmoothingMethod method, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics) - { - if (!Enum.IsDefined(typeof(SmoothingMethod), method)) - throw new ArgumentOutOfRangeException(nameof(method)); - if (path == null || path.Count == 0) - throw new ArgumentException("Published smoothing results require a non-empty path.", nameof(path)); - if (segments == null || segments.Count == 0) - throw new ArgumentException("Published smoothing results require non-empty segments.", nameof(segments)); - if (diagnostics == null || diagnostics.Metrics == null || !diagnostics.Metrics.IsFeasible) - throw new ArgumentException("Published smoothing results require feasible diagnostics.", nameof(diagnostics)); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) - copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs deleted file mode 100644 index df1cdab..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 路径平滑的最终发布状态。 -public enum PathSmoothingStatus -{ - Success, - FallbackToCoarsePath, - InvalidInput, - Infeasible, - Failed, - Cancelled, - Complete, - PartialImprovement, - NotNeeded, - Unchanged, -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs deleted file mode 100644 index 4e45f06..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 分段五次 Hermite 平滑参数。 -public sealed class PiecewiseQuinticOptions -{ - /// 相邻内部结点的目标距离,单位 m。 - public double KnotSpacingMeters { get; set; } = 0.50d; - - /// 允许创建内部结点的最小间距,单位 m。 - public double MinimumKnotSpacingMeters { get; set; } = 0.10d; -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs deleted file mode 100644 index b0a3c8a..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs +++ /dev/null @@ -1,100 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.CoarsePath; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 平滑空间路径上的不可变采样点;位置和长度单位为 m,航向为 rad,曲率为 1/m。 -public sealed class SmoothedPathPoint -{ - public SmoothedPathPoint( - double xMeters, - double yMeters, - double headingRadians, - double unwrappedHeadingRadians, - double arcLengthMeters, - TravelDirection direction, - double geometricCurvaturePerMeter, - double vehicleCurvaturePerMeter, - double bodyClearanceMeters, - bool isGearSwitchPoint, - SmoothedPathPointSource source) - : this( - xMeters, - yMeters, - headingRadians, - unwrappedHeadingRadians, - arcLengthMeters, - direction, - geometricCurvaturePerMeter, - vehicleCurvaturePerMeter, - 0d, - bodyClearanceMeters, - isGearSwitchPoint, - source) - { - } - - /// 创建带有车辆曲率对弧长导数的不可变采样点。 - public SmoothedPathPoint( - double xMeters, - double yMeters, - double headingRadians, - double unwrappedHeadingRadians, - double arcLengthMeters, - TravelDirection direction, - double geometricCurvaturePerMeter, - double vehicleCurvaturePerMeter, - double vehicleCurvatureDerivativePerSquareMeter, - double bodyClearanceMeters, - bool isGearSwitchPoint, - SmoothedPathPointSource source) - { - X = xMeters; - Y = yMeters; - Heading = headingRadians; - UnwrappedHeading = unwrappedHeadingRadians; - ArcLength = arcLengthMeters; - Direction = direction; - GeometricCurvature = geometricCurvaturePerMeter; - VehicleCurvature = vehicleCurvaturePerMeter; - VehicleCurvatureDerivative = vehicleCurvatureDerivativePerSquareMeter; - BodyClearance = bodyClearanceMeters; - IsGearSwitchPoint = isGearSwitchPoint; - Source = source; - } - - /// 世界 X 坐标,单位 m。 - public double X { get; } - - /// 世界 Y 坐标,单位 m。 - public double Y { get; } - - /// 归一化的车辆航向,单位 rad。 - public double Heading { get; } - - /// 连续展开的车辆航向,单位 rad。 - public double UnwrappedHeading { get; } - - /// 从完整路径起点累计的弧长,单位 m。 - public double ArcLength { get; } - - /// 该点所在连续方向段的行驶方向。 - public TravelDirection Direction { get; } - - /// 按几何弧长计算的有符号路径曲率,单位 1/m。 - public double GeometricCurvature { get; } - - /// 车辆模型使用的有符号曲率,单位 1/m。 - public double VehicleCurvature { get; } - - /// 车辆曲率对弧长的导数 dκ/ds,单位 1/m²。 - public double VehicleCurvatureDerivative { get; } - - /// 扩大车体后的保守净空下界,单位 m。 - public double BodyClearance { get; } - - /// 该点是否为新方向段开始的换向点。 - public bool IsGearSwitchPoint { get; } - - /// 该点在平滑流程中的来源。 - public SmoothedPathPointSource Source { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs deleted file mode 100644 index 9469fc6..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 平滑路径点的来源。 -public enum SmoothedPathPointSource -{ - Anchor, - Interpolated, - GearSwitch, - CoarsePathFallback, - LocalG2Transition, -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs deleted file mode 100644 index 5ce1d95..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.CoarsePath; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 平滑路径中方向一致的连续点范围;起止索引均包含在内。 -public sealed class SmoothedPathSegment -{ - public SmoothedPathSegment( - int segmentIndex, - TravelDirection direction, - int startIndex, - int endIndex, - bool startsAtGearSwitch, - bool endsAtGearSwitch) - { - SegmentIndex = segmentIndex; - Direction = direction; - StartIndex = startIndex; - EndIndex = endIndex; - StartsAtGearSwitch = startsAtGearSwitch; - EndsAtGearSwitch = endsAtGearSwitch; - } - - /// 从零开始的分段序号。 - public int SegmentIndex { get; } - - /// 本段行驶方向。 - public TravelDirection Direction { get; } - - /// 本段在平滑路径中的起始包含式索引。 - public int StartIndex { get; } - - /// 本段在平滑路径中的结束包含式索引。 - public int EndIndex { get; } - - /// 本段首点是否为换向后保留的新方向点。 - public bool StartsAtGearSwitch { get; } - - /// 本段末点是否紧邻下一方向段的换向对。 - public bool EndsAtGearSwitch { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs deleted file mode 100644 index 890e02f..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -/// 支持的粗路径平滑方法。 -public enum SmoothingMethod -{ - CubicBSpline, - LocalCubicBezier, - PiecewiseQuintic, - LocalG2Quintic, -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs deleted file mode 100644 index 91521bc..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; - -/// 以固定预热和五次测量隔离比较所有请求平滑方法的离线入口。 -public sealed class PathSmoothingComparisonService -{ - private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); - private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); - private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); - private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); - - /// 比较所有请求方法;一个方法的失败不会阻止其他方法,取消会停止后续启动。 - public PathSmoothingComparisonResult Compare( - PathSmoothingComparisonRequest request, - CancellationToken cancellationToken = default) - { - PathSmoothingComparisonEntry baseline = CreateRawPathBaseline(request, out string baselineReason); - var entries = new List(); - - if (cancellationToken.IsCancellationRequested) - return Cancelled(baseline, entries); - - if (request == null || request.SmoothingRequest == null) - return new PathSmoothingComparisonResult(baseline, entries, null, false, baselineReason); - - for (int methodIndex = 0; methodIndex < request.Methods.Count; methodIndex++) - { - if (cancellationToken.IsCancellationRequested) - return Cancelled(baseline, entries); - - SmoothingMethod method = request.Methods[methodIndex]; - if (!TryCompareMethod( - request.SmoothingRequest, - method, - baseline.Metrics, - cancellationToken, - out PathSmoothingComparisonEntry entry)) - return Cancelled(baseline, entries); - entries.Add(entry); - } - - return new PathSmoothingComparisonResult( - baseline, - entries, - SmoothingMethodRanker.Rank(entries), - false, - baselineReason); - } - - private bool TryCompareMethod( - PathSmoothingRequest sourceRequest, - SmoothingMethod method, - PathQualityMetrics rawMetrics, - CancellationToken cancellationToken, - out PathSmoothingComparisonEntry entry) - { - entry = null; - try - { - PathSmoothingRequest methodRequest = CreateMethodRequest(sourceRequest, method); - cancellationToken.ThrowIfCancellationRequested(); - PathSmoothingResult warmup = _smoothingService.Smooth(methodRequest, cancellationToken); - if (warmup.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; - - var timings = new List(5); - var measuredResults = new List(5); - for (int sampleIndex = 0; sampleIndex < 5; sampleIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - Stopwatch stopwatch = Stopwatch.StartNew(); - PathSmoothingResult result = _smoothingService.Smooth(methodRequest, cancellationToken); - stopwatch.Stop(); - if (result.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; - timings.Add(stopwatch.Elapsed.TotalMilliseconds); - measuredResults.Add(result); - } - - PathSmoothingResult canonical = measuredResults[0]; - string digest = StableGeometryDigest.Compute(canonical); - SmoothingTimingSummary timing = SmoothingTimingSummary.FromMeasurements(timings, measuredResults); - string diagnostic = string.IsNullOrWhiteSpace(timing.Diagnostic) - ? canonical.Diagnostics.TerminationReason - : timing.Diagnostic; - - PathQualityMetrics metrics = canonical.Status == PathSmoothingStatus.Success - ? NormalizeMetrics(canonical.Diagnostics.Metrics, rawMetrics) - : new PathQualityMetrics(); - entry = PathSmoothingComparisonEntry.CreateCandidate( - method, - canonical.Status, - metrics, - timing, - digest, - diagnostic, - canonical.Diagnostics.RetryCount, - canonical.Diagnostics.AcceptedStrength, - canonical.Path, - canonical.Segments); - return true; - } - catch (OperationCanceledException) - { - return false; - } - catch (Exception exception) - { - entry = PathSmoothingComparisonEntry.CreateCandidate( - method, - PathSmoothingStatus.Failed, - new PathQualityMetrics(), - new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, exception.GetType().Name), - string.Empty, - exception.Message, - 0, - 0d, - null, - null); - return true; - } - } - - private PathSmoothingComparisonEntry CreateRawPathBaseline( - PathSmoothingComparisonRequest request, - out string reason) - { - reason = string.Empty; - if (request == null || request.SmoothingRequest == null) - return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求为空。", out reason); - - PathSmoothingRequest smoothingRequest = request.SmoothingRequest; - PathSmoothingConfiguration configuration = smoothingRequest.Configuration; - if (configuration == null || smoothingRequest.Map == null || smoothingRequest.Vehicle == null) - return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求缺少可用的地图、车辆或配置。", out reason); - - if (!_preprocessor.TryPrepare(smoothingRequest, out PreparedPath preparedPath, out reason)) - return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); - if (!RawPathBaselineBuilder.TryCreate( - smoothingRequest, - preparedPath, - _analyzer, - configuration.OutputSpacingMeters, - _validator, - configuration.MaximumCollisionCheckStepMeters, - out RawPathBaseline rawPath, - out reason)) - return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); - - string digest = StableGeometryDigest.Compute(PathSmoothingStatus.Success, null, rawPath.Path, rawPath.Segments); - return PathSmoothingComparisonEntry.CreateRawPathBaseline( - PathSmoothingStatus.Success, - rawPath.Metrics, - digest, - string.Empty, - rawPath.Path, - rawPath.Segments); - } - - private static PathSmoothingComparisonEntry FailedBaseline( - PathSmoothingStatus status, - string failureReason, - out string reason) - { - reason = failureReason ?? string.Empty; - return PathSmoothingComparisonEntry.CreateRawPathBaseline( - status, - new PathQualityMetrics(), - StableGeometryDigest.Compute(status, null, null, null), - reason, - null, - null); - } - - private static PathSmoothingComparisonResult Cancelled( - PathSmoothingComparisonEntry baseline, - IReadOnlyList entries) - { - return new PathSmoothingComparisonResult(baseline, entries, null, true, "路径平滑比较已取消。"); - } - - private static PathSmoothingRequest CreateMethodRequest(PathSmoothingRequest source, SmoothingMethod method) - { - PathSmoothingConfiguration configuration = source.Configuration; - configuration.Method = method; - configuration.AllowFallbackToCoarsePath = false; - return new PathSmoothingRequest( - source.CoarsePath, - source.Segments, - source.Map, - source.Vehicle, - configuration); - } - - private static PathQualityMetrics NormalizeMetrics( - PathQualityMetrics candidate, - PathQualityMetrics raw) - { - if (candidate == null || raw == null || !candidate.IsFeasible) - return new PathQualityMetrics(); - - return new PathQualityMetrics( - true, - candidate.PathLengthMeters, - candidate.MaximumAbsoluteVehicleCurvaturePerMeter, - candidate.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - candidate.RootMeanSquareVehicleCurvaturePerMeter, - candidate.TotalAbsoluteCurvatureVariationPerMeter, - candidate.CurvatureVariationEnergy, - candidate.MinimumBodyClearanceMeters, - RelativePercentOrAbsoluteDelta(candidate.PathLengthMeters, raw.PathLengthMeters), - RelativePercentOrAbsoluteDelta( - candidate.MaximumAbsoluteVehicleCurvaturePerMeter, - raw.MaximumAbsoluteVehicleCurvaturePerMeter), - RelativePercentOrAbsoluteDelta( - candidate.TotalAbsoluteCurvatureVariationPerMeter, - raw.TotalAbsoluteCurvatureVariationPerMeter), - candidate.MinimumBodyClearanceMeters - raw.MinimumBodyClearanceMeters); - } - - private static double RelativePercentOrAbsoluteDelta(double candidate, double raw) - { - double delta = candidate - raw; - return Math.Abs(raw) < 1e-12d ? delta : delta / raw * 100d; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs deleted file mode 100644 index 5b1db56..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; - -/// 正式单算法路径平滑入口,负责输入校验、有限重试与经过复核的粗路径回退。 -public sealed class PathSmoothingService -{ - private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); - private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); - private readonly SmoothingAlgorithmRunner _runner = new SmoothingAlgorithmRunner(); - private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); - private readonly IPathSmoother _bSpline = new CubicBSplineSmoother(); - private readonly IPathSmoother _bezier = new LocalCubicBezierSmoother(); - private readonly IPathSmoother _quintic = new PiecewiseQuinticSmoother(); - private readonly LocalG2PreSmoothingPipeline _localG2Pipeline = new LocalG2PreSmoothingPipeline(); - - /// 执行一次经过完整安全复核的单算法平滑。 - public PathSmoothingResult Smooth( - PathSmoothingRequest request, - CancellationToken cancellationToken = default) - { - var stopwatch = Stopwatch.StartNew(); - try - { - cancellationToken.ThrowIfCancellationRequested(); - if (!TryValidateRequest(request, out PathSmoothingConfiguration configuration, out string reason)) - return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); - - if (!_preprocessor.TryPrepare(request, out PreparedPath preparedPath, out reason)) - return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); - - if (!RawPathBaselineBuilder.TryCreate( - request, - preparedPath, - _analyzer, - configuration.OutputSpacingMeters, - _validator, - configuration.MaximumCollisionCheckStepMeters, - out RawPathBaseline rawBaseline, - out reason)) - { - return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); - } - - if (configuration.Method == SmoothingMethod.LocalG2Quintic) - return _localG2Pipeline.Smooth(request, preparedPath, rawBaseline, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - var input = new SmoothingAlgorithmInput( - preparedPath, - request.Map, - request.Vehicle, - configuration.MaximumCollisionCheckStepMeters, - configuration.MinimumClearanceReserveMeters, - new SmoothingOptionsSnapshot(configuration)); - SmoothingAlgorithmRunner.AlgorithmRunResult runResult = _runner.Run( - Resolve(configuration.Method), input, configuration, cancellationToken); - int retryCount = GetRetryCount(runResult.AttemptedStrengths); - PathSmoothingDiagnostics diagnostics = new PathSmoothingDiagnostics( - runResult.Metrics, - stopwatch.Elapsed, - retryCount, - runResult.AcceptedStrength, - runResult.Reason); - - if (runResult.Status == PathSmoothingStatus.Success) - { - return PathSmoothingResult.Success( - configuration.Method, - runResult.Path, - runResult.Segments, - diagnostics); - } - - if (!configuration.AllowFallbackToCoarsePath) - return PathSmoothingResult.Failure(runResult.Status, diagnostics); - - cancellationToken.ThrowIfCancellationRequested(); - if (!TryCreateVerifiedFallback( - request, - configuration, - cancellationToken, - out IReadOnlyList fallbackPath, - out IReadOnlyList fallbackSegments, - out PathQualityMetrics fallbackMetrics, - out reason)) - { - return PathSmoothingResult.Failure(runResult.Status, diagnostics); - } - - var fallbackDiagnostics = new PathSmoothingDiagnostics( - fallbackMetrics, - stopwatch.Elapsed, - retryCount, - runResult.AcceptedStrength, - runResult.Reason); - return PathSmoothingResult.Fallback( - configuration.Method, - fallbackPath, - fallbackSegments, - fallbackDiagnostics); - } - catch (OperationCanceledException) - { - return Failure(PathSmoothingStatus.Cancelled, stopwatch, 0, 0d, "路径平滑已取消。"); - } - catch (Exception exception) - { - return Failure(PathSmoothingStatus.Failed, stopwatch, 0, 0d, exception.Message); - } - } - - private bool TryCreateVerifiedFallback( - PathSmoothingRequest request, - PathSmoothingConfiguration configuration, - CancellationToken cancellationToken, - out IReadOnlyList fallbackPath, - out IReadOnlyList fallbackSegments, - out PathQualityMetrics fallbackMetrics, - out string reason) - { - fallbackPath = null; - fallbackSegments = null; - fallbackMetrics = null; - reason = string.Empty; - - // Reprepare from the immutable request instead of reusing the algorithm input: fallback is a - // separately published output and must repeat the coarse-path contract validation. - if (!_preprocessor.TryPrepare(request, out PreparedPath revalidatedPath, out reason)) return false; - cancellationToken.ThrowIfCancellationRequested(); - - if (!RawPathBaselineBuilder.TryCreate( - request, - revalidatedPath, - _analyzer, - configuration.OutputSpacingMeters, - _validator, - configuration.MaximumCollisionCheckStepMeters, - out RawPathBaseline rawPath, - out reason)) - { - return false; - } - - fallbackPath = ToFallbackPoints(rawPath.Path); - fallbackSegments = rawPath.Segments; - fallbackMetrics = rawPath.Metrics; - return true; - } - - private IPathSmoother Resolve(SmoothingMethod method) - { - return method switch - { - SmoothingMethod.CubicBSpline => _bSpline, - SmoothingMethod.LocalCubicBezier => _bezier, - SmoothingMethod.PiecewiseQuintic => _quintic, - _ => throw new ArgumentOutOfRangeException(nameof(method)), - }; - } - - private static bool TryValidateRequest( - PathSmoothingRequest request, - out PathSmoothingConfiguration configuration, - out string reason) - { - configuration = null; - reason = string.Empty; - if (request == null) - { - reason = "平滑请求为空。"; - return false; - } - - configuration = request.Configuration; - VehicleParameters vehicle = request.Vehicle; - if (configuration == null || request.Map == null || !request.Map.PlanningReady || vehicle == null) - { - reason = "平滑请求缺少可用的地图、车辆或配置。"; - return false; - } - - if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || - !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || - !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out _)) - { - reason = "平滑请求中的车辆几何或曲率约束无效。"; - return false; - } - - if (!Enum.IsDefined(typeof(SmoothingMethod), configuration.Method)) - { - reason = "平滑方法无效。"; - return false; - } - - if (!NumericGuard.IsPositiveFinite(configuration.OutputSpacingMeters) || - !NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || - !NumericGuard.IsFinite(configuration.MinimumClearanceReserveMeters) || - configuration.MinimumClearanceReserveMeters < 0d || - !NumericGuard.IsPositiveFinite(configuration.SmoothingStrength)) - { - reason = "平滑配置包含非法数值或不满足方法契约。"; - return false; - } - - if (configuration.Method == SmoothingMethod.LocalG2Quintic) - return IsValidLocalG2Options(configuration.LocalG2Quintic, out reason); - - if ( - !NumericGuard.IsPositiveFinite(configuration.CubicBSpline.EndpointTangentScale) || - !IsValidBezierThreshold(configuration.LocalCubicBezier.CornerHeadingThresholdRadians) || - !NumericGuard.IsPositiveFinite(configuration.LocalCubicBezier.MaximumWindowLengthMeters) || - !NumericGuard.IsPositiveFinite(configuration.LocalCubicBezier.HandleLengthRatio) || - !NumericGuard.IsPositiveFinite(configuration.PiecewiseQuintic.KnotSpacingMeters) || - !NumericGuard.IsPositiveFinite(configuration.PiecewiseQuintic.MinimumKnotSpacingMeters) || - configuration.PiecewiseQuintic.KnotSpacingMeters < configuration.PiecewiseQuintic.MinimumKnotSpacingMeters) - { - reason = "平滑配置包含非法数值或不满足方法契约。"; - return false; - } - return true; - } - - private static bool IsValidLocalG2Options(LocalG2QuinticOptions options, out string reason) - { - reason = string.Empty; - if (options != null && - NumericGuard.IsPositiveFinite(options.MinimumWindowLengthMeters) && - NumericGuard.IsFinite(options.PreferredWindowLengthMeters) && - options.PreferredWindowLengthMeters >= options.MinimumWindowLengthMeters && - NumericGuard.IsFinite(options.MaximumWindowLengthMeters) && - options.MaximumWindowLengthMeters >= options.PreferredWindowLengthMeters && - NumericGuard.IsPositiveFinite(options.MaximumDeviationMeters) && - NumericGuard.IsPositiveFinite(options.AbsoluteCurvatureJumpFloorPerMeter) && - NumericGuard.IsFinite(options.CurvatureJumpRatioOfMaximum) && - options.CurvatureJumpRatioOfMaximum > 0d && options.CurvatureJumpRatioOfMaximum <= 1d && - NumericGuard.IsFinite(options.MinimumPeakGradientImprovementRatio) && - options.MinimumPeakGradientImprovementRatio > 0d && options.MinimumPeakGradientImprovementRatio < 1d && - NumericGuard.IsFinite(options.MaximumVariationCostRegressionRatio) && - options.MaximumVariationCostRegressionRatio >= 0d && options.MaximumCandidatesPerRegion >= 1) - { - return true; - } - - reason = "局部 G2 配置包含非法数值或不满足方法契约。"; - return false; - } - - private static bool IsValidBezierThreshold(double thresholdRadians) - { - return NumericGuard.IsFinite(thresholdRadians) && thresholdRadians > 0d && thresholdRadians <= Math.PI; - } - - private static IReadOnlyList ToFallbackPoints(IReadOnlyList path) - { - var points = new List(path.Count); - for (int index = 0; index < path.Count; index++) - { - SmoothedPathPoint point = path[index]; - points.Add(new SmoothedPathPoint( - point.X, - point.Y, - point.Heading, - point.UnwrappedHeading, - point.ArcLength, - point.Direction, - point.GeometricCurvature, - point.VehicleCurvature, - point.VehicleCurvatureDerivative, - point.BodyClearance, - point.IsGearSwitchPoint, - SmoothedPathPointSource.CoarsePathFallback)); - } - return points; - } - - private static int GetRetryCount(IReadOnlyList attemptedStrengths) - { - return attemptedStrengths == null || attemptedStrengths.Count == 0 ? 0 : attemptedStrengths.Count - 1; - } - - private static PathSmoothingResult Failure( - PathSmoothingStatus status, - Stopwatch stopwatch, - int retryCount, - double acceptedStrength, - string reason) - { - return PathSmoothingResult.Failure( - status, - new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, retryCount, acceptedStrength, reason)); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs deleted file mode 100644 index 671f3ca..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 同一方向原语边界两侧车辆曲率的离散跳变。 -internal sealed class CurvatureTransition -{ - internal CurvatureTransition( - int segmentIndex, - int leftCoarsePathIndex, - int rightCoarsePathIndex, - double localArcLengthMeters, - double x, - double y, - double vehicleHeadingRadians, - double leftVehicleCurvaturePerMeter, - double rightVehicleCurvaturePerMeter) - { - if (segmentIndex < 0 || leftCoarsePathIndex < 0 || rightCoarsePathIndex != leftCoarsePathIndex + 1) - throw new ArgumentOutOfRangeException(nameof(leftCoarsePathIndex)); - if (!NumericGuard.IsFinite(localArcLengthMeters) || localArcLengthMeters < 0d || - !NumericGuard.IsFinite(x) || !NumericGuard.IsFinite(y) || !NumericGuard.IsFinite(vehicleHeadingRadians) || - !NumericGuard.IsFinite(leftVehicleCurvaturePerMeter) || !NumericGuard.IsFinite(rightVehicleCurvaturePerMeter)) - { - throw new ArgumentOutOfRangeException(nameof(localArcLengthMeters)); - } - - double curvatureJumpPerMeter = Math.Abs(rightVehicleCurvaturePerMeter - leftVehicleCurvaturePerMeter); - if (!NumericGuard.IsFinite(curvatureJumpPerMeter)) - throw new ArgumentOutOfRangeException(nameof(rightVehicleCurvaturePerMeter)); - - SegmentIndex = segmentIndex; - LeftCoarsePathIndex = leftCoarsePathIndex; - RightCoarsePathIndex = rightCoarsePathIndex; - LocalArcLengthMeters = localArcLengthMeters; - X = x; - Y = y; - VehicleHeadingRadians = vehicleHeadingRadians; - LeftVehicleCurvaturePerMeter = leftVehicleCurvaturePerMeter; - RightVehicleCurvaturePerMeter = rightVehicleCurvaturePerMeter; - CurvatureJumpPerMeter = curvatureJumpPerMeter; - } - - internal int SegmentIndex { get; } - internal int LeftCoarsePathIndex { get; } - internal int RightCoarsePathIndex { get; } - internal double LocalArcLengthMeters { get; } - internal double X { get; } - internal double Y { get; } - internal double VehicleHeadingRadians { get; } - internal double LeftVehicleCurvaturePerMeter { get; } - internal double RightVehicleCurvaturePerMeter { get; } - internal double CurvatureJumpPerMeter { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs deleted file mode 100644 index 54d95ba..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 从原始粗路径的同向相邻点中识别恒曲率原语边界。 -internal sealed class CurvatureTransitionDetector -{ - internal bool TryDetect( - PathSmoothingRequest request, - double maximumVehicleCurvaturePerMeter, - LocalG2OptionsSnapshot options, - out IReadOnlyList transitions, - out string reason) - { - transitions = Empty(); - reason = string.Empty; - if (request == null || request.CoarsePath == null || request.Segments == null || options == null || - !NumericGuard.IsPositiveFinite(maximumVehicleCurvaturePerMeter)) - { - reason = "局部 G2 曲率事件检测输入无效。"; - return false; - } - if (!ValidatePath(request.CoarsePath, out reason) || !ValidateSegments(request.CoarsePath, request.Segments, out reason)) - return false; - - double threshold = Math.Max( - options.AbsoluteCurvatureJumpFloorPerMeter, - options.CurvatureJumpRatioOfMaximum * maximumVehicleCurvaturePerMeter); - if (!NumericGuard.IsPositiveFinite(threshold)) - { - reason = "局部 G2 曲率事件阈值无效。"; - return false; - } - - var detected = new List(); - for (int segmentPosition = 0; segmentPosition < request.Segments.Count; segmentPosition++) - { - PathSegment segment = request.Segments[segmentPosition]; - double segmentStartArc = request.CoarsePath[segment.StartIndex].ArcLength; - for (int leftIndex = segment.StartIndex; leftIndex < segment.EndIndex; leftIndex++) - { - CoarsePathPoint left = request.CoarsePath[leftIndex]; - CoarsePathPoint right = request.CoarsePath[leftIndex + 1]; - double delta = right.VehicleCurvature - left.VehicleCurvature; - if (!NumericGuard.IsFinite(delta)) - { - reason = "局部 G2 曲率事件跳变溢出。"; - return false; - } - if (Math.Abs(delta) >= threshold) - { - detected.Add(new CurvatureTransition( - segment.SegmentIndex, - leftIndex, - leftIndex + 1, - left.ArcLength - segmentStartArc, - left.X, - left.Y, - left.Heading, - left.VehicleCurvature, - right.VehicleCurvature)); - } - } - } - transitions = new ReadOnlyCollection(detected); - return true; - } - - /// 反射脚本使用的确定性检测与窗口规划接缝。 - public static class TestHooks - { - public static DetectionTestSnapshot Execute(string scenario) - { - if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario)); - switch (scenario) - { - case "SingleTransition": return DetectSingleTransition(); - case "GearSwitch": return DetectGearSwitch(); - case "Noise": return DetectNoise(); - case "Overlap": return PlanOverlap(); - case "NearStart": return PlanNearStart(); - default: throw new ArgumentOutOfRangeException(nameof(scenario)); - } - } - - public sealed class DetectionTestSnapshot - { - internal DetectionTestSnapshot(int transitionCount, double maximumJump, int regionCount, - int transitionCountInFirstRegion, double startArcLength, double leftWindowLength, double rightWindowLength) - { - TransitionCount = transitionCount; - MaximumJump = maximumJump; - RegionCount = regionCount; - TransitionCountInFirstRegion = transitionCountInFirstRegion; - StartArcLength = startArcLength; - LeftWindowLength = leftWindowLength; - RightWindowLength = rightWindowLength; - } - - public int TransitionCount { get; } - public double MaximumJump { get; } - public int RegionCount { get; } - public int TransitionCountInFirstRegion { get; } - public double StartArcLength { get; } - public double LeftWindowLength { get; } - public double RightWindowLength { get; } - } - - private static DetectionTestSnapshot DetectSingleTransition() - { - IReadOnlyList transitions = Detect(CreateRequest( - new[] { Point(0d, 0d), Point(0.1d, 0.4167d), Point(0.2d, 0.4167d) }, - new[] { new PathSegment(0, TravelDirection.Forward, 0, 2, false, false) })); - return Snapshot(transitions); - } - - private static DetectionTestSnapshot DetectGearSwitch() - { - IReadOnlyList transitions = Detect(CreateRequest( - new[] - { - Point(0d, 0d, TravelDirection.Forward), Point(0.2d, 0d, TravelDirection.Forward), - Point(0.2d, 0.4167d, TravelDirection.Reverse, true), Point(0.4d, 0.4167d, TravelDirection.Reverse), - }, - new[] - { - new PathSegment(0, TravelDirection.Forward, 0, 1, false, true), - new PathSegment(1, TravelDirection.Reverse, 2, 3, true, false), - })); - return Snapshot(transitions); - } - - private static DetectionTestSnapshot DetectNoise() - { - IReadOnlyList transitions = Detect(CreateRequest( - new[] { Point(0d, 0d), Point(0.1d, 0.01d), Point(0.2d, 0.01d) }, - new[] { new PathSegment(0, TravelDirection.Forward, 0, 2, false, false) })); - return Snapshot(transitions); - } - - private static DetectionTestSnapshot PlanOverlap() - { - LocalG2OptionsSnapshot options = CreateOptions(); - var transitions = new[] - { - new CurvatureTransition(0, 1, 2, 0.4d, 0.4d, 0d, 0d, 0d, 0.5d), - new CurvatureTransition(0, 2, 3, 0.7d, 0.7d, 0d, 0d, 0.5d, 0d), - }; - var planner = new LocalG2WindowPlanner(); - if (!planner.TryPlan(CreatePreparedPath(1.4d), transitions, options, out IReadOnlyList regions, out string reason)) - throw new InvalidOperationException(reason); - return new DetectionTestSnapshot(2, 0.5d, regions.Count, regions[0].Transitions.Count, 0d, 0d, 0d); - } - - private static DetectionTestSnapshot PlanNearStart() - { - LocalG2OptionsSnapshot options = CreateOptions(); - var transitions = new[] { new CurvatureTransition(0, 0, 1, 0.1d, 0.1d, 0d, 0d, 0d, 0.5d) }; - var planner = new LocalG2WindowPlanner(); - if (!planner.TryPlan(CreatePreparedPath(1d), transitions, options, out IReadOnlyList regions, out string reason)) - throw new InvalidOperationException(reason); - LocalG2WindowVariant first = regions[0].WindowVariants[0]; - return new DetectionTestSnapshot(1, 0.5d, regions.Count, 1, - first.StartArcLengthMeters, first.LeftWindowLengthMeters, first.RightWindowLengthMeters); - } - - private static IReadOnlyList Detect(PathSmoothingRequest request) - { - var detector = new CurvatureTransitionDetector(); - if (!detector.TryDetect(request, 0.8333d, CreateOptions(), out IReadOnlyList transitions, out string reason)) - throw new InvalidOperationException(reason); - return transitions; - } - - private static DetectionTestSnapshot Snapshot(IReadOnlyList transitions) - { - double maximum = 0d; - for (int index = 0; index < transitions.Count; index++) maximum = Math.Max(maximum, transitions[index].CurvatureJumpPerMeter); - return new DetectionTestSnapshot(transitions.Count, maximum, 0, 0, 0d, 0d, 0d); - } - - private static PathSmoothingRequest CreateRequest(IReadOnlyList points, IReadOnlyList segments) - { - return new PathSmoothingRequest(points, segments, null, null, new PathSmoothingConfiguration()); - } - - private static CoarsePathPoint Point(double arcLength, double curvature, TravelDirection direction = TravelDirection.Forward, bool gearSwitch = false) - { - return new CoarsePathPoint(arcLength, 0d, 0d, 0d, arcLength, direction, curvature, 1d, gearSwitch, - CoarsePathPointSource.MotionPrimitive); - } - - private static LocalG2OptionsSnapshot CreateOptions() => new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()); - - private static Processing.PreparedPath CreatePreparedPath(double length) - { - var points = new[] - { - new Processing.SmoothingPoint2D(0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new Processing.SmoothingPoint2D(length, 0d, length, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - var segment = new Processing.PreparedDirectionSegment(0, TravelDirection.Forward, points, false, false); - return new Processing.PreparedPath(new[] { segment }); - } - } - - private static bool ValidatePath(IReadOnlyList path, out string reason) - { - reason = string.Empty; - if (path.Count == 0) - { - reason = "局部 G2 曲率事件检测需要粗路径点。"; - return false; - } - double previousArc = -1d; - for (int index = 0; index < path.Count; index++) - { - CoarsePathPoint point = path[index]; - if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || - !NumericGuard.IsFinite(point.Heading) || !NumericGuard.IsFinite(point.ArcLength) || point.ArcLength < 0d || - !NumericGuard.IsFinite(point.VehicleCurvature) || point.ArcLength < previousArc) - { - reason = "局部 G2 曲率事件检测要求有限且非递减的粗路径。"; - return false; - } - previousArc = point.ArcLength; - } - return true; - } - - private static bool ValidateSegments(IReadOnlyList path, IReadOnlyList segments, out string reason) - { - reason = string.Empty; - for (int position = 0; position < segments.Count; position++) - { - PathSegment segment = segments[position]; - if (segment == null || segment.SegmentIndex != position || segment.StartIndex < 0 || - segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count) - { - reason = "局部 G2 曲率事件检测的方向分段无效。"; - return false; - } - for (int index = segment.StartIndex; index <= segment.EndIndex; index++) - { - if (path[index].Direction != segment.Direction) - { - reason = "局部 G2 曲率事件检测的方向分段包含换向点。"; - return false; - } - } - } - return true; - } - - private static IReadOnlyList Empty() => new ReadOnlyCollection(new List()); -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs deleted file mode 100644 index 2a9e207..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs +++ /dev/null @@ -1,1242 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 以固定窗口和尺度顺序构造有限的局部五次 Hermite G2 候选。 -internal sealed class LocalG2CandidateBuilder -{ - private const double MinimumDerivativeNorm = 1e-10d; - private const int MaximumDerivativeCertificationDepth = 40; - private const int MaximumDerivativeCertificationIntervals = 8192; - private const double DerivativeCertificationMargin = 1e-12d; - private const int MaximumSubdivisionDepth = 32; - private static readonly double[] DerivativeScaleMultipliers = { 1.00d, 1.20d, 1.40d, 1.60d }; - private static readonly double[] InternalCurvatureFractions = { 0d, 0.10d, 0.20d }; - - internal IReadOnlyList Build( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - LocalG2OptionsSnapshot options, - CancellationToken cancellationToken) - { - if (!IsValidInput(originalSegment, region, outputSpacingMeters, options)) return Empty(); - - var candidates = new List(); - for (int windowIndex = 0; windowIndex < region.WindowVariants.Count; windowIndex++) - { - LocalG2WindowVariant window = region.WindowVariants[windowIndex]; - bool hasInternalAnchor = HasInternalAnchor(region, window); - int curvatureCount = hasInternalAnchor ? InternalCurvatureFractions.Length : 1; - int scaleCount = hasInternalAnchor ? DerivativeScaleMultipliers.Length : 1; - for (int curvatureIndex = 0; curvatureIndex < curvatureCount; curvatureIndex++) - { - for (int scaleIndex = 0; scaleIndex < scaleCount; scaleIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - if (candidates.Count >= Math.Min(options.MaximumCandidatesPerRegion, 12)) return ReadOnly(candidates); - if (TryBuildCandidate( - curvatureIndex * scaleCount + scaleIndex, - originalSegment, - region, - window, - DerivativeScaleMultipliers[scaleIndex], - hasInternalAnchor ? InternalCurvatureFractions[curvatureIndex] : 0.5d, - outputSpacingMeters, - cancellationToken, - out LocalG2CandidateGeometry candidate)) - { - candidates.Add(candidate); - } - } - } - } - return ReadOnly(candidates); - } - - private static bool TryBuildCandidate( - int candidateIndex, - PreparedDirectionSegment segment, - LocalG2SmoothingRegion region, - LocalG2WindowVariant window, - double multiplier, - double internalCurvatureFraction, - double outputSpacingMeters, - CancellationToken cancellationToken, - out LocalG2CandidateGeometry candidate) - { - candidate = null; - if (!PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, window.StartArcLengthMeters, out SmoothingPoint2D startReference, out _) || - !PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, window.EndArcLengthMeters, out SmoothingPoint2D endReference, out _)) - { - return false; - } - - double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; - CurvatureTransition first = region.Transitions[0]; - CurvatureTransition last = region.Transitions[region.Transitions.Count - 1]; - bool firstAtStart = SameArc(first.LocalArcLengthMeters, window.StartArcLengthMeters); - bool lastAtEnd = SameArc(last.LocalArcLengthMeters, window.EndArcLengthMeters); - double startVehicleCurvature = firstAtStart && segment.StartVehicleCurvaturePerMeter.HasValue - ? segment.StartVehicleCurvaturePerMeter.Value - : first.LeftVehicleCurvaturePerMeter; - double endVehicleCurvature = last.RightVehicleCurvaturePerMeter; - if (!NumericGuard.IsFinite(startVehicleCurvature) || !NumericGuard.IsFinite(endVehicleCurvature)) return false; - - var nodes = new List(); - nodes.Add(new BoundaryNode(window.StartArcLengthMeters, startReference.X, startReference.Y, - startReference.Heading, startVehicleCurvature)); - for (int transitionIndex = 0; transitionIndex < region.Transitions.Count; transitionIndex++) - { - CurvatureTransition transition = region.Transitions[transitionIndex]; - bool atStart = SameArc(transition.LocalArcLengthMeters, window.StartArcLengthMeters); - bool atEnd = SameArc(transition.LocalArcLengthMeters, window.EndArcLengthMeters); - // A one-sided transition at a hard segment boundary has no left/right zero-length piece. - if (atStart || atEnd) continue; - double leftAvailable = transition.LocalArcLengthMeters - window.StartArcLengthMeters; - double rightAvailable = window.EndArcLengthMeters - transition.LocalArcLengthMeters; - double denominator = leftAvailable + rightAvailable; - if (!NumericGuard.IsPositiveFinite(denominator)) return false; - double sharedCurvature = transition.LeftVehicleCurvaturePerMeter + - (transition.RightVehicleCurvaturePerMeter - transition.LeftVehicleCurvaturePerMeter) * internalCurvatureFraction; - if (!NumericGuard.IsFinite(sharedCurvature)) return false; - nodes.Add(new BoundaryNode(transition.LocalArcLengthMeters, transition.X, transition.Y, - transition.VehicleHeadingRadians, sharedCurvature)); - } - nodes.Add(new BoundaryNode(window.EndArcLengthMeters, endReference.X, endReference.Y, - endReference.Heading, endVehicleCurvature)); - - if (!TryAssignDerivativeScales(nodes, multiplier) || nodes.Count < 2) return false; - var sampled = new List(); - for (int nodeIndex = 1; nodeIndex < nodes.Count; nodeIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - BoundaryNode left = nodes[nodeIndex - 1]; - BoundaryNode right = nodes[nodeIndex]; - if (!TryCreateCurve(left, right, directionSign, out QuinticHermiteCurve2D curve)) return false; - if (!TryAppendCurveSamples( - curve, - left, - right, - segment, - outputSpacingMeters, - sampled, - cancellationToken)) - { - return false; - } - } - - if (sampled.Count < 2) return false; - candidate = new LocalG2CandidateGeometry( - candidateIndex, - segment.SegmentIndex, - window.StartArcLengthMeters, - window.EndArcLengthMeters, - window.LeftWindowLengthMeters, - window.RightWindowLengthMeters, - sampled, - startVehicleCurvature, - endVehicleCurvature, - directionSign * startVehicleCurvature, - directionSign * endVehicleCurvature, - true); - return true; - } - - private static bool HasInternalAnchor(LocalG2SmoothingRegion region, LocalG2WindowVariant window) - { - for (int index = 0; index < region.Transitions.Count; index++) - { - double arc = region.Transitions[index].LocalArcLengthMeters; - if (!SameArc(arc, window.StartArcLengthMeters) && !SameArc(arc, window.EndArcLengthMeters)) return true; - } - return false; - } - - private static bool TryAssignDerivativeScales(List nodes, double multiplier) - { - if (!NumericGuard.IsPositiveFinite(multiplier)) return false; - for (int index = 0; index < nodes.Count; index++) - { - double scale; - if (index == 0) scale = nodes[1].ArcLengthMeters - nodes[0].ArcLengthMeters; - else if (index == nodes.Count - 1) scale = nodes[index].ArcLengthMeters - nodes[index - 1].ArcLengthMeters; - else scale = ((nodes[index].ArcLengthMeters - nodes[index - 1].ArcLengthMeters) + - (nodes[index + 1].ArcLengthMeters - nodes[index].ArcLengthMeters)) / 2d; - if (index > 0 && index < nodes.Count - 1) scale *= multiplier; - if (!NumericGuard.IsPositiveFinite(scale)) return false; - nodes[index] = nodes[index].WithDerivativeScale(scale); - } - return true; - } - - private static bool TryCreateCurve(BoundaryNode left, BoundaryNode right, double directionSign, out QuinticHermiteCurve2D curve) - { - curve = null; - if (!TryGetDerivatives(left, directionSign, out double ldx, out double ldy, out double lddx, out double lddy) || - !TryGetDerivatives(right, directionSign, out double rdx, out double rdy, out double rddx, out double rddy)) - { - return false; - } - return QuinticHermiteCurve2D.TryCreate(left.X, left.Y, ldx, ldy, lddx, lddy, - right.X, right.Y, rdx, rdy, rddx, rddy, out curve, out _); - } - - private static bool TryGetDerivatives(BoundaryNode node, double directionSign, - out double dx, out double dy, out double ddx, out double ddy) - { - dx = dy = ddx = ddy = 0d; - double travelHeading = directionSign > 0d ? node.VehicleHeadingRadians : node.VehicleHeadingRadians - Math.PI; - double geometricCurvature = directionSign * node.VehicleCurvaturePerMeter; - double tx = Math.Cos(travelHeading); - double ty = Math.Sin(travelHeading); - double nx = -ty; - double ny = tx; - dx = node.DerivativeScale * tx; - dy = node.DerivativeScale * ty; - ddx = node.DerivativeScale * node.DerivativeScale * geometricCurvature * nx; - ddy = node.DerivativeScale * node.DerivativeScale * geometricCurvature * ny; - return NumericGuard.IsFinite(dx) && NumericGuard.IsFinite(dy) && NumericGuard.IsFinite(ddx) && NumericGuard.IsFinite(ddy) && - Math.Sqrt(dx * dx + dy * dy) >= MinimumDerivativeNorm; - } - - private static bool TryAppendCurveSamples( - QuinticHermiteCurve2D curve, - BoundaryNode left, - BoundaryNode right, - PreparedDirectionSegment segment, - double outputSpacingMeters, - List output, - CancellationToken cancellationToken) - { - // A short endpoint chord and a finite sample grid cannot rule out an interior cusp. - // Certify a derivative-norm lower bound over the whole parameter interval first. - if (!TryCertifyDerivativeLowerBound(curve)) return false; - if (!TryEvaluate(curve, 0d, out CurveSample start) || !TryEvaluate(curve, 1d, out CurveSample end)) return false; - if (output.Count == 0 && !TryAppendPoint(start, left, right, segment, output)) return false; - return TrySubdivide(curve, left, right, segment, outputSpacingMeters, 0d, start, 1d, end, 0, output, cancellationToken); - } - - private static bool TrySubdivide( - QuinticHermiteCurve2D curve, - BoundaryNode left, - BoundaryNode right, - PreparedDirectionSegment segment, - double outputSpacingMeters, - double u0, - CurveSample sample0, - double u1, - CurveSample sample1, - int depth, - List output, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - if (Distance(sample0, sample1) <= outputSpacingMeters) - { - return TryAppendPoint(sample1, left, right, segment, output); - } - if (depth >= MaximumSubdivisionDepth) return false; - double middle = (u0 + u1) / 2d; - if (!TryEvaluate(curve, middle, out CurveSample middleSample)) return false; - return TrySubdivide(curve, left, right, segment, outputSpacingMeters, u0, sample0, middle, middleSample, - depth + 1, output, cancellationToken) && - TrySubdivide(curve, left, right, segment, outputSpacingMeters, middle, middleSample, u1, sample1, - depth + 1, output, cancellationToken); - } - - private static bool TryAppendPoint( - CurveSample sample, - BoundaryNode left, - BoundaryNode right, - PreparedDirectionSegment segment, - List output) - { - double referenceArcLength = left.ArcLengthMeters + (right.ArcLengthMeters - left.ArcLengthMeters) * sample.Parameter; - if (!PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, referenceArcLength, out SmoothingPoint2D reference, out _)) - return false; - double x = SameArc(sample.Parameter, 0d) ? left.X : SameArc(sample.Parameter, 1d) ? right.X : sample.X; - double y = SameArc(sample.Parameter, 0d) ? left.Y : SameArc(sample.Parameter, 1d) ? right.Y : sample.Y; - double travelHeading = Math.Atan2(sample.Dy, sample.Dx); - double vehicleHeading = segment.Direction == TravelDirection.Forward ? travelHeading : travelHeading + Math.PI; - if (!NumericGuard.IsFinite(x) || !NumericGuard.IsFinite(y) || !NumericGuard.IsFinite(travelHeading) || !NumericGuard.IsFinite(vehicleHeading)) return false; - if (output.Count > 0 && Distance(output[output.Count - 1], x, y) <= 1e-12d) return false; - output.Add(new SmoothingPoint2D( - x, - y, - referenceArcLength, - AngleMath.NormalizeRadians(vehicleHeading), - vehicleHeading, - reference.BodyClearance, - false, - SmoothedPathPointSource.LocalG2Transition)); - return true; - } - - private static bool TryEvaluate(QuinticHermiteCurve2D curve, double parameter, out CurveSample sample) - { - sample = default; - curve.Evaluate(parameter, out double x, out double y, out double dx, out double dy, out double ddx, out double ddy); - double derivativeNorm = Math.Sqrt(dx * dx + dy * dy); - if (!NumericGuard.IsFinite(x) || !NumericGuard.IsFinite(y) || !NumericGuard.IsFinite(dx) || !NumericGuard.IsFinite(dy) || - !NumericGuard.IsFinite(ddx) || !NumericGuard.IsFinite(ddy) || !NumericGuard.IsFinite(derivativeNorm) || - derivativeNorm < MinimumDerivativeNorm) - { - return false; - } - sample = new CurveSample(parameter, x, y, dx, dy); - return true; - } - - private static bool TryCertifyDerivativeLowerBound(QuinticHermiteCurve2D curve) - { - if (!TryCreateDerivativeBezierControls(curve, out DerivativeControlPoint d0, out DerivativeControlPoint d1, - out DerivativeControlPoint d2, out DerivativeControlPoint d3, out DerivativeControlPoint d4)) - { - return false; - } - int visitedIntervals = 0; - return TryCertifyDerivativeLowerBound(d0, d1, d2, d3, d4, 0, ref visitedIntervals); - } - - private static bool TryCreateDerivativeBezierControls( - QuinticHermiteCurve2D curve, - out DerivativeControlPoint d0, - out DerivativeControlPoint d1, - out DerivativeControlPoint d2, - out DerivativeControlPoint d3, - out DerivativeControlPoint d4) - { - d0 = d1 = d2 = d3 = d4 = default; - curve.Evaluate(0d, out _, out _, out double dx0, out double dy0, out double ddx0, out double ddy0); - curve.Evaluate(0.5d, out _, out _, out double dxMiddle, out double dyMiddle, out _, out _); - curve.Evaluate(1d, out _, out _, out double dxEnd, out double dyEnd, out double ddxEnd, out double ddyEnd); - d0 = new DerivativeControlPoint(dx0, dy0); - d1 = new DerivativeControlPoint(dx0 + ddx0 / 4d, dy0 + ddy0 / 4d); - d4 = new DerivativeControlPoint(dxEnd, dyEnd); - d3 = new DerivativeControlPoint(dxEnd - ddxEnd / 4d, dyEnd - ddyEnd / 4d); - d2 = new DerivativeControlPoint( - (16d * dxMiddle - d0.X - 4d * d1.X - 4d * d3.X - d4.X) / 6d, - (16d * dyMiddle - d0.Y - 4d * d1.Y - 4d * d3.Y - d4.Y) / 6d); - return d0.IsFinite && d1.IsFinite && d2.IsFinite && d3.IsFinite && d4.IsFinite; - } - - private static bool TryCertifyDerivativeLowerBound( - DerivativeControlPoint d0, - DerivativeControlPoint d1, - DerivativeControlPoint d2, - DerivativeControlPoint d3, - DerivativeControlPoint d4, - int depth, - ref int visitedIntervals) - { - if (++visitedIntervals > MaximumDerivativeCertificationIntervals) return false; - if (!TryGetOriginToConvexHullDistance(d0, d1, d2, d3, d4, out double lowerBound, out double margin)) - return false; - if (lowerBound > MinimumDerivativeNorm + margin) return true; - if (depth >= MaximumDerivativeCertificationDepth) return false; - - DerivativeControlPoint d01 = Midpoint(d0, d1); - DerivativeControlPoint d12 = Midpoint(d1, d2); - DerivativeControlPoint d23 = Midpoint(d2, d3); - DerivativeControlPoint d34 = Midpoint(d3, d4); - DerivativeControlPoint d012 = Midpoint(d01, d12); - DerivativeControlPoint d123 = Midpoint(d12, d23); - DerivativeControlPoint d234 = Midpoint(d23, d34); - DerivativeControlPoint d0123 = Midpoint(d012, d123); - DerivativeControlPoint d1234 = Midpoint(d123, d234); - DerivativeControlPoint middle = Midpoint(d0123, d1234); - if (!d01.IsFinite || !d12.IsFinite || !d23.IsFinite || !d34.IsFinite || !d012.IsFinite || !d123.IsFinite || - !d234.IsFinite || !d0123.IsFinite || !d1234.IsFinite || !middle.IsFinite) - { - return false; - } - - return TryCertifyDerivativeLowerBound(d0, d01, d012, d0123, middle, depth + 1, ref visitedIntervals) && - TryCertifyDerivativeLowerBound(middle, d1234, d234, d34, d4, depth + 1, ref visitedIntervals); - } - - private static bool TryGetOriginToConvexHullDistance( - DerivativeControlPoint d0, - DerivativeControlPoint d1, - DerivativeControlPoint d2, - DerivativeControlPoint d3, - DerivativeControlPoint d4, - out double distance, - out double margin) - { - distance = 0d; - margin = 0d; - var controls = new[] { d0, d1, d2, d3, d4 }; - double maximumCoordinate = 0d; - for (int index = 0; index < controls.Length; index++) - { - if (!controls[index].IsFinite) return false; - maximumCoordinate = Math.Max(maximumCoordinate, Math.Max(Math.Abs(controls[index].X), Math.Abs(controls[index].Y))); - } - if (!NumericGuard.IsFinite(maximumCoordinate)) return false; - margin = DerivativeCertificationMargin * Math.Max(1d, maximumCoordinate); - - for (int first = 0; first < controls.Length - 2; first++) - { - for (int second = first + 1; second < controls.Length - 1; second++) - { - for (int third = second + 1; third < controls.Length; third++) - { - if (ContainsOrigin(controls[first], controls[second], controls[third], margin)) return true; - } - } - } - - distance = double.PositiveInfinity; - for (int first = 0; first < controls.Length; first++) - { - distance = Math.Min(distance, controls[first].Norm); - for (int second = first + 1; second < controls.Length; second++) - distance = Math.Min(distance, DistanceToSegment(controls[first], controls[second])); - } - return NumericGuard.IsFinite(distance); - } - - private static bool ContainsOrigin(DerivativeControlPoint first, DerivativeControlPoint second, - DerivativeControlPoint third, double margin) - { - double sideX = second.X - first.X; - double sideY = second.Y - first.Y; - double thirdOffsetX = third.X - first.X; - double thirdOffsetY = third.Y - first.Y; - double triangleArea = sideX * thirdOffsetY - sideY * thirdOffsetX; - double crossFirstSecond = Cross(first, second); - double crossSecondThird = Cross(second, third); - double crossThirdFirst = Cross(third, first); - if (!NumericGuard.IsFinite(triangleArea) || !NumericGuard.IsFinite(crossFirstSecond) || - !NumericGuard.IsFinite(crossSecondThird) || !NumericGuard.IsFinite(crossThirdFirst)) - { - return true; - } - double areaMargin = margin * Math.Max(1d, Math.Max(first.Norm, Math.Max(second.Norm, third.Norm))); - if (!NumericGuard.IsFinite(areaMargin)) return true; - if (Math.Abs(triangleArea) <= areaMargin) - { - // A degenerate triangle is only a closed line segment (or a point), not a - // two-dimensional region. Treat it as origin-containing only when the origin - // lies on one of its actual closed segments; otherwise its distance is positive. - return IsOriginOnSegment(first, second, margin) || IsOriginOnSegment(second, third, margin) || - IsOriginOnSegment(third, first, margin); - } - return (crossFirstSecond >= -areaMargin && crossSecondThird >= -areaMargin && crossThirdFirst >= -areaMargin) || - (crossFirstSecond <= areaMargin && crossSecondThird <= areaMargin && crossThirdFirst <= areaMargin); - } - - private static bool IsOriginOnSegment(DerivativeControlPoint start, DerivativeControlPoint end, double margin) - { - double dx = end.X - start.X; - double dy = end.Y - start.Y; - double lengthSquared = dx * dx + dy * dy; - if (!NumericGuard.IsFinite(lengthSquared)) return true; - if (lengthSquared == 0d) return start.Norm <= margin; - double length = Math.Sqrt(lengthSquared); - double cross = Cross(start, end); - double projection = -(start.X * dx + start.Y * dy); - if (!NumericGuard.IsFinite(length) || !NumericGuard.IsFinite(cross) || !NumericGuard.IsFinite(projection)) return true; - double lineMargin = margin * Math.Max(1d, length); - double projectionMargin = margin * Math.Max(1d, length); - if (!NumericGuard.IsFinite(lineMargin) || !NumericGuard.IsFinite(projectionMargin)) return true; - return Math.Abs(cross) <= lineMargin && projection >= -projectionMargin && - projection <= lengthSquared + projectionMargin; - } - - private static double DistanceToSegment(DerivativeControlPoint start, DerivativeControlPoint end) - { - double dx = end.X - start.X; - double dy = end.Y - start.Y; - double denominator = dx * dx + dy * dy; - if (!NumericGuard.IsFinite(denominator)) return double.NaN; - if (denominator == 0d) return start.Norm; - double parameter = -(start.X * dx + start.Y * dy) / denominator; - if (!NumericGuard.IsFinite(parameter)) return double.NaN; - parameter = Math.Max(0d, Math.Min(1d, parameter)); - double x = start.X + parameter * dx; - double y = start.Y + parameter * dy; - return Math.Sqrt(x * x + y * y); - } - - private static DerivativeControlPoint Midpoint(DerivativeControlPoint left, DerivativeControlPoint right) => - new DerivativeControlPoint((left.X + right.X) / 2d, (left.Y + right.Y) / 2d); - private static double Cross(DerivativeControlPoint left, DerivativeControlPoint right) => left.X * right.Y - left.Y * right.X; - - private static bool IsValidInput(PreparedDirectionSegment segment, LocalG2SmoothingRegion region, - double outputSpacingMeters, LocalG2OptionsSnapshot options) - { - if (segment == null || region == null || options == null || segment.SegmentIndex != region.SegmentIndex || - !NumericGuard.IsPositiveFinite(outputSpacingMeters) || segment.Points == null || segment.Points.Count < 2 || - region.Transitions == null || region.Transitions.Count == 0 || region.WindowVariants == null) - { - return false; - } - for (int index = 0; index < region.Transitions.Count; index++) - { - CurvatureTransition transition = region.Transitions[index]; - if (transition == null || transition.SegmentIndex != segment.SegmentIndex) return false; - } - return true; - } - - private static double Distance(CurveSample left, CurveSample right) => Distance(left.X, left.Y, right.X, right.Y); - private static double Distance(SmoothingPoint2D point, double x, double y) => Distance(point.X, point.Y, x, y); - private static double Distance(double x0, double y0, double x1, double y1) - { - double dx = x1 - x0; - double dy = y1 - y0; - return Math.Sqrt(dx * dx + dy * dy); - } - private static bool SameArc(double left, double right) => Math.Abs(left - right) <= 1e-10d; - private static IReadOnlyList Empty() => new ReadOnlyCollection(new List()); - private static IReadOnlyList ReadOnly(List values) => - new ReadOnlyCollection(values); - - private readonly struct BoundaryNode - { - internal BoundaryNode(double arcLengthMeters, double x, double y, double vehicleHeadingRadians, double vehicleCurvaturePerMeter) - { - ArcLengthMeters = arcLengthMeters; - X = x; - Y = y; - VehicleHeadingRadians = vehicleHeadingRadians; - VehicleCurvaturePerMeter = vehicleCurvaturePerMeter; - DerivativeScale = 0d; - } - private BoundaryNode(double arcLengthMeters, double x, double y, double vehicleHeadingRadians, - double vehicleCurvaturePerMeter, double derivativeScale) - { - ArcLengthMeters = arcLengthMeters; - X = x; - Y = y; - VehicleHeadingRadians = vehicleHeadingRadians; - VehicleCurvaturePerMeter = vehicleCurvaturePerMeter; - DerivativeScale = derivativeScale; - } - internal double ArcLengthMeters { get; } - internal double X { get; } - internal double Y { get; } - internal double VehicleHeadingRadians { get; } - internal double VehicleCurvaturePerMeter { get; } - internal double DerivativeScale { get; } - internal BoundaryNode WithDerivativeScale(double derivativeScale) => new BoundaryNode( - ArcLengthMeters, X, Y, VehicleHeadingRadians, VehicleCurvaturePerMeter, derivativeScale); - } - - private readonly struct CurveSample - { - internal CurveSample(double parameter, double x, double y, double dx, double dy) - { - Parameter = parameter; - X = x; - Y = y; - Dx = dx; - Dy = dy; - } - internal double Parameter { get; } - internal double X { get; } - internal double Y { get; } - internal double Dx { get; } - internal double Dy { get; } - } - - private readonly struct DerivativeControlPoint - { - internal DerivativeControlPoint(double x, double y) - { - X = x; - Y = y; - } - internal double X { get; } - internal double Y { get; } - internal bool IsFinite => NumericGuard.IsFinite(X) && NumericGuard.IsFinite(Y); - internal double Norm => Math.Sqrt(X * X + Y * Y); - } - - /// 反射脚本使用的窄范围候选与拼接场景入口。 - public static class TestHooks - { - public static CandidateTestSnapshot Execute(string scenario) - { - if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario)); - switch (scenario) - { - case "Isolated": return BuildIsolated(); - case "Cluster": return BuildCluster(); - case "Reverse": return BuildReverse(); - case "Spliced": return BuildSpliced(); - case "StartBoundary": return BuildStartBoundary(); - case "InteriorStationaryCurve": return BuildInteriorStationaryCurve(); - case "ConstantVelocityCurve": return BuildConstantVelocityCurve(); - case "ExactSpliceEndpoints": return BuildExactSpliceEndpoints(); - case "GearBoundary": return BuildGearBoundary(); - case "TwoRegionWorkOrder": return BuildTwoRegionWorkOrder(); - case "MultiSegmentWorkOrder": return BuildMultiSegmentWorkOrder(); - default: throw new ArgumentOutOfRangeException(nameof(scenario)); - } - } - - public sealed class CandidateTestSnapshot - { - internal CandidateTestSnapshot(int candidateCount, double startPositionError, double endPositionError, - double startCurvatureError, double endCurvatureError, bool containsLocalG2Source, - int outputRegionCount, bool internalConnectionsAreG2, string direction, - bool vehicleAndGeometricCurvatureSignsAreOpposite, bool noDuplicateNonGearPoints, - bool endpointsUnchanged, bool rejected = false, bool endpointsAreExact = false, - bool gearBoundaryMarkerPreserved = false, bool accepted = false, - bool workOrderDescending = false, bool frontArcPreservedAfterBackReplacement = false, - bool bothReplacementsRetained = false, bool forwardOrderRejected = false, - bool deterministicWorkOrder = false, bool invalidWorkOrderRejected = false, - bool deterministicReplacementGeometry = false, bool segmentOrderAscending = false, - bool equalArcUsesReportOrder = false) - { - CandidateCount = candidateCount; - StartPositionError = startPositionError; - EndPositionError = endPositionError; - StartCurvatureError = startCurvatureError; - EndCurvatureError = endCurvatureError; - ContainsLocalG2Source = containsLocalG2Source; - OutputRegionCount = outputRegionCount; - InternalConnectionsAreG2 = internalConnectionsAreG2; - Direction = direction; - VehicleAndGeometricCurvatureSignsAreOpposite = vehicleAndGeometricCurvatureSignsAreOpposite; - NoDuplicateNonGearPoints = noDuplicateNonGearPoints; - EndpointsUnchanged = endpointsUnchanged; - Rejected = rejected; - EndpointsAreExact = endpointsAreExact; - GearBoundaryMarkerPreserved = gearBoundaryMarkerPreserved; - Accepted = accepted; - WorkOrderDescending = workOrderDescending; - FrontArcPreservedAfterBackReplacement = frontArcPreservedAfterBackReplacement; - BothReplacementsRetained = bothReplacementsRetained; - ForwardOrderRejected = forwardOrderRejected; - DeterministicWorkOrder = deterministicWorkOrder; - InvalidWorkOrderRejected = invalidWorkOrderRejected; - DeterministicReplacementGeometry = deterministicReplacementGeometry; - SegmentOrderAscending = segmentOrderAscending; - EqualArcUsesReportOrder = equalArcUsesReportOrder; - } - public int CandidateCount { get; } - public double StartPositionError { get; } - public double EndPositionError { get; } - public double StartCurvatureError { get; } - public double EndCurvatureError { get; } - public bool ContainsLocalG2Source { get; } - public int OutputRegionCount { get; } - public bool InternalConnectionsAreG2 { get; } - public string Direction { get; } - public bool VehicleAndGeometricCurvatureSignsAreOpposite { get; } - public bool NoDuplicateNonGearPoints { get; } - public bool EndpointsUnchanged { get; } - public bool Rejected { get; } - public bool EndpointsAreExact { get; } - public bool GearBoundaryMarkerPreserved { get; } - public bool Accepted { get; } - public bool WorkOrderDescending { get; } - public bool FrontArcPreservedAfterBackReplacement { get; } - public bool BothReplacementsRetained { get; } - public bool ForwardOrderRejected { get; } - public bool DeterministicWorkOrder { get; } - public bool InvalidWorkOrderRejected { get; } - public bool DeterministicReplacementGeometry { get; } - public bool SegmentOrderAscending { get; } - public bool EqualArcUsesReportOrder { get; } - } - - private static CandidateTestSnapshot BuildIsolated() - { - PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); - LocalG2SmoothingRegion region = CreateRegion(new[] - { - new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, 0d, 0d, 0.4d), - }, 0.25d, 0.75d); - IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( - segment, region, 0.025d, CreateOptions(), CancellationToken.None); - if (candidates.Count == 0) throw new InvalidOperationException("The isolated candidate scenario produced no candidate."); - LocalG2CandidateGeometry candidate = candidates[0]; - PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.25d, out SmoothingPoint2D start, out _); - PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.75d, out SmoothingPoint2D end, out _); - return new CandidateTestSnapshot(candidates.Count, - Distance(start, candidate.RegionPoints[0]), Distance(end, candidate.RegionPoints[candidate.RegionPoints.Count - 1]), - Math.Abs(candidate.StartVehicleCurvaturePerMeter - 0d), - Math.Abs(candidate.EndVehicleCurvaturePerMeter - 0.4d), - ContainsLocalG2(candidate.RegionPoints), 0, false, string.Empty, false, false, false); - } - - private static CandidateTestSnapshot BuildCluster() - { - PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); - var transitions = new[] - { - new CurvatureTransition(0, 1, 2, 0.4d, 0.4d, 0d, 0d, 0d, 0.4d), - new CurvatureTransition(0, 2, 3, 0.6d, 0.6d, 0d, 0d, 0.4d, 0d), - }; - var planner = new LocalG2WindowPlanner(); - if (!planner.TryPlan(new PreparedPath(new[] { segment }), transitions, CreateOptions(), - out IReadOnlyList regions, out string reason)) - { - throw new InvalidOperationException(reason); - } - IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( - segment, regions[0], 0.025d, CreateOptions(), CancellationToken.None); - if (candidates.Count == 0) throw new InvalidOperationException("The clustered candidate scenario produced no candidate."); - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, regions.Count, - candidates[0].InternalConnectionsAreG2, string.Empty, false, false, false); - } - - private static CandidateTestSnapshot BuildReverse() - { - PreparedDirectionSegment segment = CreateSegment(TravelDirection.Reverse, null); - LocalG2SmoothingRegion region = CreateRegion(new[] - { - new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, Math.PI, 0.2d, 0.4d), - }, 0.25d, 0.75d); - IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( - segment, region, 0.025d, CreateOptions(), CancellationToken.None); - if (candidates.Count == 0) throw new InvalidOperationException("The reverse candidate scenario produced no candidate."); - LocalG2CandidateGeometry candidate = candidates[0]; - bool opposite = candidate.StartVehicleCurvaturePerMeter * candidate.StartGeometricCurvaturePerMeter < 0d && - candidate.EndVehicleCurvaturePerMeter * candidate.EndGeometricCurvaturePerMeter < 0d; - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, - segment.Direction.ToString(), opposite, false, false); - } - - private static CandidateTestSnapshot BuildSpliced() - { - PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, 0.17d); - LocalG2SmoothingRegion region = CreateRegion(new[] - { - new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, 0d, 0.17d, 0.4d), - }, 0.25d, 0.75d); - IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( - segment, region, 0.025d, CreateOptions(), CancellationToken.None); - if (candidates.Count == 0) throw new InvalidOperationException("The splice candidate scenario produced no candidate."); - PreparedDirectionSegment untouched = CreateSegment(TravelDirection.Reverse, null); - var path = new PreparedPath(new[] { segment, untouched }); - if (!new LocalG2PathSplicer().TryReplace(path, candidates[0], out PreparedPath replaced, out string reason)) - throw new InvalidOperationException(reason); - PreparedDirectionSegment result = replaced.Segments[0]; - bool noDuplicates = true; - for (int index = 1; index < result.Points.Count; index++) - { - if (!result.Points[index - 1].IsGearSwitchPoint && !result.Points[index].IsGearSwitchPoint && - Distance(result.Points[index - 1], result.Points[index]) <= 1e-12d) - { - noDuplicates = false; - break; - } - } - bool monotonicallyIncreasing = true; - for (int index = 1; index < result.Points.Count; index++) - { - if (result.Points[index].ArcLength <= result.Points[index - 1].ArcLength) - { - monotonicallyIncreasing = false; - break; - } - } - bool endpoints = Distance(segment.Points[0], result.Points[0]) <= 1e-12d && - Distance(segment.Points[segment.Points.Count - 1], result.Points[result.Points.Count - 1]) <= 1e-12d && - result.StartVehicleCurvaturePerMeter == segment.StartVehicleCurvaturePerMeter && monotonicallyIncreasing && - ReferenceEquals(untouched, replaced.Segments[1]); - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, - false, noDuplicates, endpoints); - } - - private static CandidateTestSnapshot BuildStartBoundary() - { - PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, 0.17d); - LocalG2SmoothingRegion region = CreateRegion(new[] - { - new CurvatureTransition(0, 0, 1, 0d, 0d, 0d, 0d, 0.17d, 0.4d), - }, 0d, 0.5d); - IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( - segment, region, 0.025d, CreateOptions(), CancellationToken.None); - if (candidates.Count == 0) throw new InvalidOperationException("The start-boundary transition must not create a zero-length left curve."); - LocalG2CandidateGeometry candidate = candidates[0]; - return new CandidateTestSnapshot(candidates.Count, - Math.Abs(candidate.StartVehicleCurvaturePerMeter - 0.17d), 0d, - Math.Abs(candidate.StartVehicleCurvaturePerMeter - 0.17d), 0d, - ContainsLocalG2(candidate.RegionPoints), 0, false, string.Empty, false, false, false); - } - - private static CandidateTestSnapshot BuildInteriorStationaryCurve() - { - const double endpointX = 0.00325d; - if (!QuinticHermiteCurve2D.TryCreate(0d, 0d, 0.01d, 0d, 0d, 0d, - endpointX, 0d, 0.01d, 0d, 0d, 0d, out QuinticHermiteCurve2D curve, out string reason)) - { - throw new InvalidOperationException(reason); - } - var points = new[] - { - new SmoothingPoint2D(0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothingPoint2D(endpointX, 0d, endpointX, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - var segment = new PreparedDirectionSegment(0, TravelDirection.Forward, points, false, false); - var sampled = new List(); - bool accepted = TryAppendCurveSamples(curve, - new BoundaryNode(0d, 0d, 0d, 0d, 0d), - new BoundaryNode(endpointX, endpointX, 0d, 0d, 0d), - segment, 0.025d, sampled, CancellationToken.None); - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, - false, false, false, !accepted); - } - - private static CandidateTestSnapshot BuildConstantVelocityCurve() - { - if (!QuinticHermiteCurve2D.TryCreate(0d, 0d, 1d, 0d, 0d, 0d, - 1d, 0d, 1d, 0d, 0d, 0d, out QuinticHermiteCurve2D curve, out string reason)) - { - throw new InvalidOperationException(reason); - } - var points = new[] - { - new SmoothingPoint2D(0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothingPoint2D(1d, 0d, 1d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - var segment = new PreparedDirectionSegment(0, TravelDirection.Forward, points, false, false); - var sampled = new List(); - bool accepted = TryAppendCurveSamples(curve, - new BoundaryNode(0d, 0d, 0d, 0d, 0d), - new BoundaryNode(1d, 1d, 0d, 0d, 0d), - segment, 0.025d, sampled, CancellationToken.None); - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, - false, false, false, false, false, false, accepted); - } - - private static CandidateTestSnapshot BuildExactSpliceEndpoints() - { - PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); - var candidate = new LocalG2CandidateGeometry(0, 0, 0.25d, 0.75d, 0.25d, 0.25d, - new[] - { - Point(0.2500000005d, 0d, 0.25d, false), - Point(0.5d, 0d, 0.5d, false), - Point(0.7499999995d, 0d, 0.75d, false), - }, 0d, 0d, 0d, 0d, true); - if (!new LocalG2PathSplicer().TryReplace(new PreparedPath(new[] { segment }), candidate, - out PreparedPath replaced, out string reason)) - { - throw new InvalidOperationException(reason); - } - PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.25d, out SmoothingPoint2D start, out _); - PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.75d, out SmoothingPoint2D end, out _); - IReadOnlyList result = replaced.Segments[0].Points; - bool exact = result[1].X == start.X && result[1].Y == start.Y && - result[result.Count - 2].X == end.X && result[result.Count - 2].Y == end.Y; - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, - false, false, false, false, exact); - } - - private static CandidateTestSnapshot BuildGearBoundary() - { - var sourcePoints = new[] - { - Point(0d, 0d, 0d, true), Point(0.25d, 0d, 0.25d, false), Point(0.5d, 0d, 0.5d, false), - }; - var segment = new PreparedDirectionSegment(0, TravelDirection.Forward, sourcePoints, true, false); - var candidate = new LocalG2CandidateGeometry(0, 0, 0d, 0.5d, 0d, 0.5d, - new[] { Point(0d, 0d, 0d, false), Point(0.25d, 0d, 0.25d, false), Point(0.5d, 0d, 0.5d, false) }, - 0d, 0d, 0d, 0d, true); - if (!new LocalG2PathSplicer().TryReplace(new PreparedPath(new[] { segment }), candidate, - out PreparedPath replaced, out string reason)) - { - throw new InvalidOperationException(reason); - } - bool preserved = replaced.Segments[0].StartsAtGearSwitch && replaced.Segments[0].Points[0].IsGearSwitchPoint; - return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, - false, false, false, false, false, preserved); - } - - private static CandidateTestSnapshot BuildTwoRegionWorkOrder() - { - var points = new List(); - for (int index = 0; index <= 12; index++) - { - double arc = index * 0.25d; - points.Add(new SmoothingPoint2D( - arc, - 0d, - arc, - 0d, - 0d, - 1d, - false, - SmoothedPathPointSource.Anchor)); - } - - var segment = new PreparedDirectionSegment( - 0, - TravelDirection.Forward, - points, - false, - false); - var original = new PreparedPath(new[] { segment }); - LocalG2SmoothingRegion frontRegion = CreateOrderedRegion(0.75d, 0.5d, 1.0d, 0); - LocalG2SmoothingRegion backRegion = CreateOrderedRegion(2.25d, 2.0d, 2.5d, 1); - var reportOrder = new[] { frontRegion, backRegion }; - - var orderer = new LocalG2RegionWorkOrder(); - if (!orderer.TryCreate( - reportOrder, - out IReadOnlyList workOrder, - out string orderReason)) - { - throw new InvalidOperationException(orderReason); - } - if (!orderer.TryCreate( - reportOrder, - out IReadOnlyList repeatedOrder, - out string repeatedReason)) - { - throw new InvalidOperationException(repeatedReason); - } - - bool descending = - ReferenceEquals(backRegion, workOrder[0]) && - ReferenceEquals(frontRegion, workOrder[1]); - bool deterministic = - ReferenceEquals(workOrder[0], repeatedOrder[0]) && - ReferenceEquals(workOrder[1], repeatedOrder[1]) && - ReferenceEquals(frontRegion, reportOrder[0]) && - ReferenceEquals(backRegion, reportOrder[1]); - - var wrongSegmentTransition = new CurvatureTransition( - 1, - 0, - 1, - 0.75d, - 0.75d, - 0d, - 0d, - 0d, - 0.4d); - var invalidRegion = new LocalG2SmoothingRegion( - 0, - new[] { wrongSegmentTransition }, - 0.5d, - 1.0d, - new[] - { - new LocalG2WindowVariant(0, 0.5d, 1.0d, 0.25d, 0.25d), - }); - bool invalidWorkOrderRejected = - !orderer.TryCreate( - new[] { invalidRegion }, - out _, - out _) && - !orderer.TryCreate( - new LocalG2SmoothingRegion[] { null }, - out _, - out _); - - LocalG2CandidateGeometry frontCandidate = - CreateLengthChangingCandidate(0, 0.5d, 1.0d); - LocalG2CandidateGeometry backCandidate = - CreateLengthChangingCandidate(1, 2.0d, 2.5d); - var splicer = new LocalG2PathSplicer(); - - if (!splicer.TryReplace( - original, - backCandidate, - out PreparedPath afterBack, - out string backReason)) - { - throw new InvalidOperationException(backReason); - } - - bool frontArcPreserved = - PathReferenceInterpolator.TryInterpolateByArcLength( - afterBack.Segments[0].Points, - 0.5d, - out SmoothingPoint2D frontStart, - out _) && - PathReferenceInterpolator.TryInterpolateByArcLength( - afterBack.Segments[0].Points, - 1.0d, - out SmoothingPoint2D frontEnd, - out _) && - Math.Abs(frontStart.X - 0.5d) <= 1e-12d && - Math.Abs(frontEnd.X - 1.0d) <= 1e-12d; - - if (!splicer.TryReplace( - afterBack, - frontCandidate, - out PreparedPath afterBoth, - out string frontReason)) - { - throw new InvalidOperationException(frontReason); - } - - if (!splicer.TryReplace( - original, - backCandidate, - out PreparedPath repeatedAfterBack, - out string repeatedBackReason)) - { - throw new InvalidOperationException(repeatedBackReason); - } - if (!splicer.TryReplace( - repeatedAfterBack, - frontCandidate, - out PreparedPath repeatedAfterBoth, - out string repeatedFrontReason)) - { - throw new InvalidOperationException(repeatedFrontReason); - } - - bool frontInteriorRetained = false; - bool backInteriorRetained = false; - int frontInteriorCount = 0; - int backInteriorCount = 0; - for (int index = 0; index < afterBoth.Segments[0].Points.Count; index++) - { - SmoothingPoint2D point = afterBoth.Segments[0].Points[index]; - if (point.Source != SmoothedPathPointSource.LocalG2Transition) - continue; - if (point.X == 0.75d && point.Y == 0.20d) - { - frontInteriorCount++; - frontInteriorRetained = true; - } - if (point.X == 2.25d && point.Y == 0.20d) - { - backInteriorCount++; - backInteriorRetained = true; - } - } - bool exactInteriorsRetained = - frontInteriorRetained && - backInteriorRetained && - frontInteriorCount == 1 && - backInteriorCount == 1; - bool deterministicReplacementGeometry = - HasSameReplacementGeometry(afterBoth, repeatedAfterBoth); - - if (!splicer.TryReplace( - original, - frontCandidate, - out PreparedPath afterFront, - out string firstReason)) - { - throw new InvalidOperationException(firstReason); - } - bool forwardOrderRejected = !splicer.TryReplace( - afterFront, - backCandidate, - out _, - out _); - - return new CandidateTestSnapshot( - 0, - 0d, - 0d, - 0d, - 0d, - false, - 0, - false, - string.Empty, - false, - false, - false, - false, - false, - false, - false, - descending, - frontArcPreserved, - exactInteriorsRetained, - forwardOrderRejected, - deterministic, - invalidWorkOrderRejected, - deterministicReplacementGeometry); - } - - private static CandidateTestSnapshot BuildMultiSegmentWorkOrder() - { - LocalG2SmoothingRegion laterSegment = - CreateOrderedRegion(0.25d, 0d, 0.5d, 0, 1); - LocalG2SmoothingRegion firstTie = - CreateOrderedRegion(0.75d, 0.5d, 1.0d, 0); - LocalG2SmoothingRegion secondTie = - CreateOrderedRegion(0.75d, 0.5d, 1.0d, 1); - var reportOrder = new[] { laterSegment, firstTie, secondTie }; - if (!new LocalG2RegionWorkOrder().TryCreate( - reportOrder, - out IReadOnlyList workOrder, - out string reason)) - { - throw new InvalidOperationException(reason); - } - - bool segmentOrderAscending = - workOrder.Count == 3 && - workOrder[0].SegmentIndex == 0 && - workOrder[1].SegmentIndex == 0 && - workOrder[2].SegmentIndex == 1; - bool equalArcUsesReportOrder = - ReferenceEquals(firstTie, workOrder[0]) && - ReferenceEquals(secondTie, workOrder[1]); - return new CandidateTestSnapshot( - 0, - 0d, - 0d, - 0d, - 0d, - false, - 0, - false, - string.Empty, - false, - false, - false, - segmentOrderAscending: segmentOrderAscending, - equalArcUsesReportOrder: equalArcUsesReportOrder); - } - - private static bool HasSameReplacementGeometry( - PreparedPath first, - PreparedPath second) - { - if (first == null || second == null || - first.Points.Count != second.Points.Count || - first.Segments.Count != second.Segments.Count) - { - return false; - } - - for (int segmentIndex = 0; segmentIndex < first.Segments.Count; segmentIndex++) - { - PreparedDirectionSegment firstSegment = first.Segments[segmentIndex]; - PreparedDirectionSegment secondSegment = second.Segments[segmentIndex]; - if (firstSegment.Direction != secondSegment.Direction || - firstSegment.Points.Count != secondSegment.Points.Count) - { - return false; - } - - for (int pointIndex = 0; pointIndex < firstSegment.Points.Count; pointIndex++) - { - SmoothingPoint2D firstPoint = firstSegment.Points[pointIndex]; - SmoothingPoint2D secondPoint = secondSegment.Points[pointIndex]; - if (firstPoint.X != secondPoint.X || - firstPoint.Y != secondPoint.Y || - firstPoint.Source != secondPoint.Source || - firstSegment.Direction != secondSegment.Direction) - { - return false; - } - } - } - return true; - } - - private static LocalG2SmoothingRegion CreateOrderedRegion( - double eventArc, - double startArc, - double endArc, - int index, - int segmentIndex = 0) - { - var transition = new CurvatureTransition( - segmentIndex, - index, - index + 1, - eventArc, - eventArc, - 0d, - 0d, - 0d, - 0.4d); - return new LocalG2SmoothingRegion( - segmentIndex, - new[] { transition }, - startArc, - endArc, - new[] - { - new LocalG2WindowVariant( - 0, - startArc, - endArc, - eventArc - startArc, - endArc - eventArc), - }); - } - - private static LocalG2CandidateGeometry CreateLengthChangingCandidate( - int candidateIndex, - double startArc, - double endArc) - { - double middleArc = 0.5d * (startArc + endArc); - var points = new[] - { - Point(startArc, 0d, startArc, false), - Point(middleArc, 0.20d, middleArc, false), - Point(endArc, 0d, endArc, false), - }; - return new LocalG2CandidateGeometry( - candidateIndex, - 0, - startArc, - endArc, - middleArc - startArc, - endArc - middleArc, - points, - 0d, - 0d, - 0d, - 0d, - true); - } - - private static PreparedDirectionSegment CreateSegment(TravelDirection direction, double? startCurvature) - { - double heading = direction == TravelDirection.Forward ? 0d : Math.PI; - var points = new List(); - for (int index = 0; index <= 4; index++) - { - double arc = index * 0.25d; - points.Add(new SmoothingPoint2D(arc, 0d, arc, heading, heading, 1d, false, SmoothedPathPointSource.Anchor)); - } - return new PreparedDirectionSegment(0, direction, points, false, false, startCurvature); - } - - private static LocalG2SmoothingRegion CreateRegion( - IReadOnlyList transitions, - double startArcLengthMeters, - double endArcLengthMeters) - { - return new LocalG2SmoothingRegion(0, transitions, startArcLengthMeters, endArcLengthMeters, - new[] { new LocalG2WindowVariant(0, startArcLengthMeters, endArcLengthMeters, - transitions[0].LocalArcLengthMeters - startArcLengthMeters, - endArcLengthMeters - transitions[transitions.Count - 1].LocalArcLengthMeters) }); - } - - private static LocalG2OptionsSnapshot CreateOptions() => new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()); - private static bool ContainsLocalG2(IReadOnlyList points) - { - for (int index = 0; index < points.Count; index++) - if (points[index].Source == SmoothedPathPointSource.LocalG2Transition) return true; - return false; - } - private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) => - Math.Sqrt((right.X - left.X) * (right.X - left.X) + (right.Y - left.Y) * (right.Y - left.Y)); - private static SmoothingPoint2D Point(double x, double y, double arcLength, bool isGearSwitch) => - new SmoothingPoint2D(x, y, arcLength, 0d, 0d, 1d, isGearSwitch, - isGearSwitch ? SmoothedPathPointSource.GearSwitch : SmoothedPathPointSource.LocalG2Transition); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs deleted file mode 100644 index 50b84d5..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs +++ /dev/null @@ -1,559 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 对一个局部 G2 替换候选执行区域质量门和完整路径安全复核。 -internal sealed class LocalG2CandidateEvaluator -{ - private const double CurvatureRangeTolerance = 1e-6d; - private const double WindowPointToleranceMeters = 1e-10d; - private static readonly IReadOnlyList EmptyPath = - new ReadOnlyCollection(new List()); - private static readonly IReadOnlyList EmptySegments = - new ReadOnlyCollection(new List()); - private readonly PathGeometryAnalyzer _analyzer; - private readonly LocalG2PathSplicer _splicer; - private readonly SmoothedPathValidator _validator; - - internal LocalG2CandidateEvaluator() - : this(new PathGeometryAnalyzer(), new LocalG2PathSplicer(), new SmoothedPathValidator()) - { - } - - internal LocalG2CandidateEvaluator( - PathGeometryAnalyzer analyzer, - LocalG2PathSplicer splicer, - SmoothedPathValidator validator) - { - _analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer)); - _splicer = splicer ?? throw new ArgumentNullException(nameof(splicer)); - _validator = validator ?? throw new ArgumentNullException(nameof(validator)); - } - - internal LocalG2CandidateEvaluation Evaluate( - PreparedPath rawPath, - PreparedPath currentPath, - LocalG2SmoothingRegion region, - LocalG2CandidateGeometry candidate, - PathSmoothingRequest request, - LocalG2OptionsSnapshot options, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - int candidateIndex = candidate == null ? -1 : candidate.CandidateIndex; - if (!HasUsableInput(rawPath, currentPath, region, candidate, request, options)) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "局部 G2 候选评价输入无效。"); - - PreparedDirectionSegment currentSegment = currentPath.Segments[candidate.SegmentIndex]; - if (!TryExtractWindow(currentSegment, candidate.StartArcLengthMeters, candidate.EndArcLengthMeters, - out IReadOnlyList rawWindow, out string reason) || - !TryAnalyzeWindow(currentSegment, rawWindow, request.Configuration.OutputSpacingMeters, - out PathGeometryAnalysis rawAnalysis, out reason) || - !TryAnalyzeWindow(currentSegment, candidate.RegionPoints, request.Configuration.OutputSpacingMeters, - out PathGeometryAnalysis candidateAnalysis, out reason) || - !HasFiniteMetrics(rawAnalysis) || !HasFiniteMetrics(candidateAnalysis)) - { - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, - string.IsNullOrEmpty(reason) ? "局部 G2 区域几何分析失败。" : reason); - } - - if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double vehicleMaximumCurvature)) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "车辆曲率约束无效。"); - if (candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter > vehicleMaximumCurvature + CurvatureRangeTolerance) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureExceeded, "局部 G2 候选超过车辆曲率上限。"); - - GetCurvatureRange(rawAnalysis.Path, out double rawMinimumCurvature, out double rawMaximumCurvature); - if (ExceedsRawCurvatureRange(candidateAnalysis.Path, rawMinimumCurvature, rawMaximumCurvature)) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureOvershoot, "局部 G2 候选超出原始区域曲率范围。"); - - double maximumDeviation = MaximumDistanceToPolyline(candidate.RegionPoints, rawWindow); - if (!NumericGuard.IsFinite(maximumDeviation)) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "局部 G2 候选偏差计算失败。"); - if (maximumDeviation > options.MaximumDeviationMeters) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.DeviationExceeded, "局部 G2 候选偏离原始窗口过远。"); - - if (!_splicer.TryReplace(currentPath, candidate, out PreparedPath spliced, out reason) || - !_analyzer.TryAnalyze(spliced.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis fullAnalysis, out reason) || - !HasFiniteMetrics(fullAnalysis)) - { - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, - string.IsNullOrEmpty(reason) ? "局部 G2 完整路径几何分析失败。" : reason); - } - - if (!_validator.TryValidate(fullAnalysis.Path, fullAnalysis.Segments, rawPath, request.Map, request.Vehicle, - request.Configuration.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath, - out double minimumClearance, out reason)) - { - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.Collision, - string.IsNullOrEmpty(reason) ? "局部 G2 完整路径安全复核失败。" : reason); - } - if (!NumericGuard.IsFinite(minimumClearance) || minimumClearance < request.Configuration.MinimumClearanceReserveMeters) - return new LocalG2CandidateEvaluation(false, PathSmoothingRegionFailureReason.InsufficientClearance, candidateIndex, - null, EmptyPath, EmptySegments, rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, rawAnalysis.CurvatureVariationCost, - candidateAnalysis.CurvatureVariationCost, maximumDeviation, minimumClearance, - candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter, 0d, "局部 G2 候选净空不足。"); - - if (candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter > - rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter * - (1d - options.MinimumPeakGradientImprovementRatio)) - { - return new LocalG2CandidateEvaluation(false, PathSmoothingRegionFailureReason.InsufficientImprovement, candidateIndex, - null, EmptyPath, EmptySegments, rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, rawAnalysis.CurvatureVariationCost, - candidateAnalysis.CurvatureVariationCost, maximumDeviation, minimumClearance, - candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter, 0d, "局部 G2 候选曲率导数峰值改善不足。"); - } - if (candidateAnalysis.CurvatureVariationCost > rawAnalysis.CurvatureVariationCost * - (1d + options.MaximumVariationCostRegressionRatio)) - { - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.VariationCostRegression, "局部 G2 候选曲率变化代价回退。"); - } - - if (!_analyzer.TryAnalyze(rawPath.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis rawFullAnalysis, out reason) || - !HasFiniteMetrics(rawFullAnalysis)) - { - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, - string.IsNullOrEmpty(reason) ? "原始完整路径几何分析失败。" : reason); - } - - return new LocalG2CandidateEvaluation( - true, - PathSmoothingRegionFailureReason.None, - candidateIndex, - spliced, - safePath, - fullAnalysis.Segments, - rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - rawAnalysis.CurvatureVariationCost, - candidateAnalysis.CurvatureVariationCost, - maximumDeviation, - minimumClearance, - candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter, - Math.Abs(fullAnalysis.PathLengthMeters - rawFullAnalysis.PathLengthMeters), - "Accepted"); - } - - internal static LocalG2CandidateEvaluation SelectBest(IReadOnlyList evaluations) - { - if (evaluations == null) throw new ArgumentNullException(nameof(evaluations)); - LocalG2CandidateEvaluation best = null; - for (int index = 0; index < evaluations.Count; index++) - { - LocalG2CandidateEvaluation evaluation = evaluations[index]; - if (evaluation == null || !evaluation.Accepted) continue; - if (best == null || Compare(evaluation, best) < 0) best = evaluation; - } - return best ?? Rejected(-1, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "没有通过质量门的局部 G2 候选。"); - } - - private static int Compare(LocalG2CandidateEvaluation left, LocalG2CandidateEvaluation right) - { - int result = left.MaximumDeviationMeters.CompareTo(right.MaximumDeviationMeters); - if (result != 0) return result; - result = left.ResultPeakCurvatureDerivativePerSquareMeter.CompareTo(right.ResultPeakCurvatureDerivativePerSquareMeter); - if (result != 0) return result; - result = left.ResultCurvatureVariationCost.CompareTo(right.ResultCurvatureVariationCost); - if (result != 0) return result; - result = left.AbsolutePathLengthChangeMeters.CompareTo(right.AbsolutePathLengthChangeMeters); - return result != 0 ? result : left.CandidateIndex.CompareTo(right.CandidateIndex); - } - - private static bool HasUsableInput(PreparedPath rawPath, PreparedPath currentPath, LocalG2SmoothingRegion region, - LocalG2CandidateGeometry candidate, PathSmoothingRequest request, LocalG2OptionsSnapshot options) - { - return rawPath != null && currentPath != null && region != null && candidate != null && request != null && options != null && - request.Configuration != null && request.Map != null && request.Vehicle != null && candidate.SegmentIndex == region.SegmentIndex && - candidate.SegmentIndex >= 0 && candidate.SegmentIndex < currentPath.Segments.Count && - NumericGuard.IsPositiveFinite(request.Configuration.OutputSpacingMeters) && - NumericGuard.IsPositiveFinite(request.Configuration.MaximumCollisionCheckStepMeters) && - NumericGuard.IsFinite(request.Configuration.MinimumClearanceReserveMeters) && - request.Configuration.MinimumClearanceReserveMeters >= 0d; - } - - private static bool TryExtractWindow(PreparedDirectionSegment segment, double startArcLength, double endArcLength, - out IReadOnlyList window, out string reason) - { - window = null; - reason = string.Empty; - if (segment == null || !PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, startArcLength, out SmoothingPoint2D start, out reason) || - !PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, endArcLength, out SmoothingPoint2D end, out reason)) return false; - var points = new List { start }; - for (int index = 0; index < segment.Points.Count; index++) - { - SmoothingPoint2D point = segment.Points[index]; - if (point.ArcLength > startArcLength && point.ArcLength < endArcLength && - !SamePosition(points[points.Count - 1], point)) - { - points.Add(point); - } - } - if (SamePosition(points[points.Count - 1], end)) - points[points.Count - 1] = end; - else - points.Add(end); - window = new ReadOnlyCollection(points); - return true; - } - - private static bool SamePosition(SmoothingPoint2D left, SmoothingPoint2D right) - { - if (left == null || right == null) return false; - double x = right.X - left.X; - double y = right.Y - left.Y; - return x * x + y * y <= WindowPointToleranceMeters * WindowPointToleranceMeters; - } - - private bool TryAnalyzeWindow(PreparedDirectionSegment source, IReadOnlyList points, double spacing, - out PathGeometryAnalysis analysis, out string reason) - { - var segment = new PreparedDirectionSegment(0, source.Direction, points, false, false, - source.Points[0].ArcLength == points[0].ArcLength ? source.StartVehicleCurvaturePerMeter : null); - return _analyzer.TryAnalyze(new[] { segment }, spacing, out analysis, out reason); - } - - private static bool HasFiniteMetrics(PathGeometryAnalysis analysis) - { - return analysis != null && NumericGuard.IsFinite(analysis.PathLengthMeters) && - NumericGuard.IsFinite(analysis.MaximumAbsoluteVehicleCurvaturePerMeter) && - NumericGuard.IsFinite(analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter) && - NumericGuard.IsFinite(analysis.CurvatureVariationCost) && analysis.Path != null && analysis.Path.Count >= 2; - } - - private static void GetCurvatureRange(IReadOnlyList path, out double minimum, out double maximum) - { - minimum = double.PositiveInfinity; - maximum = double.NegativeInfinity; - for (int index = 0; index < path.Count; index++) - { - minimum = Math.Min(minimum, path[index].VehicleCurvature); - maximum = Math.Max(maximum, path[index].VehicleCurvature); - } - } - - private static bool ExceedsRawCurvatureRange(IReadOnlyList candidate, double minimum, double maximum) - { - for (int index = 0; index < candidate.Count; index++) - { - double curvature = candidate[index].VehicleCurvature; - if (curvature < minimum - CurvatureRangeTolerance || curvature > maximum + CurvatureRangeTolerance) return true; - } - return false; - } - - private static double MaximumDistanceToPolyline(IReadOnlyList candidate, IReadOnlyList raw) - { - if (candidate == null || raw == null || candidate.Count == 0 || raw.Count < 2) return double.NaN; - double maximum = 0d; - for (int index = 0; index < candidate.Count; index++) - { - double nearest = double.PositiveInfinity; - for (int segment = 1; segment < raw.Count; segment++) - nearest = Math.Min(nearest, PointToSegmentDistance(candidate[index], raw[segment - 1], raw[segment])); - maximum = Math.Max(maximum, nearest); - } - return maximum; - } - - private static double PointToSegmentDistance(SmoothingPoint2D point, SmoothingPoint2D start, SmoothingPoint2D end) - { - double dx = end.X - start.X; - double dy = end.Y - start.Y; - double lengthSquared = dx * dx + dy * dy; - if (!NumericGuard.IsPositiveFinite(lengthSquared)) return double.NaN; - double projection = ((point.X - start.X) * dx + (point.Y - start.Y) * dy) / lengthSquared; - projection = Math.Max(0d, Math.Min(1d, projection)); - double nearestX = start.X + projection * dx; - double nearestY = start.Y + projection * dy; - double distanceX = point.X - nearestX; - double distanceY = point.Y - nearestY; - return Math.Sqrt(distanceX * distanceX + distanceY * distanceY); - } - - private static LocalG2CandidateEvaluation Rejected(int candidateIndex, PathSmoothingRegionFailureReason failureReason, string reason) - { - return new LocalG2CandidateEvaluation(false, failureReason, candidateIndex, null, EmptyPath, EmptySegments, - 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, reason); - } - - /// 反射脚本使用的窄范围确定性质量门覆盖入口。 - public static class TestHooks - { - public static EvaluationTestSnapshot Execute(string scenario) - { - LocalG2CandidateEvaluation evaluation; - switch (scenario) - { - case "TooFar": - PreparedPath tooFarPath = CreateWavyPath(1d); - evaluation = Evaluate(tooFarPath, CreateCandidate(tooFarPath, 0, 1.12d), 0d); - break; - case "Overshoot": - PreparedPath overshootPath = CreateStraightPath(1d); - evaluation = Evaluate(overshootPath, CreateCandidate(overshootPath, 0, 1.15d), 0d); - break; - case "NoOp": - PreparedPath noOpPath = CreateWavyPath(1d); - evaluation = Evaluate(noOpPath, CreateCandidateFromPath(noOpPath, 0), 0d); - break; - case "Oscillating": - PreparedPath oscillatingPath = CreateOscillationBaseline(1d); - evaluation = Evaluate(oscillatingPath, CreateOscillatingCandidate(oscillatingPath), 0d); - break; - case "LowClearance": - PreparedPath lowClearancePath = CreateWavyPath(0.27d); - evaluation = Evaluate(lowClearancePath, CreateCandidateFromPath(lowClearancePath, 0), 0.02d, false, true); - break; - case "Improved": - PreparedPath improvedPath = CreateWavyPath(1d); - evaluation = Evaluate(improvedPath, CreateCandidate(improvedPath, 0, 1d), 0d); - break; - case "SmallestDeviation": - PreparedPath smallestDeviationPath = CreateWavyPath(1d); - evaluation = Evaluate(smallestDeviationPath, CreateCandidate(smallestDeviationPath, 4, 1d), 100d); - break; - case "Best": evaluation = SelectBest(CreateWavyPath(1d)); break; - default: throw new ArgumentOutOfRangeException(nameof(scenario)); - } - return new EvaluationTestSnapshot(evaluation.Accepted ? "Accepted" : "Rejected", evaluation.FailureReason.ToString(), evaluation.CandidateIndex, - evaluation.MinimumBodyClearanceMeters, evaluation.Reason); - } - - private static LocalG2CandidateEvaluation Evaluate(PreparedPath path, LocalG2CandidateGeometry candidate, double minimumClearance, - bool useNearObstacle = false, bool useEmptyMap = false) - { - PathSmoothingConfiguration configuration = CreateConfiguration(minimumClearance); - PathSmoothingRequest request = new PathSmoothingRequest(null, null, - useEmptyMap ? CreateEmptyMap() : CreateMap(useNearObstacle), CreateVehicle(), configuration); - return new LocalG2CandidateEvaluator().Evaluate(path, path, CreateRegion(), candidate, request, - new LocalG2OptionsSnapshot(configuration), CancellationToken.None); - } - - private static LocalG2CandidateEvaluation SelectBest(PreparedPath path) - { - PathSmoothingConfiguration configuration = CreateConfiguration(0d); - PathSmoothingRequest request = new PathSmoothingRequest(null, null, CreateMap(false), CreateVehicle(), configuration); - var evaluator = new LocalG2CandidateEvaluator(); - LocalG2CandidateEvaluation smallestDeviation = evaluator.Evaluate(path, path, CreateRegion(), - CreateCandidate(path, 4, 1d), request, new LocalG2OptionsSnapshot(configuration), CancellationToken.None); - LocalG2CandidateEvaluation smootherButFarther = evaluator.Evaluate(path, path, CreateRegion(), - CreateCandidate(path, 5, 1.01d), request, new LocalG2OptionsSnapshot(configuration), CancellationToken.None); - return LocalG2CandidateEvaluator.SelectBest(new[] { smootherButFarther, smallestDeviation }); - } - - private static PreparedPath CreateWavyPath(double startX) - { - return CreatePath(new[] - { - Point(startX, 1d), Point(startX + 0.10d, 1.04d), Point(startX + 0.20d, 1.08d), - Point(startX + 0.30d, 1.05d), Point(startX + 0.40d, 0.98d), Point(startX + 0.50d, 0.92d), - Point(startX + 0.60d, 0.98d), Point(startX + 0.70d, 1.05d), Point(startX + 0.80d, 1.08d), - Point(startX + 0.90d, 1.04d), Point(startX + 1d, 1d), - }); - } - - private static PreparedPath CreateStraightPath(double startX) => CreatePath(new[] { Point(startX, 1d), Point(startX + 0.5d, 1d), Point(startX + 1d, 1d) }); - - private static PreparedPath CreateOscillationBaseline(double startX) - { - return CreatePath(new[] - { - Point(startX, 1d), Point(startX + 0.05d, 1.022d), Point(startX + 0.10d, 1d), - Point(startX + 2.03d, 1d), - }); - } - - private static PreparedPath CreatePath(IReadOnlyList points) - { - var normalized = new List(points.Count); - double arcLength = 0d; - for (int index = 0; index < points.Count; index++) - { - if (index > 0) - { - double dx = points[index].X - points[index - 1].X; - double dy = points[index].Y - points[index - 1].Y; - arcLength += Math.Sqrt(dx * dx + dy * dy); - } - normalized.Add(new SmoothingPoint2D(points[index].X, points[index].Y, arcLength, 0d, 0d, 1d, - false, SmoothedPathPointSource.LocalG2Transition)); - } - return new PreparedPath(new[] { new PreparedDirectionSegment(0, TravelDirection.Forward, normalized, false, false) }); - } - - private static LocalG2CandidateGeometry CreateCandidate(PreparedPath path, int index, double middleY) - { - IReadOnlyList source = path.Segments[0].Points; - SmoothingPoint2D start = source[0]; - SmoothingPoint2D end = source[source.Count - 1]; - return new LocalG2CandidateGeometry(index, 0, start.ArcLength, end.ArcLength, 0d, 0d, - new[] { Point(start.X, start.Y), Point((start.X + end.X) / 2d, middleY), Point(end.X, end.Y) }, - 0d, 0d, 0d, 0d, true); - } - - private static LocalG2CandidateGeometry CreateCandidateFromPath(PreparedPath path, int index) - { - IReadOnlyList points = path.Segments[0].Points; - return new LocalG2CandidateGeometry(index, 0, points[0].ArcLength, points[points.Count - 1].ArcLength, 0d, 0d, points, - 0d, 0d, 0d, 0d, true); - } - - private static LocalG2CandidateGeometry CreateOscillatingCandidate(PreparedPath path) - { - IReadOnlyList source = path.Segments[0].Points; - SmoothingPoint2D start = source[0]; - SmoothingPoint2D end = source[source.Count - 1]; - var points = new List { Point(start.X, start.Y) }; - SmoothingPoint2D straightStart = source[source.Count - 2]; - points.Add(Point(straightStart.X, straightStart.Y)); - for (int index = 1; index < 20; index++) - { - double x = straightStart.X + index * (end.X - straightStart.X) / 20d; - points.Add(Point(x, straightStart.Y + (index % 2 == 0 ? 0.004d : -0.004d))); - } - points.Add(Point(end.X, end.Y)); - return new LocalG2CandidateGeometry(0, 0, start.ArcLength, end.ArcLength, 0d, 0d, points, 0d, 0d, 0d, 0d, true); - } - - private static LocalG2SmoothingRegion CreateRegion() - { - return new LocalG2SmoothingRegion(0, - new[] { new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, 0d, 0d, 0d) }, - 0d, 1d, new[] { new LocalG2WindowVariant(0, 0d, 1d, 0d, 0d) }); - } - - private static PathSmoothingConfiguration CreateConfiguration(double minimumClearance) - { - var configuration = new PathSmoothingConfiguration - { - OutputSpacingMeters = 0.05d, - MaximumCollisionCheckStepMeters = 0.05d, - MinimumClearanceReserveMeters = minimumClearance, - }; - configuration.LocalG2Quintic.MaximumDeviationMeters = 0.10d; - configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = 0.20d; - configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio = 0.02d; - return configuration; - } - - private static VehicleParameters CreateVehicle() => new VehicleParameters - { - LengthMeters = 0.20d, WidthMeters = 0.20d, SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 1000000d, MinimumTurningRadiusMeters = 0.000001d, - }; - - private static PlanningGridMap CreateMap(bool useNearObstacle) - { - var request = new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 4000f, 0f, 4000f), ResolutionMm = 20f, - ObstacleSources = new IMapObstacleSource[] - { - new ManualObstacleSource("local-g2-evaluator", 1, true, new IMapObstacle[] - { - useNearObstacle - ? new AxisAlignedRectangleObstacle(100f, 150f, 900f, 1100f) - : new AxisAlignedRectangleObstacle(3000f, 3100f, 3000f, 3100f), - }), - }, - }; - PlanningMapBuildResult result = new PlanningMapFactory().Create(request); - if (!result.Succeeded || result.Map == null) throw new InvalidOperationException("测试地图创建失败。"); - return result.Map; - } - - private static PlanningGridMap CreateEmptyMap() - { - var request = new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 4000f, 0f, 4000f), ResolutionMm = 20f, - ObstacleSources = Array.Empty(), AllowExplicitEmptyMap = true, - }; - PlanningMapBuildResult result = new PlanningMapFactory().Create(request); - if (!result.Succeeded || result.Map == null) throw new InvalidOperationException("测试空地图创建失败。"); - return result.Map; - } - - private static SmoothingPoint2D Point(double x, double y) => - new SmoothingPoint2D(x, y, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.LocalG2Transition); - - public sealed class EvaluationTestSnapshot - { - internal EvaluationTestSnapshot(string status, string failureReason, int candidateIndex, double minimumClearanceMeters, string reason) - { - Status = status; - FailureReason = failureReason; - CandidateIndex = candidateIndex; - MinimumClearanceMeters = minimumClearanceMeters; - Reason = reason; - } - public string Status { get; } - public string FailureReason { get; } - public int CandidateIndex { get; } - public double MinimumClearanceMeters { get; } - public string Reason { get; } - } - } -} - -/// 不可变的局部 G2 候选质量与安全评价结果。 -internal sealed class LocalG2CandidateEvaluation -{ - internal LocalG2CandidateEvaluation(bool accepted, PathSmoothingRegionFailureReason failureReason, int candidateIndex, - PreparedPath splicedPreparedPath, IReadOnlyList safePath, IReadOnlyList safeSegments, - double rawPeakCurvatureDerivativePerSquareMeter, double resultPeakCurvatureDerivativePerSquareMeter, - double rawCurvatureVariationCost, double resultCurvatureVariationCost, double maximumDeviationMeters, - double minimumBodyClearanceMeters, double maximumAbsoluteVehicleCurvaturePerMeter, - double absolutePathLengthChangeMeters, string reason) - { - Accepted = accepted; - FailureReason = failureReason; - CandidateIndex = candidateIndex; - SplicedPreparedPath = splicedPreparedPath; - SafePath = Copy(safePath); - SafeSegments = Copy(safeSegments); - RawPeakCurvatureDerivativePerSquareMeter = rawPeakCurvatureDerivativePerSquareMeter; - ResultPeakCurvatureDerivativePerSquareMeter = resultPeakCurvatureDerivativePerSquareMeter; - RawCurvatureVariationCost = rawCurvatureVariationCost; - ResultCurvatureVariationCost = resultCurvatureVariationCost; - MaximumDeviationMeters = maximumDeviationMeters; - MinimumBodyClearanceMeters = minimumBodyClearanceMeters; - MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; - AbsolutePathLengthChangeMeters = absolutePathLengthChangeMeters; - Reason = reason ?? string.Empty; - } - - internal bool Accepted { get; } - internal PathSmoothingRegionFailureReason FailureReason { get; } - internal int CandidateIndex { get; } - internal PreparedPath SplicedPreparedPath { get; } - internal IReadOnlyList SafePath { get; } - internal IReadOnlyList SafeSegments { get; } - internal double RawPeakCurvatureDerivativePerSquareMeter { get; } - internal double ResultPeakCurvatureDerivativePerSquareMeter { get; } - internal double RawCurvatureVariationCost { get; } - internal double ResultCurvatureVariationCost { get; } - internal double MaximumDeviationMeters { get; } - internal double MinimumBodyClearanceMeters { get; } - internal double MaximumAbsoluteVehicleCurvaturePerMeter { get; } - internal double AbsolutePathLengthChangeMeters { get; } - internal string Reason { get; } - - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs deleted file mode 100644 index 22161f7..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 一个尚未经过安全和质量评价的局部 G2 替换几何。 -internal sealed class LocalG2CandidateGeometry -{ - internal LocalG2CandidateGeometry( - int candidateIndex, - int segmentIndex, - double startArcLengthMeters, - double endArcLengthMeters, - double leftWindowLengthMeters, - double rightWindowLengthMeters, - IReadOnlyList regionPoints, - double startVehicleCurvaturePerMeter, - double endVehicleCurvaturePerMeter, - double startGeometricCurvaturePerMeter, - double endGeometricCurvaturePerMeter, - bool internalConnectionsAreG2) - { - if (candidateIndex < 0 || segmentIndex < 0 || !NumericGuard.IsFinite(startArcLengthMeters) || - !NumericGuard.IsFinite(endArcLengthMeters) || startArcLengthMeters < 0d || - endArcLengthMeters < startArcLengthMeters || !NumericGuard.IsFinite(leftWindowLengthMeters) || - !NumericGuard.IsFinite(rightWindowLengthMeters) || leftWindowLengthMeters < 0d || - rightWindowLengthMeters < 0d || regionPoints == null || regionPoints.Count < 2 || - !NumericGuard.IsFinite(startVehicleCurvaturePerMeter) || !NumericGuard.IsFinite(endVehicleCurvaturePerMeter) || - !NumericGuard.IsFinite(startGeometricCurvaturePerMeter) || !NumericGuard.IsFinite(endGeometricCurvaturePerMeter)) - { - throw new ArgumentOutOfRangeException(nameof(regionPoints)); - } - - CandidateIndex = candidateIndex; - SegmentIndex = segmentIndex; - StartArcLengthMeters = startArcLengthMeters; - EndArcLengthMeters = endArcLengthMeters; - LeftWindowLengthMeters = leftWindowLengthMeters; - RightWindowLengthMeters = rightWindowLengthMeters; - RegionPoints = Copy(regionPoints); - StartVehicleCurvaturePerMeter = startVehicleCurvaturePerMeter; - EndVehicleCurvaturePerMeter = endVehicleCurvaturePerMeter; - StartGeometricCurvaturePerMeter = startGeometricCurvaturePerMeter; - EndGeometricCurvaturePerMeter = endGeometricCurvaturePerMeter; - InternalConnectionsAreG2 = internalConnectionsAreG2; - } - - internal int CandidateIndex { get; } - internal int SegmentIndex { get; } - internal double StartArcLengthMeters { get; } - internal double EndArcLengthMeters { get; } - internal double LeftWindowLengthMeters { get; } - internal double RightWindowLengthMeters { get; } - internal IReadOnlyList RegionPoints { get; } - - // 这些边界状态供候选评价和窄范围反射验证使用,不能替代统一几何分析器的最终统计。 - internal double StartVehicleCurvaturePerMeter { get; } - internal double EndVehicleCurvaturePerMeter { get; } - internal double StartGeometricCurvaturePerMeter { get; } - internal double EndGeometricCurvaturePerMeter { get; } - internal bool InternalConnectionsAreG2 { get; } - - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) - { - if (source[index] == null) throw new ArgumentOutOfRangeException(nameof(source)); - copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs deleted file mode 100644 index 3fddd10..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 局部 G2 预平滑一次运行使用的已校验不可变选项。 -internal sealed class LocalG2OptionsSnapshot -{ - internal LocalG2OptionsSnapshot(PathSmoothingConfiguration configuration) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - LocalG2QuinticOptions source = configuration.LocalG2Quintic; - if (source == null) throw new ArgumentOutOfRangeException(nameof(configuration)); - - MinimumWindowLengthMeters = source.MinimumWindowLengthMeters; - PreferredWindowLengthMeters = source.PreferredWindowLengthMeters; - MaximumWindowLengthMeters = source.MaximumWindowLengthMeters; - MaximumDeviationMeters = source.MaximumDeviationMeters; - AbsoluteCurvatureJumpFloorPerMeter = source.AbsoluteCurvatureJumpFloorPerMeter; - CurvatureJumpRatioOfMaximum = source.CurvatureJumpRatioOfMaximum; - MinimumPeakGradientImprovementRatio = source.MinimumPeakGradientImprovementRatio; - MaximumVariationCostRegressionRatio = source.MaximumVariationCostRegressionRatio; - MaximumCandidatesPerRegion = source.MaximumCandidatesPerRegion; - - if (!NumericGuard.IsPositiveFinite(MinimumWindowLengthMeters)) - throw new ArgumentOutOfRangeException(nameof(MinimumWindowLengthMeters)); - if (!NumericGuard.IsFinite(PreferredWindowLengthMeters) || PreferredWindowLengthMeters < MinimumWindowLengthMeters) - throw new ArgumentOutOfRangeException(nameof(PreferredWindowLengthMeters)); - if (!NumericGuard.IsFinite(MaximumWindowLengthMeters) || MaximumWindowLengthMeters < PreferredWindowLengthMeters) - throw new ArgumentOutOfRangeException(nameof(MaximumWindowLengthMeters)); - if (!NumericGuard.IsPositiveFinite(MaximumDeviationMeters)) - throw new ArgumentOutOfRangeException(nameof(MaximumDeviationMeters)); - if (!NumericGuard.IsPositiveFinite(AbsoluteCurvatureJumpFloorPerMeter)) - throw new ArgumentOutOfRangeException(nameof(AbsoluteCurvatureJumpFloorPerMeter)); - if (!NumericGuard.IsFinite(CurvatureJumpRatioOfMaximum) || CurvatureJumpRatioOfMaximum <= 0d || CurvatureJumpRatioOfMaximum > 1d) - throw new ArgumentOutOfRangeException(nameof(CurvatureJumpRatioOfMaximum)); - if (!NumericGuard.IsFinite(MinimumPeakGradientImprovementRatio) || - MinimumPeakGradientImprovementRatio <= 0d || MinimumPeakGradientImprovementRatio >= 1d) - { - throw new ArgumentOutOfRangeException(nameof(MinimumPeakGradientImprovementRatio)); - } - if (!NumericGuard.IsFinite(MaximumVariationCostRegressionRatio) || MaximumVariationCostRegressionRatio < 0d) - throw new ArgumentOutOfRangeException(nameof(MaximumVariationCostRegressionRatio)); - if (MaximumCandidatesPerRegion < 1) - throw new ArgumentOutOfRangeException(nameof(MaximumCandidatesPerRegion)); - } - - internal double MinimumWindowLengthMeters { get; } - internal double PreferredWindowLengthMeters { get; } - internal double MaximumWindowLengthMeters { get; } - internal double MaximumDeviationMeters { get; } - internal double AbsoluteCurvatureJumpFloorPerMeter { get; } - internal double CurvatureJumpRatioOfMaximum { get; } - internal double MinimumPeakGradientImprovementRatio { get; } - internal double MaximumVariationCostRegressionRatio { get; } - internal int MaximumCandidatesPerRegion { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs deleted file mode 100644 index 294efe0..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 只替换一个方向段内局部窗口,并保持其余方向拓扑不变。 -internal sealed class LocalG2PathSplicer -{ - internal bool TryReplace( - PreparedPath currentPath, - LocalG2CandidateGeometry candidate, - out PreparedPath replacedPath, - out string reason) - { - replacedPath = null; - reason = string.Empty; - if (currentPath == null || candidate == null || candidate.SegmentIndex < 0 || - candidate.SegmentIndex >= currentPath.Segments.Count || candidate.RegionPoints == null || - candidate.RegionPoints.Count < 2) - { - reason = "局部 G2 拼接输入无效。"; - return false; - } - - PreparedDirectionSegment source = currentPath.Segments[candidate.SegmentIndex]; - if (source == null || source.SegmentIndex != candidate.SegmentIndex || - !PathReferenceInterpolator.TryInterpolateByArcLength(source.Points, candidate.StartArcLengthMeters, out SmoothingPoint2D start, out reason) || - !PathReferenceInterpolator.TryInterpolateByArcLength(source.Points, candidate.EndArcLengthMeters, out SmoothingPoint2D end, out reason) || - !SamePosition(candidate.RegionPoints[0], start) || !SamePosition(candidate.RegionPoints[candidate.RegionPoints.Count - 1], end)) - { - if (string.IsNullOrEmpty(reason)) reason = "局部 G2 候选端点不匹配原始窗口。"; - return false; - } - - var combined = new List(); - for (int index = 0; index < source.Points.Count; index++) - { - SmoothingPoint2D point = source.Points[index]; - if (point.ArcLength < candidate.StartArcLengthMeters) AddWithoutNonGearDuplicates(combined, point); - } - // Candidate endpoints are only validated within a numerical tolerance. The replacement - // itself must use the exact source-window endpoints, including a possible gear marker. - AddWithoutNonGearDuplicates(combined, start); - for (int index = 1; index < candidate.RegionPoints.Count - 1; index++) - AddWithoutNonGearDuplicates(combined, candidate.RegionPoints[index]); - AddWithoutNonGearDuplicates(combined, end); - for (int index = 0; index < source.Points.Count; index++) - { - SmoothingPoint2D point = source.Points[index]; - if (point.ArcLength > candidate.EndArcLengthMeters) AddWithoutNonGearDuplicates(combined, point); - } - if (!TryRecalculateArcLengths(combined, out IReadOnlyList localPoints, out reason)) return false; - - var segments = new List(currentPath.Segments.Count); - for (int index = 0; index < currentPath.Segments.Count; index++) - { - PreparedDirectionSegment segment = currentPath.Segments[index]; - if (index != candidate.SegmentIndex) - { - segments.Add(segment); - continue; - } - segments.Add(new PreparedDirectionSegment(segment.SegmentIndex, segment.Direction, localPoints, - segment.StartsAtGearSwitch, segment.EndsAtGearSwitch, segment.StartVehicleCurvaturePerMeter)); - } - replacedPath = new PreparedPath(segments); - return true; - } - - private static bool TryRecalculateArcLengths( - IReadOnlyList points, - out IReadOnlyList recalculated, - out string reason) - { - recalculated = null; - reason = string.Empty; - if (points == null || points.Count < 2) - { - reason = "局部 G2 拼接后方向段没有足够点。"; - return false; - } - var output = new List(points.Count); - double arcLength = 0d; - for (int index = 0; index < points.Count; index++) - { - SmoothingPoint2D point = points[index]; - if (!IsValid(point)) - { - reason = "局部 G2 拼接点包含非法数值。"; - return false; - } - if (index > 0) - { - double distance = Distance(points[index - 1], point); - if (!NumericGuard.IsPositiveFinite(distance)) - { - reason = "局部 G2 拼接后存在重复非换向点。"; - return false; - } - arcLength += distance; - } - output.Add(new SmoothingPoint2D(point.X, point.Y, arcLength, point.Heading, point.UnwrappedHeading, - point.BodyClearance, point.IsGearSwitchPoint, point.Source)); - } - recalculated = output; - return true; - } - - private static void AddWithoutNonGearDuplicates(List output, SmoothingPoint2D point) - { - if (output.Count == 0) - { - output.Add(point); - return; - } - SmoothingPoint2D previous = output[output.Count - 1]; - if (!previous.IsGearSwitchPoint && !point.IsGearSwitchPoint && SamePosition(previous, point)) return; - output.Add(point); - } - - private static bool IsValid(SmoothingPoint2D point) => point != null && NumericGuard.IsFinite(point.X) && - NumericGuard.IsFinite(point.Y) && NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && - NumericGuard.IsFinite(point.BodyClearance) && point.BodyClearance >= 0d; - private static bool SamePosition(SmoothingPoint2D left, SmoothingPoint2D right) => - left != null && right != null && Distance(left, right) <= 1e-9d; - private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) - { - double dx = right.X - left.X; - double dy = right.Y - left.Y; - return Math.Sqrt(dx * dx + dy * dy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs deleted file mode 100644 index a2b8d42..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// Publishes independently validated Local G2 regional replacements through the dedicated service route. -internal sealed class LocalG2PreSmoothingPipeline -{ - private readonly CurvatureTransitionDetector _detector = new CurvatureTransitionDetector(); - private readonly LocalG2WindowPlanner _windowPlanner = new LocalG2WindowPlanner(); - private readonly LocalG2CandidateBuilder _builder = new LocalG2CandidateBuilder(); - private readonly LocalG2CandidateEvaluator _evaluator = new LocalG2CandidateEvaluator(); - private readonly LocalG2RegionWorkOrder _workOrder = new LocalG2RegionWorkOrder(); - private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); - private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); - - internal PathSmoothingResult Smooth( - PathSmoothingRequest request, - PreparedPath preparedPath, - RawPathBaseline rawBaseline, - CancellationToken cancellationToken) - { - var stopwatch = Stopwatch.StartNew(); - try - { - cancellationToken.ThrowIfCancellationRequested(); - if (request == null || preparedPath == null || rawBaseline == null || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double maximumCurvature)) - { - return Failure(PathSmoothingStatus.Failed, stopwatch, "局部 G2 预平滑输入无效。"); - } - - var options = new LocalG2OptionsSnapshot(request.Configuration); - if (!_detector.TryDetect(request, maximumCurvature, options, out IReadOnlyList transitions, out string reason) || - !_windowPlanner.TryPlan(preparedPath, transitions, options, out IReadOnlyList regions, out reason)) - { - return Failure(PathSmoothingStatus.Failed, stopwatch, reason); - } - - IReadOnlyList reportOrder = - new ReadOnlyCollection(new List(regions)); - if (!_workOrder.TryCreate(reportOrder, out IReadOnlyList workRegions, out string orderReason)) - return Failure(PathSmoothingStatus.Failed, stopwatch, orderReason); - - PreparedPath current = preparedPath; - var reportsByRegion = new Dictionary(); - var accepted = new List(); - int improvedCount = 0; - - foreach (LocalG2SmoothingRegion region in workRegions) - { - cancellationToken.ThrowIfCancellationRequested(); - IReadOnlyList candidates = _builder.Build( - preparedPath.Segments[region.SegmentIndex], region, - request.Configuration.OutputSpacingMeters, options, cancellationToken); - var evaluations = new List(); - for (int candidateIndex = 0; candidateIndex < candidates.Count; candidateIndex++) - evaluations.Add(_evaluator.Evaluate( - preparedPath, current, region, candidates[candidateIndex], request, options, cancellationToken)); - - LocalG2CandidateEvaluation best = LocalG2CandidateEvaluator.SelectBest(evaluations); - if (best.Accepted) - { - accepted.Add(new AcceptedRegion(region, current, best)); - current = best.SplicedPreparedPath; - improvedCount++; - reportsByRegion.Add(region, CreateImprovedReport(region, candidates.Count, best)); - } - else - { - reportsByRegion.Add(region, CreateRetainedReport(region, candidates.Count, best)); - } - } - - if (!TryValidateFinal(current, preparedPath, rawBaseline, request, options, out IReadOnlyList path, - out IReadOnlyList segments, out PathQualityMetrics metrics, out reason)) - { - for (int index = accepted.Count - 1; index >= 0; index--) - { - AcceptedRegion rollback = accepted[index]; - current = rollback.Before; - reportsByRegion[rollback.Region] = CreateRollbackReport(rollback.Region, rollback.Evaluation); - improvedCount--; - if (TryValidateFinal(current, preparedPath, rawBaseline, request, options, out path, out segments, out metrics, out reason)) - break; - } - } - - if (metrics == null) - return Failure(PathSmoothingStatus.Failed, stopwatch, reason); - - var reports = new List(reportOrder.Count); - for (int reportIndex = 0; reportIndex < reportOrder.Count; reportIndex++) - reports.Add(reportsByRegion[reportOrder[reportIndex]]); - - PathSmoothingStatus status; - if (transitions.Count == 0) status = PathSmoothingStatus.NotNeeded; - else if (improvedCount == regions.Count) status = PathSmoothingStatus.Complete; - else if (improvedCount > 0) status = PathSmoothingStatus.PartialImprovement; - else status = PathSmoothingStatus.Unchanged; - return PathSmoothingResult.PublishLocalG2( - status, - path, - segments, - new PathSmoothingDiagnostics(metrics, stopwatch.Elapsed, 0, 0d, reason ?? string.Empty), - reports); - } - catch (OperationCanceledException) - { - return Failure(PathSmoothingStatus.Cancelled, stopwatch, "路径平滑已取消。"); - } - catch (Exception exception) - { - return Failure(PathSmoothingStatus.Failed, stopwatch, exception.Message); - } - } - - private bool TryValidateFinal( - PreparedPath current, - PreparedPath rawPath, - RawPathBaseline rawBaseline, - PathSmoothingRequest request, - LocalG2OptionsSnapshot options, - out IReadOnlyList path, - out IReadOnlyList segments, - out PathQualityMetrics metrics, - out string reason) - { - path = null; - segments = null; - metrics = null; - reason = string.Empty; - if (!_analyzer.TryAnalyze(current.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason) || - !_validator.TryValidate(analysis.Path, analysis.Segments, rawPath, request.Map, request.Vehicle, - request.Configuration.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath, - out double minimumClearance, out reason) || - minimumClearance < request.Configuration.MinimumClearanceReserveMeters || - analysis.CurvatureVariationCost > rawBaseline.Metrics.CurvatureVariationCost * - (1d + options.MaximumVariationCostRegressionRatio)) - { - if (string.IsNullOrEmpty(reason)) reason = "局部 G2 完整路径复核未通过。"; - return false; - } - - path = safePath; - segments = analysis.Segments; - metrics = new PathQualityMetrics( - true, - analysis.PathLengthMeters, - analysis.MaximumAbsoluteVehicleCurvaturePerMeter, - analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - analysis.RootMeanSquareVehicleCurvaturePerMeter, - analysis.TotalAbsoluteCurvatureVariationPerMeter, - analysis.CurvatureVariationCost, - minimumClearance, - 0d, 0d, 0d, 0d); - return true; - } - - private static PathSmoothingRegionReport CreateImprovedReport( - LocalG2SmoothingRegion region, - int candidateCount, - LocalG2CandidateEvaluation evaluation) => - CreateReport(region, candidateCount, evaluation, PathSmoothingRegionStatus.Improved, PathSmoothingRegionFailureReason.None); - - private static PathSmoothingRegionReport CreateRetainedReport( - LocalG2SmoothingRegion region, - int candidateCount, - LocalG2CandidateEvaluation evaluation) => - CreateReport(region, candidateCount, evaluation, PathSmoothingRegionStatus.RetainedOriginal, evaluation.FailureReason); - - private static PathSmoothingRegionReport CreateRollbackReport( - LocalG2SmoothingRegion region, - LocalG2CandidateEvaluation evaluation) => - CreateReport(region, region.WindowVariants.Count, evaluation, PathSmoothingRegionStatus.RetainedOriginal, - PathSmoothingRegionFailureReason.GlobalValidationRollback); - - private static PathSmoothingRegionReport CreateReport( - LocalG2SmoothingRegion region, - int candidateCount, - LocalG2CandidateEvaluation evaluation, - PathSmoothingRegionStatus status, - PathSmoothingRegionFailureReason failureReason) - { - LocalG2WindowVariant window = region.WindowVariants[0]; - var jumps = new List(region.Transitions.Count); - for (int index = 0; index < region.Transitions.Count; index++) - jumps.Add(region.Transitions[index].RightVehicleCurvaturePerMeter - region.Transitions[index].LeftVehicleCurvaturePerMeter); - return new PathSmoothingRegionReport( - region.SegmentIndex, - window.StartArcLengthMeters, - window.EndArcLengthMeters, - jumps, - window.EndArcLengthMeters - window.StartArcLengthMeters, - window.EndArcLengthMeters - window.StartArcLengthMeters, - window.LeftWindowLengthMeters, - window.RightWindowLengthMeters, - candidateCount, - evaluation.CandidateIndex, - status, - failureReason, - evaluation.RawPeakCurvatureDerivativePerSquareMeter, - evaluation.ResultPeakCurvatureDerivativePerSquareMeter, - evaluation.RawCurvatureVariationCost, - evaluation.ResultCurvatureVariationCost, - evaluation.MaximumDeviationMeters, - evaluation.MinimumBodyClearanceMeters, - evaluation.MaximumAbsoluteVehicleCurvaturePerMeter); - } - - private static PathSmoothingResult Failure(PathSmoothingStatus status, Stopwatch stopwatch, string reason) => - PathSmoothingResult.Failure(status, - new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, 0, 0d, reason)); - - private sealed class AcceptedRegion - { - internal AcceptedRegion(LocalG2SmoothingRegion region, PreparedPath before, LocalG2CandidateEvaluation evaluation) - { - Region = region; - Before = before; - Evaluation = evaluation; - } - - internal LocalG2SmoothingRegion Region { get; } - internal PreparedPath Before { get; } - internal LocalG2CandidateEvaluation Evaluation { get; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs deleted file mode 100644 index dde7845..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 把稳定报告顺序转换为不会使后续原始局部弧长错位的工作顺序。 -internal sealed class LocalG2RegionWorkOrder -{ - internal bool TryCreate( - IReadOnlyList reportOrder, - out IReadOnlyList workOrder, - out string reason) - { - workOrder = Empty(); - reason = string.Empty; - if (reportOrder == null) - { - reason = "局部 G2 区域工作顺序输入无效。"; - return false; - } - - var indexed = new List(reportOrder.Count); - for (int index = 0; index < reportOrder.Count; index++) - { - LocalG2SmoothingRegion region = reportOrder[index]; - if (!TryValidate(region, out double firstArc, out reason)) - return false; - indexed.Add(new IndexedRegion(region, index, firstArc)); - } - - indexed.Sort(Compare); - var ordered = new List(indexed.Count); - for (int index = 0; index < indexed.Count; index++) - ordered.Add(indexed[index].Region); - workOrder = new ReadOnlyCollection(ordered); - return true; - } - - private static bool TryValidate( - LocalG2SmoothingRegion region, - out double firstArc, - out string reason) - { - firstArc = 0d; - reason = string.Empty; - if (region == null || region.SegmentIndex < 0 || - region.Transitions == null || region.Transitions.Count == 0 || - region.WindowVariants == null || region.WindowVariants.Count == 0) - { - reason = "局部 G2 工作顺序包含空区域或空窗口集。"; - return false; - } - - double previousArc = -1d; - for (int index = 0; index < region.Transitions.Count; index++) - { - CurvatureTransition transition = region.Transitions[index]; - if (transition == null || - transition.SegmentIndex != region.SegmentIndex || - !NumericGuard.IsFinite(transition.LocalArcLengthMeters) || - transition.LocalArcLengthMeters < previousArc) - { - reason = "局部 G2 工作顺序要求区域事件有限、同段且按弧长升序。"; - return false; - } - previousArc = transition.LocalArcLengthMeters; - } - - firstArc = region.Transitions[0].LocalArcLengthMeters; - return true; - } - - private static int Compare(IndexedRegion left, IndexedRegion right) - { - int segment = left.Region.SegmentIndex.CompareTo(right.Region.SegmentIndex); - if (segment != 0) return segment; - int descendingArc = right.FirstArc.CompareTo(left.FirstArc); - return descendingArc != 0 - ? descendingArc - : left.ReportIndex.CompareTo(right.ReportIndex); - } - - private static IReadOnlyList Empty() - { - return new ReadOnlyCollection( - new List()); - } - - private sealed class IndexedRegion - { - internal IndexedRegion( - LocalG2SmoothingRegion region, - int reportIndex, - double firstArc) - { - Region = region; - ReportIndex = reportIndex; - FirstArc = firstArc; - } - - internal LocalG2SmoothingRegion Region { get; } - internal int ReportIndex { get; } - internal double FirstArc { get; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs deleted file mode 100644 index e2e102a..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 一个局部 G2 候选可替换的弧长窗口。 -internal sealed class LocalG2WindowVariant -{ - internal LocalG2WindowVariant(int candidateIndex, double startArcLengthMeters, double endArcLengthMeters, - double leftWindowLengthMeters, double rightWindowLengthMeters) - { - if (candidateIndex < 0 || startArcLengthMeters < 0d || endArcLengthMeters < startArcLengthMeters || - leftWindowLengthMeters < 0d || rightWindowLengthMeters < 0d) - { - throw new ArgumentOutOfRangeException(nameof(candidateIndex)); - } - CandidateIndex = candidateIndex; - StartArcLengthMeters = startArcLengthMeters; - EndArcLengthMeters = endArcLengthMeters; - LeftWindowLengthMeters = leftWindowLengthMeters; - RightWindowLengthMeters = rightWindowLengthMeters; - } - - internal int CandidateIndex { get; } - internal double StartArcLengthMeters { get; } - internal double EndArcLengthMeters { get; } - internal double LeftWindowLengthMeters { get; } - internal double RightWindowLengthMeters { get; } -} - -/// 至少存在一个联合合法生成窗口变体的一组曲率过渡。 -internal sealed class LocalG2SmoothingRegion -{ - internal LocalG2SmoothingRegion( - int segmentIndex, - IReadOnlyList transitions, - double maximumStartArcLengthMeters, - double maximumEndArcLengthMeters, - IReadOnlyList windowVariants) - { - if (segmentIndex < 0 || transitions == null || transitions.Count == 0 || - !NumericGuard.IsFinite(maximumStartArcLengthMeters) || - !NumericGuard.IsFinite(maximumEndArcLengthMeters) || - maximumStartArcLengthMeters < 0d || - maximumEndArcLengthMeters < maximumStartArcLengthMeters || - windowVariants == null || windowVariants.Count == 0) - { - throw new ArgumentOutOfRangeException(nameof(transitions)); - } - SegmentIndex = segmentIndex; - Transitions = Copy(transitions); - MaximumStartArcLengthMeters = maximumStartArcLengthMeters; - MaximumEndArcLengthMeters = maximumEndArcLengthMeters; - WindowVariants = Copy(windowVariants); - } - - internal int SegmentIndex { get; } - internal IReadOnlyList Transitions { get; } - internal double MaximumStartArcLengthMeters { get; } - internal double MaximumEndArcLengthMeters { get; } - internal IReadOnlyList WindowVariants { get; } - - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs deleted file mode 100644 index 26143e2..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 按硬方向边界生成并合并局部 G2 曲率事件的候选窗口。 -internal sealed class LocalG2WindowPlanner -{ - private const double MergeToleranceMeters = 1e-9d; - - internal bool TryPlan( - PreparedPath originalPath, - IReadOnlyList transitions, - LocalG2OptionsSnapshot options, - out IReadOnlyList regions, - out string reason) - { - regions = Empty(); - reason = string.Empty; - if (originalPath == null || transitions == null || options == null) - { - reason = "局部 G2 窗口规划输入无效。"; - return false; - } - if (!TryGetSegmentLengths(originalPath, out Dictionary segmentLengths, out reason)) return false; - - var ordered = new List(transitions.Count); - for (int index = 0; index < transitions.Count; index++) - { - CurvatureTransition transition = transitions[index]; - if (transition == null || !segmentLengths.TryGetValue(transition.SegmentIndex, out double length) || - !NumericGuard.IsFinite(transition.LocalArcLengthMeters) || transition.LocalArcLengthMeters < 0d || - transition.LocalArcLengthMeters > length + MergeToleranceMeters) - { - reason = "局部 G2 曲率事件不属于有效方向分段。"; - return false; - } - ordered.Add(transition); - } - ordered.Sort(CompareTransitions); - - var planned = new List(); - int cursor = 0; - while (cursor < ordered.Count) - { - CurvatureTransition first = ordered[cursor]; - double segmentLength = segmentLengths[first.SegmentIndex]; - var group = new List { first }; - IReadOnlyList variants = - BuildVariants(group, segmentLength, options); - if (variants.Count == 0) - { - reason = "局部 G2 单事件无法生成满足总长度约束的窗口。"; - return false; - } - cursor++; - - while (cursor < ordered.Count && - ordered[cursor].SegmentIndex == first.SegmentIndex) - { - var tentative = new List(group) - { - ordered[cursor], - }; - IReadOnlyList tentativeVariants = - BuildVariants(tentative, segmentLength, options); - if (tentativeVariants.Count == 0) break; - group = tentative; - variants = tentativeVariants; - cursor++; - } - - double minimumStart = double.PositiveInfinity; - double maximumEnd = double.NegativeInfinity; - for (int variantIndex = 0; variantIndex < variants.Count; variantIndex++) - { - minimumStart = Math.Min( - minimumStart, - variants[variantIndex].StartArcLengthMeters); - maximumEnd = Math.Max( - maximumEnd, - variants[variantIndex].EndArcLengthMeters); - } - - planned.Add(new LocalG2SmoothingRegion( - first.SegmentIndex, - group, - minimumStart, - maximumEnd, - variants)); - } - regions = new ReadOnlyCollection(planned); - return true; - } - - private static IReadOnlyList BuildVariants( - IReadOnlyList transitions, - double segmentLength, - LocalG2OptionsSnapshot options) - { - var variants = new List(); - double firstEvent = transitions[0].LocalArcLengthMeters; - double lastEvent = transitions[transitions.Count - 1].LocalArcLengthMeters; - double anchor = (firstEvent + lastEvent) / 2d; - foreach (double target in BuildTargets(options, segmentLength)) - { - if (variants.Count >= options.MaximumCandidatesPerRegion) break; - AddIfLegal(variants, target, 0.5d, anchor, firstEvent, lastEvent, segmentLength, true, options); - AddIfLegal(variants, target, 0.4d, anchor, firstEvent, lastEvent, segmentLength, false, options); - AddIfLegal(variants, target, 0.6d, anchor, firstEvent, lastEvent, segmentLength, false, options); - } - return new ReadOnlyCollection(variants); - } - - private static void AddIfLegal(List variants, double target, double leftRatio, - double anchor, double firstEvent, double lastEvent, double segmentLength, bool permitBoundaryShift, - LocalG2OptionsSnapshot options) - { - if (variants.Count >= options.MaximumCandidatesPerRegion) return; - double left = target * leftRatio; - double right = target - left; - double availableLeft = anchor; - double availableRight = segmentLength - anchor; - if (permitBoundaryShift) - { - left = Math.Min(left, availableLeft); - right = Math.Min(right, availableRight); - double missing = target - left - right; - double addRight = Math.Min(missing, availableRight - right); - right += addRight; - left += Math.Min(missing - addRight, availableLeft - left); - } - else if (left > availableLeft + MergeToleranceMeters || right > availableRight + MergeToleranceMeters) - { - return; - } - - double start = anchor - left; - double end = anchor + right; - if (start > firstEvent + MergeToleranceMeters || end + MergeToleranceMeters < lastEvent || end - start + MergeToleranceMeters < target) - return; - double actualLength = end - start; - if (actualLength + MergeToleranceMeters < options.MinimumWindowLengthMeters || - actualLength > options.MaximumWindowLengthMeters + MergeToleranceMeters) - { - return; - } - variants.Add(new LocalG2WindowVariant(variants.Count, start, end, left, right)); - } - - private static IReadOnlyList BuildTargets(LocalG2OptionsSnapshot options, double segmentLength) - { - if (segmentLength + MergeToleranceMeters < options.MinimumWindowLengthMeters) - return new ReadOnlyCollection(new List()); - - double[] requested = - { - options.PreferredWindowLengthMeters, - 0.75d * options.PreferredWindowLengthMeters, - 1.25d * options.PreferredWindowLengthMeters, - options.MinimumWindowLengthMeters, - options.MaximumWindowLengthMeters, - }; - var targets = new List(requested.Length); - for (int index = 0; index < requested.Length; index++) - { - double target = Math.Min(segmentLength, - Math.Max(options.MinimumWindowLengthMeters, Math.Min(options.MaximumWindowLengthMeters, requested[index]))); - bool duplicate = false; - for (int prior = 0; prior < targets.Count; prior++) - { - if (Math.Abs(targets[prior] - target) <= MergeToleranceMeters) - { - duplicate = true; - break; - } - } - if (!duplicate) targets.Add(target); - } - return targets; - } - - private static bool TryGetSegmentLengths(PreparedPath originalPath, out Dictionary lengths, out string reason) - { - lengths = new Dictionary(); - reason = string.Empty; - for (int position = 0; position < originalPath.Segments.Count; position++) - { - PreparedDirectionSegment segment = originalPath.Segments[position]; - if (segment == null || segment.SegmentIndex != position || segment.Points == null || segment.Points.Count == 0) - { - reason = "局部 G2 窗口规划的预处理方向分段无效。"; - return false; - } - double previousArc = -1d; - for (int pointIndex = 0; pointIndex < segment.Points.Count; pointIndex++) - { - double arc = segment.Points[pointIndex].ArcLength; - if (!NumericGuard.IsFinite(arc) || arc < 0d || arc < previousArc) - { - reason = "局部 G2 窗口规划要求分段弧长有限且非递减。"; - return false; - } - previousArc = arc; - } - lengths.Add(segment.SegmentIndex, previousArc); - } - return true; - } - - private static int CompareTransitions(CurvatureTransition left, CurvatureTransition right) - { - int segment = left.SegmentIndex.CompareTo(right.SegmentIndex); - if (segment != 0) return segment; - int arc = left.LocalArcLengthMeters.CompareTo(right.LocalArcLengthMeters); - return arc != 0 ? arc : left.LeftCoarsePathIndex.CompareTo(right.LeftCoarsePathIndex); - } - - private static IReadOnlyList Empty() => new ReadOnlyCollection(new List()); - - public static class TestHooks - { - public static WindowPlanningTestSnapshot Execute(string scenario) - { - if (string.IsNullOrWhiteSpace(scenario)) - throw new ArgumentException("A scenario is required.", nameof(scenario)); - - IReadOnlyList transitions; - double segmentLength; - switch (scenario) - { - case "SeparatedByOneMeter": - transitions = new[] - { - Transition(0.2d, 0), - Transition(1.2d, 1), - }; - segmentLength = 1.4d; - break; - case "Mergeable": - transitions = new[] - { - Transition(0.4d, 0), - Transition(0.7d, 1), - }; - segmentLength = 1.4d; - break; - case "ThreeEventPartition": - transitions = new[] - { - Transition(0.2d, 0), - Transition(0.6d, 1), - Transition(1.2d, 2), - }; - segmentLength = 1.4d; - break; - case "NearBoundary": - transitions = new[] { Transition(0.1d, 0) }; - segmentLength = 1d; - break; - default: - throw new ArgumentOutOfRangeException(nameof(scenario)); - } - - var planner = new LocalG2WindowPlanner(); - if (!planner.TryPlan( - CreatePreparedPath(segmentLength), - transitions, - new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()), - out IReadOnlyList regions, - out string reason)) - { - throw new InvalidOperationException(reason); - } - - double maximumLength = 0d; - bool exactEnvelope = true; - var counts = new List(regions.Count); - var signature = new List(); - for (int regionIndex = 0; regionIndex < regions.Count; regionIndex++) - { - LocalG2SmoothingRegion region = regions[regionIndex]; - counts.Add(region.Transitions.Count.ToString()); - double minimumStart = double.PositiveInfinity; - double maximumEnd = double.NegativeInfinity; - for (int variantIndex = 0; variantIndex < region.WindowVariants.Count; variantIndex++) - { - LocalG2WindowVariant variant = region.WindowVariants[variantIndex]; - maximumLength = Math.Max( - maximumLength, - variant.EndArcLengthMeters - variant.StartArcLengthMeters); - minimumStart = Math.Min(minimumStart, variant.StartArcLengthMeters); - maximumEnd = Math.Max(maximumEnd, variant.EndArcLengthMeters); - signature.Add( - region.SegmentIndex + ":" + - variant.CandidateIndex + ":" + - variant.StartArcLengthMeters.ToString("R") + ":" + - variant.EndArcLengthMeters.ToString("R")); - } - exactEnvelope &= Math.Abs(region.MaximumStartArcLengthMeters - minimumStart) <= 1e-9d; - exactEnvelope &= Math.Abs(region.MaximumEndArcLengthMeters - maximumEnd) <= 1e-9d; - } - - LocalG2WindowVariant first = regions[0].WindowVariants[0]; - return new WindowPlanningTestSnapshot( - regions.Count, - string.Join(",", counts), - maximumLength, - exactEnvelope, - first.LeftWindowLengthMeters, - first.RightWindowLengthMeters, - string.Join("|", signature)); - } - - public sealed class WindowPlanningTestSnapshot - { - internal WindowPlanningTestSnapshot( - int regionCount, - string transitionCounts, - double maximumWindowLength, - bool exactEnvelope, - double firstLeftLength, - double firstRightLength, - string signature) - { - RegionCount = regionCount; - TransitionCounts = transitionCounts; - MaximumWindowLength = maximumWindowLength; - ExactEnvelope = exactEnvelope; - FirstLeftLength = firstLeftLength; - FirstRightLength = firstRightLength; - Signature = signature; - } - - public int RegionCount { get; } - public string TransitionCounts { get; } - public double MaximumWindowLength { get; } - public bool ExactEnvelope { get; } - public double FirstLeftLength { get; } - public double FirstRightLength { get; } - public string Signature { get; } - } - - private static CurvatureTransition Transition(double arcLength, int index) - { - return new CurvatureTransition( - 0, - index, - index + 1, - arcLength, - arcLength, - 0d, - 0d, - index % 2 == 0 ? 0d : 0.5d, - index % 2 == 0 ? 0.5d : 0d); - } - - private static PreparedPath CreatePreparedPath(double length) - { - var points = new[] - { - new SmoothingPoint2D( - 0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothingPoint2D( - length, 0d, length, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - return new PreparedPath(new[] - { - new PreparedDirectionSegment( - 0, TravelDirection.Forward, points, false, false), - }); - } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs deleted file mode 100644 index 006189f..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 满足两个二维端点二阶边界条件的参数五次 Hermite 曲线。 -internal sealed class QuinticHermiteCurve2D -{ - private readonly double _x0; - private readonly double _x1; - private readonly double _x2; - private readonly double _x3; - private readonly double _x4; - private readonly double _x5; - private readonly double _y0; - private readonly double _y1; - private readonly double _y2; - private readonly double _y3; - private readonly double _y4; - private readonly double _y5; - - private QuinticHermiteCurve2D( - double x0, double x1, double x2, double x3, double x4, double x5, - double y0, double y1, double y2, double y3, double y4, double y5) - { - _x0 = x0; - _x1 = x1; - _x2 = x2; - _x3 = x3; - _x4 = x4; - _x5 = x5; - _y0 = y0; - _y1 = y1; - _y2 = y2; - _y3 = y3; - _y4 = y4; - _y5 = y5; - } - - internal static bool TryCreate( - double x0, double y0, double dx0, double dy0, double ddx0, double ddy0, - double x1, double y1, double dx1, double dy1, double ddx1, double ddy1, - out QuinticHermiteCurve2D curve, - out string reason) - { - curve = null; - reason = string.Empty; - if (!AreFinite(x0, y0, dx0, dy0, ddx0, ddy0, x1, y1, dx1, dy1, ddx1, ddy1)) - { - reason = "五次 Hermite 曲线需要有限的边界条件。"; - return false; - } - if (IsZeroVector(dx0, dy0) || IsZeroVector(dx1, dy1)) - { - reason = "五次 Hermite 曲线端点一阶导数不能为零。"; - return false; - } - - SolveCoordinate(x0, dx0, ddx0, x1, dx1, ddx1, - out double ax0, out double ax1, out double ax2, out double ax3, out double ax4, out double ax5); - SolveCoordinate(y0, dy0, ddy0, y1, dy1, ddy1, - out double ay0, out double ay1, out double ay2, out double ay3, out double ay4, out double ay5); - if (!AreFinite(ax0, ax1, ax2, ax3, ax4, ax5, ay0, ay1, ay2, ay3, ay4, ay5)) - { - reason = "五次 Hermite 曲线系数溢出。"; - return false; - } - - curve = new QuinticHermiteCurve2D(ax0, ax1, ax2, ax3, ax4, ax5, ay0, ay1, ay2, ay3, ay4, ay5); - return true; - } - - internal void Evaluate( - double u, - out double x, out double y, - out double dx, out double dy, - out double ddx, out double ddy) - { - if (!IsFinite(u) || u < 0d || u > 1d) - throw new ArgumentOutOfRangeException(nameof(u), "The curve parameter must be finite and within [0, 1]."); - - x = EvaluateValue(_x0, _x1, _x2, _x3, _x4, _x5, u); - y = EvaluateValue(_y0, _y1, _y2, _y3, _y4, _y5, u); - dx = EvaluateFirstDerivative(_x1, _x2, _x3, _x4, _x5, u); - dy = EvaluateFirstDerivative(_y1, _y2, _y3, _y4, _y5, u); - ddx = EvaluateSecondDerivative(_x2, _x3, _x4, _x5, u); - ddy = EvaluateSecondDerivative(_y2, _y3, _y4, _y5, u); - } - - private static void SolveCoordinate(double p0, double v0, double acceleration0, double p1, double v1, double acceleration1, - out double a0, out double a1, out double a2, out double a3, out double a4, out double a5) - { - a0 = p0; - a1 = v0; - a2 = acceleration0 / 2d; - double c0 = p1 - (a0 + a1 + a2); - double c1 = v1 - (a1 + 2d * a2); - double c2 = acceleration1 - 2d * a2; - a3 = 10d * c0 - 4d * c1 + 0.5d * c2; - a4 = -15d * c0 + 7d * c1 - c2; - a5 = 6d * c0 - 3d * c1 + 0.5d * c2; - } - - private static double EvaluateValue(double a0, double a1, double a2, double a3, double a4, double a5, double u) => - (((((a5 * u + a4) * u + a3) * u + a2) * u + a1) * u) + a0; - - private static double EvaluateFirstDerivative(double a1, double a2, double a3, double a4, double a5, double u) => - ((((5d * a5 * u + 4d * a4) * u + 3d * a3) * u + 2d * a2) * u) + a1; - - private static double EvaluateSecondDerivative(double a2, double a3, double a4, double a5, double u) => - (((20d * a5 * u + 12d * a4) * u + 6d * a3) * u) + 2d * a2; - - private static bool IsZeroVector(double x, double y) => x == 0d && y == 0d; - - private static bool AreFinite(params double[] values) - { - for (int index = 0; index < values.Length; index++) - { - if (!IsFinite(values[index])) return false; - } - return true; - } - - private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs deleted file mode 100644 index 1205368..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 按单一方向段的弧长线性插值并保留精确锚点的确定性重采样器。 -public sealed class ArcLengthResampler -{ - private const double Tolerance = 1e-10d; - - /// 以目标间距重采样一个方向段;段末锚点始终原样保留。 - public bool TryResample( - IReadOnlyList points, - double spacingMeters, - out IReadOnlyList resampled, - out string reason) - { - resampled = EmptyPoints(); - reason = string.Empty; - if (points == null || points.Count == 0 || !NumericGuard.IsPositiveFinite(spacingMeters)) - { - reason = "重采样点集或采样间距无效。"; - return false; - } - - for (int index = 0; index < points.Count; index++) - { - if (!IsValidPoint(points[index])) - { - reason = "重采样输入包含非法数值。"; - return false; - } - if (index == 0) continue; - - SmoothingPoint2D previous = points[index - 1]; - SmoothingPoint2D current = points[index]; - if (current.ArcLength <= previous.ArcLength + Tolerance) - { - reason = "同一方向段的弧长必须严格增加。"; - return false; - } - double distance = Distance(previous, current); - if (!NumericGuard.IsFinite(distance) || distance <= Tolerance) - { - reason = "同一方向段中不允许重复位姿或数值溢出的距离。"; - return false; - } - } - - if (points.Count == 1) - { - resampled = CopyReadOnly(points); - return true; - } - - var output = new List { points[0] }; - double firstArc = points[0].ArcLength; - double finalArc = points[points.Count - 1].ArcLength; - int rightIndex = 1; - for (double targetArc = firstArc + spacingMeters; - targetArc < finalArc - Tolerance; - targetArc += spacingMeters) - { - while (rightIndex < points.Count - 1 && points[rightIndex].ArcLength < targetArc) - rightIndex++; - - SmoothingPoint2D left = points[rightIndex - 1]; - SmoothingPoint2D right = points[rightIndex]; - double ratio = (targetArc - left.ArcLength) / (right.ArcLength - left.ArcLength); - double unwrappedHeading = left.UnwrappedHeading + - ratio * (right.UnwrappedHeading - left.UnwrappedHeading); - output.Add(new SmoothingPoint2D( - left.X + ratio * (right.X - left.X), - left.Y + ratio * (right.Y - left.Y), - targetArc, - AngleMath.NormalizeRadians(unwrappedHeading), - unwrappedHeading, - Math.Min(left.BodyClearance, right.BodyClearance), - false, - SmoothedPathPointSource.Interpolated)); - } - - // Appending the original object, rather than interpolating at the final arc, preserves the exact endpoint. - output.Add(points[points.Count - 1]); - resampled = new ReadOnlyCollection(output); - return true; - } - - /// 重采样一个完整方向段并保留其换向拓扑标记。 - public bool TryResample( - PreparedDirectionSegment segment, - double spacingMeters, - out PreparedDirectionSegment resampled, - out string reason) - { - resampled = null; - reason = string.Empty; - if (segment == null) - { - reason = "待重采样方向段为空。"; - return false; - } - if (!TryResample(segment.Points, spacingMeters, out IReadOnlyList points, out reason)) - return false; - - resampled = new PreparedDirectionSegment( - segment.SegmentIndex, - segment.Direction, - points, - segment.StartsAtGearSwitch, - segment.EndsAtGearSwitch, - segment.StartVehicleCurvaturePerMeter); - return true; - } - - private static bool IsValidPoint(SmoothingPoint2D point) - { - return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && - NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.Heading) && - NumericGuard.IsFinite(point.UnwrappedHeading) && NumericGuard.IsFinite(point.BodyClearance) && - point.BodyClearance >= 0d; - } - - private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) - { - double x = right.X - left.X; - double y = right.Y - left.Y; - return Math.Sqrt(x * x + y * y); - } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } - - private static IReadOnlyList EmptyPoints() - { - return new ReadOnlyCollection(new List()); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs deleted file mode 100644 index 4736eca..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 同一几何分析器产生的路径、方向段和未验证质量统计。 -public sealed class PathGeometryAnalysis -{ - internal PathGeometryAnalysis( - IReadOnlyList path, - IReadOnlyList segments, - double pathLengthMeters, - double maximumAbsoluteVehicleCurvaturePerMeter, - double maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - double rootMeanSquareVehicleCurvaturePerMeter, - double totalAbsoluteCurvatureVariationPerMeter, - double curvatureVariationEnergy, - double minimumBodyClearanceMeters) - { - Path = CopyReadOnly(path); - Segments = CopyReadOnly(segments); - PathLengthMeters = pathLengthMeters; - MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; - MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter = maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter; - RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter; - TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter; - CurvatureVariationEnergy = curvatureVariationEnergy; - MinimumBodyClearanceMeters = minimumBodyClearanceMeters; - } - - /// 完成几何重计算的不可变路径。 - public IReadOnlyList Path { get; } - - /// 完整覆盖 的不可变方向段。 - public IReadOnlyList Segments { get; } - - /// 路径总长度,单位 m。 - public double PathLengthMeters { get; } - - /// 车辆曲率绝对值峰值,单位 1/m。 - public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } - - /// 车辆曲率导数绝对值峰值,单位 1/m²。 - public double MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter { get; } - - /// 车辆曲率均方根,单位 1/m。 - public double RootMeanSquareVehicleCurvaturePerMeter { get; } - - /// 不跨换向点累计的绝对曲率变化,单位 1/m。 - public double TotalAbsoluteCurvatureVariationPerMeter { get; } - - /// 不跨换向点累计的曲率变化代价。 - public double CurvatureVariationEnergy { get; } - - /// 曲率变化代价的面向用户名称;保留 以兼容既有调用方。 - public double CurvatureVariationCost => CurvatureVariationEnergy; - - /// 输入点携带的最小保守净空,单位 m。 - public double MinimumBodyClearanceMeters { get; } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) - { - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - } - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs deleted file mode 100644 index 6153af8..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs +++ /dev/null @@ -1,490 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 在每个单独方向段内统一重采样、恢复航向并计算几何曲率。 -public sealed class PathGeometryAnalyzer -{ - private const double MinimumDistanceMeters = 1e-10d; - private const double BoundaryToleranceMeters = 1e-8d; - - /// - /// 对候选方向段进行确定性几何分析。换向点两侧永不参与同一次差分。 - /// - public bool TryAnalyze( - IReadOnlyList candidateSegments, - double spacingMeters, - out PathGeometryAnalysis analysis, - out string reason) - { - analysis = null; - reason = string.Empty; - if (candidateSegments == null || candidateSegments.Count == 0 || - !NumericGuard.IsPositiveFinite(spacingMeters)) - { - reason = "候选方向段或输出采样间距无效。"; - return false; - } - - var outputPath = new List(); - var outputSegments = new List(); - double cumulativeArcLength = 0d; - double previousOutputHeading = 0d; - double previousOutputUnwrappedHeading = 0d; - bool hasPreviousOutputHeading = false; - double maximumAbsoluteVehicleCurvature = 0d; - double maximumAbsoluteVehicleCurvatureDerivative = 0d; - double curvatureSquareSum = 0d; - int curvatureSampleCount = 0; - double totalCurvatureVariation = 0d; - double curvatureVariationEnergy = 0d; - double minimumClearance = double.PositiveInfinity; - - for (int segmentIndex = 0; segmentIndex < candidateSegments.Count; segmentIndex++) - { - PreparedDirectionSegment segment = candidateSegments[segmentIndex]; - if (!IsValidSegment(segment, segmentIndex, out reason)) return false; - if (!TryValidateBoundary(candidateSegments, segmentIndex, out reason)) return false; - - if (!TryResampleByGeometry(segment.Points, spacingMeters, out IReadOnlyList samples, out reason)) - return false; - - if (!TryAnalyzeSegment( - segment, - samples, - ref cumulativeArcLength, - ref previousOutputHeading, - ref previousOutputUnwrappedHeading, - ref hasPreviousOutputHeading, - outputPath, - out double segmentMaximumCurvature, - out double segmentMaximumCurvatureDerivative, - out double segmentCurvatureSquareSum, - out int segmentCurvatureSampleCount, - out double segmentVariation, - out double segmentVariationEnergy, - out double segmentMinimumClearance, - out reason)) - { - return false; - } - - maximumAbsoluteVehicleCurvature = Math.Max(maximumAbsoluteVehicleCurvature, segmentMaximumCurvature); - maximumAbsoluteVehicleCurvatureDerivative = Math.Max( - maximumAbsoluteVehicleCurvatureDerivative, - segmentMaximumCurvatureDerivative); - curvatureSquareSum += segmentCurvatureSquareSum; - curvatureSampleCount += segmentCurvatureSampleCount; - totalCurvatureVariation += segmentVariation; - curvatureVariationEnergy += segmentVariationEnergy; - minimumClearance = Math.Min(minimumClearance, segmentMinimumClearance); - int endIndex = outputPath.Count - 1; - int startIndex = endIndex - samples.Count + 1; - outputSegments.Add(new SmoothedPathSegment( - segment.SegmentIndex, - segment.Direction, - startIndex, - endIndex, - segment.StartsAtGearSwitch, - segment.EndsAtGearSwitch)); - } - - double rmsCurvature = curvatureSampleCount == 0 ? 0d : Math.Sqrt(curvatureSquareSum / curvatureSampleCount); - analysis = new PathGeometryAnalysis( - outputPath, - outputSegments, - cumulativeArcLength, - maximumAbsoluteVehicleCurvature, - maximumAbsoluteVehicleCurvatureDerivative, - rmsCurvature, - totalCurvatureVariation, - curvatureVariationEnergy, - minimumClearance); - return true; - } - - private static bool TryAnalyzeSegment( - PreparedDirectionSegment segment, - IReadOnlyList samples, - ref double cumulativeArcLength, - ref double previousOutputHeading, - ref double previousOutputUnwrappedHeading, - ref bool hasPreviousOutputHeading, - List output, - out double maximumAbsoluteVehicleCurvature, - out double maximumAbsoluteVehicleCurvatureDerivative, - out double curvatureSquareSum, - out int curvatureSampleCount, - out double totalCurvatureVariation, - out double curvatureVariationEnergy, - out double minimumClearance, - out string reason) - { - maximumAbsoluteVehicleCurvature = 0d; - maximumAbsoluteVehicleCurvatureDerivative = 0d; - curvatureSquareSum = 0d; - curvatureSampleCount = 0; - totalCurvatureVariation = 0d; - curvatureVariationEnergy = 0d; - minimumClearance = double.PositiveInfinity; - reason = string.Empty; - int count = samples.Count; - var localArcLengths = new double[count]; - var headings = new double[count]; - var unwrappedHeadings = new double[count]; - var geometricCurvatures = new double[count]; - var vehicleCurvatures = new double[count]; - var curvatureDerivatives = new double[count]; - - for (int index = 1; index < count; index++) - { - double distance = Distance(samples[index - 1], samples[index]); - if (!NumericGuard.IsFinite(distance) || distance <= MinimumDistanceMeters) - { - reason = "同一方向段中包含重复或退化的路径点。"; - return false; - } - localArcLengths[index] = localArcLengths[index - 1] + distance; - } - - for (int index = 0; index < count; index++) - { - double travelHeading; - if (index == 0 || index == count - 1) - { - // Coarse-path endpoints encode the vehicle pose at the exact integration anchor. - // A chord across a finite integration step is not that pose's heading. - travelHeading = segment.Direction == TravelDirection.Forward - ? samples[index].Heading - : samples[index].Heading - Math.PI; - } - else - { - travelHeading = Math.Atan2(samples[index + 1].Y - samples[index - 1].Y, - samples[index + 1].X - samples[index - 1].X); - } - - double heading = AngleMath.NormalizeRadians( - segment.Direction == TravelDirection.Forward ? travelHeading : travelHeading + Math.PI); - if (!NumericGuard.IsFinite(heading)) - { - reason = "候选路径航向无法归一化。"; - return false; - } - - headings[index] = heading; - if (index == 0 && !hasPreviousOutputHeading) - { - unwrappedHeadings[index] = heading; - } - else if (index == 0) - { - unwrappedHeadings[index] = previousOutputUnwrappedHeading + - AngleMath.ShortestSignedDifference(previousOutputHeading, heading); - } - else - { - unwrappedHeadings[index] = unwrappedHeadings[index - 1] + - AngleMath.ShortestSignedDifference(headings[index - 1], heading); - } - } - - for (int index = 0; index < count; index++) - { - if (count == 1) - { - geometricCurvatures[index] = 0d; - } - else - { - int leftIndex = index == 0 ? 0 : index - 1; - int rightIndex = index == count - 1 ? count - 1 : index + 1; - if (!TryEstimateGeometricCurvature( - samples, - unwrappedHeadings, - leftIndex, - rightIndex, - out geometricCurvatures[index], - out reason)) - { - return false; - } - } - - if (!NumericGuard.IsFinite(geometricCurvatures[index])) - { - reason = "候选路径曲率计算产生了非法数值。"; - return false; - } - } - - for (int index = 0; index < count; index++) - { - double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; - vehicleCurvatures[index] = directionSign * geometricCurvatures[index]; - } - - if (segment.StartVehicleCurvaturePerMeter.HasValue) - { - double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; - vehicleCurvatures[0] = segment.StartVehicleCurvaturePerMeter.Value; - geometricCurvatures[0] = directionSign * vehicleCurvatures[0]; - } - - for (int index = 0; index < count; index++) - { - if (count == 1) - { - curvatureDerivatives[index] = 0d; - } - else if (index == 0) - { - curvatureDerivatives[index] = - (vehicleCurvatures[1] - vehicleCurvatures[0]) / - (localArcLengths[1] - localArcLengths[0]); - } - else if (index == count - 1) - { - curvatureDerivatives[index] = - (vehicleCurvatures[index] - vehicleCurvatures[index - 1]) / - (localArcLengths[index] - localArcLengths[index - 1]); - } - else - { - curvatureDerivatives[index] = - (vehicleCurvatures[index + 1] - vehicleCurvatures[index - 1]) / - (localArcLengths[index + 1] - localArcLengths[index - 1]); - } - - if (!NumericGuard.IsFinite(curvatureDerivatives[index])) - { - reason = "候选路径曲率导数计算产生了非法数值。"; - return false; - } - } - - for (int index = 0; index < count; index++) - { - SmoothingPoint2D sample = samples[index]; - double vehicleCurvature = vehicleCurvatures[index]; - double arcLength = cumulativeArcLength + localArcLengths[index]; - bool isGearSwitch = index == 0 && segment.StartsAtGearSwitch; - SmoothedPathPointSource source = isGearSwitch ? SmoothedPathPointSource.GearSwitch : sample.Source; - output.Add(new SmoothedPathPoint( - sample.X, - sample.Y, - headings[index], - unwrappedHeadings[index], - arcLength, - segment.Direction, - geometricCurvatures[index], - vehicleCurvature, - curvatureDerivatives[index], - sample.BodyClearance, - isGearSwitch, - source)); - - maximumAbsoluteVehicleCurvature = Math.Max(maximumAbsoluteVehicleCurvature, Math.Abs(vehicleCurvature)); - maximumAbsoluteVehicleCurvatureDerivative = Math.Max( - maximumAbsoluteVehicleCurvatureDerivative, - Math.Abs(curvatureDerivatives[index])); - curvatureSquareSum += vehicleCurvature * vehicleCurvature; - curvatureSampleCount++; - minimumClearance = Math.Min(minimumClearance, sample.BodyClearance); - if (index > 0) - { - double deltaCurvature = geometricCurvatures[index] - geometricCurvatures[index - 1]; - double deltaArc = localArcLengths[index] - localArcLengths[index - 1]; - totalCurvatureVariation += Math.Abs(deltaCurvature); - curvatureVariationEnergy += (deltaCurvature / deltaArc) * (deltaCurvature / deltaArc) * deltaArc; - } - } - - cumulativeArcLength += localArcLengths[count - 1]; - previousOutputHeading = headings[count - 1]; - previousOutputUnwrappedHeading = unwrappedHeadings[count - 1]; - hasPreviousOutputHeading = true; - return true; - } - - private static bool TryEstimateGeometricCurvature( - IReadOnlyList samples, - IReadOnlyList unwrappedHeadings, - int leftIndex, - int rightIndex, - out double curvature, - out string reason) - { - curvature = 0d; - reason = string.Empty; - double chordLength = Distance(samples[leftIndex], samples[rightIndex]); - if (!NumericGuard.IsFinite(chordLength) || chordLength <= 0d) - { - reason = "候选路径曲率估计弦长无效。"; - return false; - } - - double deltaHeading = unwrappedHeadings[rightIndex] - unwrappedHeadings[leftIndex]; - if (!NumericGuard.IsFinite(deltaHeading)) - { - reason = "候选路径曲率估计航向差无效。"; - return false; - } - if (Math.Abs(deltaHeading) >= Math.PI) - { - reason = "候选路径曲率估计航向差存在π歧义。"; - return false; - } - - curvature = 2d * Math.Sin(deltaHeading / 2d) / chordLength; - if (!NumericGuard.IsFinite(curvature)) - { - reason = "候选路径曲率计算产生了非法数值。"; - return false; - } - return true; - } - - private static bool TryResampleByGeometry( - IReadOnlyList input, - double spacingMeters, - out IReadOnlyList samples, - out string reason) - { - samples = null; - reason = string.Empty; - var normalized = new List(input.Count); - double localArcLength = 0d; - for (int index = 0; index < input.Count; index++) - { - SmoothingPoint2D point = input[index]; - if (!IsValidPoint(point)) - { - reason = "候选路径点包含非法数值。"; - return false; - } - - if (index > 0) - { - double distance = Distance(input[index - 1], point); - if (!NumericGuard.IsFinite(distance) || distance <= MinimumDistanceMeters) - { - reason = "同一方向段中包含重复或退化的路径点。"; - return false; - } - localArcLength += distance; - } - - normalized.Add(new SmoothingPoint2D( - point.X, point.Y, localArcLength, point.Heading, point.UnwrappedHeading, - point.BodyClearance, point.IsGearSwitchPoint, point.Source)); - } - - if (normalized.Count == 1) - { - samples = normalized; - return true; - } - - var result = new List { normalized[0] }; - double finalArc = normalized[normalized.Count - 1].ArcLength; - int rightIndex = 1; - for (double targetArc = spacingMeters; targetArc < finalArc - MinimumDistanceMeters; targetArc += spacingMeters) - { - while (rightIndex < normalized.Count - 1 && normalized[rightIndex].ArcLength < targetArc) - rightIndex++; - SmoothingPoint2D left = normalized[rightIndex - 1]; - SmoothingPoint2D right = normalized[rightIndex]; - double ratio = (targetArc - left.ArcLength) / (right.ArcLength - left.ArcLength); - double unwrappedHeading = left.UnwrappedHeading + ratio * (right.UnwrappedHeading - left.UnwrappedHeading); - result.Add(new SmoothingPoint2D( - left.X + ratio * (right.X - left.X), - left.Y + ratio * (right.Y - left.Y), - targetArc, - AngleMath.NormalizeRadians(unwrappedHeading), - unwrappedHeading, - Math.Min(left.BodyClearance, right.BodyClearance), - false, - SmoothedPathPointSource.Interpolated)); - } - - result.Add(normalized[normalized.Count - 1]); - samples = result; - return true; - } - - private static bool IsValidSegment(PreparedDirectionSegment segment, int expectedIndex, out string reason) - { - reason = string.Empty; - if (segment == null || segment.SegmentIndex != expectedIndex || - (segment.Direction != TravelDirection.Forward && segment.Direction != TravelDirection.Reverse) || - segment.Points == null || segment.Points.Count == 0) - { - reason = "候选方向段索引、方向或点集无效。"; - return false; - } - return true; - } - - private static bool TryValidateBoundary( - IReadOnlyList segments, - int segmentIndex, - out string reason) - { - reason = string.Empty; - PreparedDirectionSegment current = segments[segmentIndex]; - if (segmentIndex == segments.Count - 1 && current.EndsAtGearSwitch) - { - reason = "末个方向段不得声明不存在的后续换向点。"; - return false; - } - if (segmentIndex == 0) - { - if (current.StartsAtGearSwitch) - { - reason = "首个方向段不得从换向点开始。"; - return false; - } - return true; - } - - PreparedDirectionSegment previous = segments[segmentIndex - 1]; - if (previous == null || !previous.EndsAtGearSwitch || !current.StartsAtGearSwitch || - previous.Direction == current.Direction) - { - reason = "方向段边界必须是前后成对且方向相反的换向点。"; - return false; - } - - SmoothingPoint2D previousEnd = previous.Points[previous.Points.Count - 1]; - SmoothingPoint2D currentStart = current.Points[0]; - if (!currentStart.IsGearSwitchPoint || - Math.Abs(previousEnd.X - currentStart.X) > BoundaryToleranceMeters || - Math.Abs(previousEnd.Y - currentStart.Y) > BoundaryToleranceMeters || - Math.Abs(AngleMath.ShortestSignedDifference(previousEnd.Heading, currentStart.Heading)) > BoundaryToleranceMeters) - { - reason = "换向点两侧必须保留同一位姿和航向。"; - return false; - } - return true; - } - - private static bool IsValidPoint(SmoothingPoint2D point) - { - return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && - NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && - NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.BodyClearance) && - point.BodyClearance >= 0d; - } - - private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) - { - double deltaX = right.X - left.X; - double deltaY = right.Y - left.Y; - return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs deleted file mode 100644 index f8cf80b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 按方向段局部弧长插值平滑算法的原始路径参考。 -internal static class PathReferenceInterpolator -{ - internal static bool TryInterpolateByArcLength( - IReadOnlyList points, - double targetArcLength, - out SmoothingPoint2D reference, - out string reason) - { - reference = null; - reason = string.Empty; - if (points == null || points.Count == 0 || !NumericGuard.IsFinite(targetArcLength)) - { - reason = "原始路径参考点或目标弧长无效。"; - return false; - } - - for (int index = 0; index < points.Count; index++) - { - SmoothingPoint2D point = points[index]; - if (!IsValid(point) || (index > 0 && point.ArcLength < points[index - 1].ArcLength)) - { - reason = "原始路径参考点包含非法数值或非递增弧长。"; - return false; - } - } - - SmoothingPoint2D first = points[0]; - SmoothingPoint2D last = points[points.Count - 1]; - if (targetArcLength < first.ArcLength || targetArcLength > last.ArcLength) - { - reason = "目标弧长不在原始方向段范围内。"; - return false; - } - - if (targetArcLength == first.ArcLength) - { - reference = first; - return true; - } - if (targetArcLength == last.ArcLength) - { - reference = last; - return true; - } - - for (int rightIndex = 1; rightIndex < points.Count; rightIndex++) - { - SmoothingPoint2D left = points[rightIndex - 1]; - SmoothingPoint2D right = points[rightIndex]; - if (targetArcLength > right.ArcLength) continue; - if (targetArcLength == right.ArcLength) - { - reference = right; - return true; - } - - double interval = right.ArcLength - left.ArcLength; - if (!NumericGuard.IsPositiveFinite(interval)) - { - reason = "原始路径参考点包含无法插值的重复弧长。"; - return false; - } - - double ratio = (targetArcLength - left.ArcLength) / interval; - reference = new SmoothingPoint2D( - left.X + ratio * (right.X - left.X), - left.Y + ratio * (right.Y - left.Y), - targetArcLength, - left.Heading + ratio * (right.Heading - left.Heading), - left.UnwrappedHeading + ratio * (right.UnwrappedHeading - left.UnwrappedHeading), - left.BodyClearance + ratio * (right.BodyClearance - left.BodyClearance), - false, - SmoothedPathPointSource.Interpolated); - if (!IsValid(reference)) - { - reference = null; - reason = "原始路径参考插值产生非法数值。"; - return false; - } - return true; - } - - reason = "原始路径参考无法定位目标弧长。"; - return false; - } - - private static bool IsValid(SmoothingPoint2D point) - { - return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && - NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.Heading) && - NumericGuard.IsFinite(point.UnwrappedHeading) && NumericGuard.IsFinite(point.BodyClearance) && - point.ArcLength >= 0d && point.BodyClearance >= 0d; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs deleted file mode 100644 index 23aaa63..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 校验粗路径契约、保护换向拓扑并产生统一间距的平滑输入。 -public sealed class PathSmoothingPreprocessor -{ - private const double Tolerance = 1e-8d; - private readonly ArcLengthResampler _resampler; - - /// 创建使用默认确定性重采样器的预处理器。 - public PathSmoothingPreprocessor() - : this(new ArcLengthResampler()) - { - } - - /// 创建使用指定重采样器的预处理器。 - public PathSmoothingPreprocessor(ArcLengthResampler resampler) - { - _resampler = resampler ?? throw new ArgumentNullException(nameof(resampler)); - } - - /// 将一条粗路径请求校验、按方向拆分并按配置间距重采样。 - public bool TryPrepare(PathSmoothingRequest request, out PreparedPath preparedPath, out string reason) - { - preparedPath = null; - reason = string.Empty; - if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || - request.CoarsePath == null || request.Segments == null || request.CoarsePath.Count == 0 || request.Segments.Count == 0) - { - reason = "平滑请求缺少粗路径、方向分段、地图、车辆或配置。"; - return false; - } - - PathSmoothingConfiguration configuration = request.Configuration; - if (!NumericGuard.IsPositiveFinite(configuration.OutputSpacingMeters)) - { - reason = "平滑输出采样间距无效。"; - return false; - } - - if (!ValidatePathPoints(request.CoarsePath, out reason) || - !ValidateSegments(request.CoarsePath, request.Segments, out reason)) - { - return false; - } - - var preparedSegments = new List(request.Segments.Count); - for (int segmentIndex = 0; segmentIndex < request.Segments.Count; segmentIndex++) - { - PathSegment sourceSegment = request.Segments[segmentIndex]; - double segmentStartArcLength = request.CoarsePath[sourceSegment.StartIndex].ArcLength; - var segmentPoints = new List(sourceSegment.EndIndex - sourceSegment.StartIndex + 1); - for (int pointIndex = sourceSegment.StartIndex; pointIndex <= sourceSegment.EndIndex; pointIndex++) - { - CoarsePathPoint point = request.CoarsePath[pointIndex]; - bool isGearSwitch = point.IsGearSwitchPoint; - segmentPoints.Add(new SmoothingPoint2D( - point.X, - point.Y, - point.ArcLength - segmentStartArcLength, - point.Heading, - point.UnwrappedHeading, - point.BodyClearance, - isGearSwitch, - isGearSwitch ? SmoothedPathPointSource.GearSwitch : SmoothedPathPointSource.Anchor)); - } - - var unresampled = new PreparedDirectionSegment( - sourceSegment.SegmentIndex, - sourceSegment.Direction, - segmentPoints, - sourceSegment.StartsAtGearSwitch, - sourceSegment.EndsAtGearSwitch, - request.CoarsePath[sourceSegment.StartIndex].VehicleCurvature); - if (!_resampler.TryResample(unresampled, configuration.OutputSpacingMeters, out PreparedDirectionSegment resampled, out reason)) - return false; - preparedSegments.Add(new PreparedDirectionSegment( - resampled.SegmentIndex, - resampled.Direction, - resampled.Points, - resampled.StartsAtGearSwitch, - resampled.EndsAtGearSwitch, - unresampled.StartVehicleCurvaturePerMeter)); - } - - preparedPath = new PreparedPath(preparedSegments); - return true; - } - - private static bool ValidatePathPoints(IReadOnlyList path, out string reason) - { - reason = string.Empty; - CoarsePathPoint first = path[0]; - if (!IsValidPoint(first) || first.IsGearSwitchPoint || Math.Abs(first.ArcLength) > Tolerance) - { - reason = "粗路径首点无效。"; - return false; - } - - for (int index = 1; index < path.Count; index++) - { - CoarsePathPoint previous = path[index - 1]; - CoarsePathPoint current = path[index]; - if (!IsValidPoint(current) || current.ArcLength + Tolerance < previous.ArcLength) - { - reason = "粗路径包含非法数值或非递增弧长。"; - return false; - } - - double expectedHeadingDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); - if (!NumericGuard.IsFinite(expectedHeadingDelta) || - Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedHeadingDelta) > Tolerance) - { - reason = "粗路径展开航向不连续。"; - return false; - } - - bool duplicatePoseAndArc = Math.Abs(current.X - previous.X) <= Tolerance && - Math.Abs(current.Y - previous.Y) <= Tolerance && - Math.Abs(current.ArcLength - previous.ArcLength) <= Tolerance && - Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= Tolerance; - if (duplicatePoseAndArc) - { - if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) - { - reason = "粗路径包含非法的重复点。"; - return false; - } - } - else if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + Tolerance) - { - reason = "粗路径普通点必须有正弧长增量且不得标记为换向点。"; - return false; - } - } - return true; - } - - private static bool ValidateSegments( - IReadOnlyList path, - IReadOnlyList segments, - out string reason) - { - reason = string.Empty; - int expectedStartIndex = 0; - for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) - { - PathSegment segment = segments[segmentIndex]; - if (segment == null || segment.SegmentIndex != segmentIndex || segment.StartIndex != expectedStartIndex || - segment.StartIndex < 0 || segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count || - segment.StartsAtGearSwitch != path[segment.StartIndex].IsGearSwitchPoint) - { - reason = "粗路径方向分段索引或起始换向标记无效。"; - return false; - } - - for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) - { - if (path[pointIndex].Direction != segment.Direction) - { - reason = "粗路径方向分段包含不同方向的点。"; - return false; - } - } - - bool hasNextSegment = segmentIndex + 1 < segments.Count; - bool expectedEndsAtGearSwitch = hasNextSegment && segment.EndIndex + 1 < path.Count && - path[segment.EndIndex + 1].IsGearSwitchPoint; - if (segment.EndsAtGearSwitch != expectedEndsAtGearSwitch) - { - reason = "粗路径方向分段末尾换向标记无效。"; - return false; - } - expectedStartIndex = segment.EndIndex + 1; - } - - if (expectedStartIndex != path.Count) - { - reason = "粗路径方向分段未完整覆盖全部点。"; - return false; - } - return true; - } - - private static bool IsValidPoint(CoarsePathPoint point) - { - return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && - NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && - NumericGuard.IsFinite(point.ArcLength) && point.ArcLength >= 0d && - NumericGuard.IsFinite(point.VehicleCurvature) && NumericGuard.IsFinite(point.BodyClearance) && - point.BodyClearance >= 0d && - (point.Direction == TravelDirection.Forward || point.Direction == TravelDirection.Reverse) && - Enum.IsDefined(typeof(CoarsePathPointSource), point.Source) && - Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= Tolerance; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs deleted file mode 100644 index 0f58727..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 已校验、已按单一行驶方向分割并重采样的路径段。 -public sealed class PreparedDirectionSegment -{ - /// 创建不可变方向段。 - public PreparedDirectionSegment( - int segmentIndex, - TravelDirection direction, - IReadOnlyList points, - bool startsAtGearSwitch, - bool endsAtGearSwitch) - : this(segmentIndex, direction, points, startsAtGearSwitch, endsAtGearSwitch, null) - { - } - - /// 创建带有真实起始车辆曲率边界状态的不可变方向段。 - public PreparedDirectionSegment( - int segmentIndex, - TravelDirection direction, - IReadOnlyList points, - bool startsAtGearSwitch, - bool endsAtGearSwitch, - double? startVehicleCurvaturePerMeter) - { - if (segmentIndex < 0) throw new ArgumentOutOfRangeException(nameof(segmentIndex)); - if (points == null || points.Count == 0) throw new ArgumentException("A prepared segment requires points.", nameof(points)); - if (startVehicleCurvaturePerMeter.HasValue && !IsFinite(startVehicleCurvaturePerMeter.Value)) - throw new ArgumentOutOfRangeException(nameof(startVehicleCurvaturePerMeter)); - - SegmentIndex = segmentIndex; - Direction = direction; - Points = CopyReadOnly(points); - StartsAtGearSwitch = startsAtGearSwitch; - EndsAtGearSwitch = endsAtGearSwitch; - StartVehicleCurvaturePerMeter = startVehicleCurvaturePerMeter; - } - - /// 从零开始的分段序号;在 中必须与其位置一致。 - public int SegmentIndex { get; } - - /// 该段的唯一行驶方向。 - public TravelDirection Direction { get; } - - /// 不包含相邻段点的本段不可变采样点。 - public IReadOnlyList Points { get; } - - /// 本段首点是否为换向后保留的新方向点。 - public bool StartsAtGearSwitch { get; } - - /// 本段末点之后是否紧邻换向点。 - public bool EndsAtGearSwitch { get; } - - /// 原始车辆在本段物理起点的曲率边界状态,单位 1/m。 - public double? StartVehicleCurvaturePerMeter { get; } - - private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs deleted file mode 100644 index 379c33c..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 已校验并按方向拆分的粗路径输入快照。 -public sealed class PreparedPath -{ - /// 创建不可变预处理路径。 - public PreparedPath(IReadOnlyList segments) - { - if (segments == null || segments.Count == 0) - throw new ArgumentException("A prepared path requires direction segments.", nameof(segments)); - for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) - { - if (segments[segmentIndex] == null) - throw new ArgumentException("A prepared path cannot contain null direction segments.", nameof(segments)); - } - - Segments = CopyReadOnly(segments); - Points = Flatten(Segments); - } - - /// 按原始前进/倒车拓扑排列的方向段。 - public IReadOnlyList Segments { get; } - - /// 将所有方向段顺序拼接后的点;换向重复点保留两次。 - public IReadOnlyList Points { get; } - - private static IReadOnlyList CopyReadOnly(IReadOnlyList source) - { - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } - - private static IReadOnlyList Flatten(IReadOnlyList segments) - { - var points = new List(); - for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) - { - PreparedDirectionSegment segment = segments[segmentIndex]; - for (int pointIndex = 0; pointIndex < segment.Points.Count; pointIndex++) - points.Add(segment.Points[pointIndex]); - } - return new ReadOnlyCollection(points); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs deleted file mode 100644 index b9d34f8..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 通过候选路径同一几何分析器和验证器构建安全、可公平比较的原始基线。 -internal static class RawPathBaselineBuilder -{ - internal static bool TryCreate( - PathSmoothingRequest request, - PreparedPath preparedPath, - PathGeometryAnalyzer analyzer, - double outputSpacingMeters, - SmoothedPathValidator validator, - double maximumCollisionCheckStepMeters, - out RawPathBaseline baseline, - out string reason) - { - baseline = null; - reason = string.Empty; - if (request == null || preparedPath == null || analyzer == null || validator == null) - { - reason = "原始粗路径基线缺少请求、预处理路径、几何分析器或安全验证器。"; - return false; - } - - if (!analyzer.TryAnalyze(preparedPath.Segments, outputSpacingMeters, out PathGeometryAnalysis analysis, out reason)) - return false; - - if (validator.TryValidate( - analysis.Path, - analysis.Segments, - preparedPath, - request.Map, - request.Vehicle, - maximumCollisionCheckStepMeters, - out IReadOnlyList safePath, - out double minimumClearanceMeters, - out reason)) - { - baseline = new RawPathBaseline(safePath, analysis.Segments, CreateMetrics(analysis, minimumClearanceMeters)); - return true; - } - - if (reason != "平滑路径包含非法数值或超限车辆曲率。" || - !HasTrustedVehicleCurvaturesWithinLimit(request) || - !TryCreateTrustedRawAnalysis(request, out analysis, out reason) || - !validator.TryValidate( - analysis.Path, - analysis.Segments, - preparedPath, - request.Map, - request.Vehicle, - maximumCollisionCheckStepMeters, - out safePath, - out minimumClearanceMeters, - out reason)) - { - return false; - } - - baseline = new RawPathBaseline(safePath, analysis.Segments, CreateMetrics(analysis, minimumClearanceMeters)); - return true; - } - - private static bool HasTrustedVehicleCurvaturesWithinLimit(PathSmoothingRequest request) - { - if (request?.CoarsePath == null || request.CoarsePath.Count == 0 || - !VehicleKinematics.TryGetMaximumCurvaturePerMeter( - request.Vehicle, - out double maximumCurvaturePerMeter)) - { - return false; - } - - for (int index = 0; index < request.CoarsePath.Count; index++) - { - CoarsePathPoint point = request.CoarsePath[index]; - if (point == null || !IsFinite(point.VehicleCurvature) || - Math.Abs(point.VehicleCurvature) > maximumCurvaturePerMeter) - { - return false; - } - } - return true; - } - - private static bool TryCreateTrustedRawAnalysis( - PathSmoothingRequest request, - out PathGeometryAnalysis analysis, - out string reason) - { - analysis = null; - reason = string.Empty; - if (request.CoarsePath == null || request.Segments == null || - request.CoarsePath.Count == 0 || request.Segments.Count == 0) - { - reason = "原始粗路径基线缺少可信路径或方向分段。"; - return false; - } - - var path = new List(request.CoarsePath.Count); - var segments = new List(request.Segments.Count); - double maximumAbsoluteVehicleCurvature = 0d; - double maximumAbsoluteVehicleCurvatureDerivative = 0d; - double curvatureSquareSum = 0d; - int curvatureSampleCount = 0; - double totalCurvatureVariation = 0d; - double curvatureVariationEnergy = 0d; - double minimumClearance = double.PositiveInfinity; - - for (int segmentIndex = 0; segmentIndex < request.Segments.Count; segmentIndex++) - { - PathSegment segment = request.Segments[segmentIndex]; - if (segment == null || segment.SegmentIndex != segmentIndex || - segment.StartIndex != path.Count || segment.EndIndex < segment.StartIndex || - segment.EndIndex >= request.CoarsePath.Count) - { - reason = "原始粗路径基线方向分段无效。"; - return false; - } - - for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) - { - CoarsePathPoint point = request.CoarsePath[pointIndex]; - if (point == null || point.Direction != segment.Direction || - !IsFinite(point.X) || !IsFinite(point.Y) || !IsFinite(point.Heading) || - !IsFinite(point.UnwrappedHeading) || !IsFinite(point.ArcLength) || point.ArcLength < 0d || - !IsFinite(point.VehicleCurvature) || !IsFinite(point.BodyClearance) || point.BodyClearance < 0d) - { - reason = "原始粗路径基线包含非法可信点。"; - return false; - } - - double derivative = EstimateVehicleCurvatureDerivative(request.CoarsePath, segment, pointIndex, out bool validDerivative); - if (!validDerivative) - { - reason = "原始粗路径基线曲率导数弧长无效。"; - return false; - } - - double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; - double geometricCurvature = directionSign * point.VehicleCurvature; - SmoothedPathPointSource source = point.IsGearSwitchPoint - ? SmoothedPathPointSource.GearSwitch - : SmoothedPathPointSource.CoarsePathFallback; - path.Add(new SmoothedPathPoint( - point.X, - point.Y, - point.Heading, - point.UnwrappedHeading, - point.ArcLength, - point.Direction, - geometricCurvature, - point.VehicleCurvature, - derivative, - point.BodyClearance, - point.IsGearSwitchPoint, - source)); - - maximumAbsoluteVehicleCurvature = Math.Max(maximumAbsoluteVehicleCurvature, Math.Abs(point.VehicleCurvature)); - maximumAbsoluteVehicleCurvatureDerivative = Math.Max( - maximumAbsoluteVehicleCurvatureDerivative, - Math.Abs(derivative)); - curvatureSquareSum += point.VehicleCurvature * point.VehicleCurvature; - curvatureSampleCount++; - minimumClearance = Math.Min(minimumClearance, point.BodyClearance); - if (pointIndex > segment.StartIndex) - { - CoarsePathPoint previous = request.CoarsePath[pointIndex - 1]; - double deltaArc = point.ArcLength - previous.ArcLength; - double deltaCurvature = geometricCurvature - - (directionSign * previous.VehicleCurvature); - totalCurvatureVariation += Math.Abs(deltaCurvature); - curvatureVariationEnergy += - (deltaCurvature / deltaArc) * (deltaCurvature / deltaArc) * deltaArc; - } - } - - segments.Add(new SmoothedPathSegment( - segment.SegmentIndex, - segment.Direction, - segment.StartIndex, - segment.EndIndex, - segment.StartsAtGearSwitch, - segment.EndsAtGearSwitch)); - } - - double rmsCurvature = curvatureSampleCount == 0 ? 0d : Math.Sqrt(curvatureSquareSum / curvatureSampleCount); - analysis = new PathGeometryAnalysis( - path, - segments, - request.CoarsePath[request.CoarsePath.Count - 1].ArcLength, - maximumAbsoluteVehicleCurvature, - maximumAbsoluteVehicleCurvatureDerivative, - rmsCurvature, - totalCurvatureVariation, - curvatureVariationEnergy, - minimumClearance); - return true; - } - - private static double EstimateVehicleCurvatureDerivative( - IReadOnlyList path, - PathSegment segment, - int pointIndex, - out bool valid) - { - valid = true; - if (segment.StartIndex == segment.EndIndex) return 0d; - - int leftIndex = pointIndex == segment.StartIndex ? pointIndex : pointIndex - 1; - int rightIndex = pointIndex == segment.EndIndex ? pointIndex : pointIndex + 1; - double deltaArc = path[rightIndex].ArcLength - path[leftIndex].ArcLength; - if (!IsFinite(deltaArc) || deltaArc <= 0d) - { - valid = false; - return 0d; - } - return (path[rightIndex].VehicleCurvature - path[leftIndex].VehicleCurvature) / deltaArc; - } - - private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); - - private static PathQualityMetrics CreateMetrics( - PathGeometryAnalysis analysis, - double minimumClearanceMeters) - { - return new PathQualityMetrics( - true, - analysis.PathLengthMeters, - analysis.MaximumAbsoluteVehicleCurvaturePerMeter, - analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, - analysis.RootMeanSquareVehicleCurvaturePerMeter, - analysis.TotalAbsoluteCurvatureVariationPerMeter, - analysis.CurvatureVariationEnergy, - minimumClearanceMeters, - 0d, - 0d, - 0d, - 0d); - } -} - -/// 已通过完整车体复核的原始粗路径及其比较指标。 -internal sealed class RawPathBaseline -{ - internal RawPathBaseline( - IReadOnlyList path, - IReadOnlyList segments, - PathQualityMetrics metrics) - { - Path = path; - Segments = segments; - Metrics = metrics; - } - - internal IReadOnlyList Path { get; } - internal IReadOnlyList Segments { get; } - internal PathQualityMetrics Metrics { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs deleted file mode 100644 index 1ca0e05..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; - -/// 供平滑算法处理的二维路径采样点;所有长度单位均为 m,航向单位为 rad。 -public sealed class SmoothingPoint2D -{ - /// 创建不可变二维路径点。 - public SmoothingPoint2D( - double xMeters, - double yMeters, - double arcLengthMeters, - double headingRadians, - double unwrappedHeadingRadians, - double bodyClearanceMeters, - bool isGearSwitchPoint, - SmoothedPathPointSource source) - { - X = xMeters; - Y = yMeters; - ArcLength = arcLengthMeters; - Heading = headingRadians; - UnwrappedHeading = unwrappedHeadingRadians; - BodyClearance = bodyClearanceMeters; - IsGearSwitchPoint = isGearSwitchPoint; - Source = source; - } - - /// 世界 X 坐标,单位 m。 - public double X { get; } - - /// 世界 Y 坐标,单位 m。 - public double Y { get; } - - /// 本方向段中的累计弧长,单位 m。 - public double ArcLength { get; } - - /// 归一化的车辆航向,单位 rad。 - public double Heading { get; } - - /// 连续展开的车辆航向,单位 rad。 - public double UnwrappedHeading { get; } - - /// 输入路径携带的保守净空,单位 m。 - public double BodyClearance { get; } - - /// 该点是否为新方向段开始处的换向点。 - public bool IsGearSwitchPoint { get; } - - /// 该点在平滑流程中的来源。 - public SmoothedPathPointSource Source { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md deleted file mode 100644 index 80d2640..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Path smoothing comparison - -This module compares the clamped cubic B-spline, local cubic Bézier, and piecewise-quintic smoothers against the raw Hybrid A* coarse path. It is an offline developer analysis tool; it does not alter coarse-path search acceptance. - -## Units and coordinates - -All path positions, lengths, clearances, and collision-check spacing use **meters**. Heading uses **radians** and vehicle curvature uses `1/m`. Map construction retains its existing millimeter input contract; `PlanningGridMap` provides the meter-coordinate world queries used by smoothing. A smoothing result preserves the coarse path's first and last pose, rather than the original requested goal pose, so its endpoint error is inherited from the coarse planner's accepted goal tolerance. - -## Facade usage - -Only smooth a successful coarse result. A minimal formal call flow is: - -```csharp -if (coarseResult.PlanningResult.Status != PlanningStatus.Success) - return; -var smoothing = new PathSmoothingService().Smooth( - new PathSmoothingRequest( - coarseResult.PlanningResult.Path, - coarseResult.PlanningResult.Segments, - coarseResult.MapResult.Map, - job.Vehicle, - smoothingConfiguration), - cancellationToken); -if (smoothing.Status == PathSmoothingStatus.Success || - smoothing.Status == PathSmoothingStatus.FallbackToCoarsePath) - ConsumeSpatialReference(smoothing.Path, smoothing.Segments); -``` - -## Status handling and fallback - -`Success` supplies a validated smoothed path. `FallbackToCoarsePath` is an explicit, safe degraded result and may be consumed by the same downstream spatial-reference interface. `Infeasible`, `InvalidInput`, `Failed`, and `Cancelled` must not be treated as a path. Comparison reports keep an infeasible candidate's markers for diagnosis but never select it as a recommendation. - -## Fixture freshness - -The eight fast fixtures are snapshots of successful coarse paths. Their configuration fingerprint is checked before use. After deliberately changing a scenario or planning configuration, regenerate them with `generate_path_smoothing_fixtures.ps1 -Overwrite`, then run `verify_path_smoothing_fixtures.ps1`. Fixture-only comparison never runs Hybrid A*. - -## IEEE colors, fonts, and Windows PNG - -SVG and PNG use the shared IEEE-style colors, status-bearing legends, coordinate ticks, and units. Every raw or smoothed trajectory is rendered as its complete set of discrete samples: reports intentionally draw no line segment between adjacent samples. The Windows PNG renderer requires the exact `SimSun` and `Times New Roman` font families and writes a 600 dpi raster image. If either font is unavailable, export returns `FontUnavailable`; it does not substitute a different font. PNG rendering relies on Windows GDI+, while SVG and CSV remain available without it. SVG uses text-family references, so portable publication requires checking the target font installation or converting text to paths in an external publishing tool. - -## SQP boundary - -This module is geometric smoothing and full-body validation, not sequential quadratic programming (SQP) trajectory optimization. It has no time parameterization, velocity, acceleration, steering-rate, or dynamic-obstacle constraints. Feed only its validated spatial reference into any later SQP or time-parameterization stage. - -## Output files - -`run_path_smoothing_comparison.ps1` writes developer reports only below `ClumsyPilot/obj/path_smoothing_reports`. Each scenario directory contains one `comparison.csv` and both SVG and 600 dpi PNG versions of these six focused figures: - -1. `01-coarse-path-overview` — the Hybrid A* coarse-path planning view with map, start, and goal. -2. `02-all-paths-comparison` — raw and all three smoother point clouds only, without map or endpoint decorations. -3. `03-cubic-bspline-overview` — faded coarse reference and cubic B-spline result with map context. -4. `04-local-cubic-bezier-overview` — faded coarse reference and local cubic Bézier result with map context. -5. `05-piecewise-quintic-overview` — faded coarse reference and piecewise-quintic result with map context. -6. `06-curvature-comparison` — all available curvature samples with `s (m)` and `κ (m⁻¹)` axes. - -Overhead figures preserve equal X/Y scale and use trajectory-focused bounds; their coordinate ticks are in metres. Inspect the SVG/PNG visually, retain CSV for numerical review, and use an external PDF/EPS publishing step if the final venue requires those formats. diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json deleted file mode 100644 index cbd08f2..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json +++ /dev/null @@ -1,19451 +0,0 @@ -{ - "schemaVersion": 1, - "scenarios": [ - { - "id": "straight", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:d42ce997e1e51d421fe923c5819a332706007d900cc74a39d4b0ac88c6ac4a21", - "map": { - "xMinMm": 0.0, - "xMaxMm": 6000.0, - "yMinMm": 0.0, - "yMaxMm": 4000.0, - "resolutionMm": 50.0, - "obstacles": [] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.025, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.024999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.049999999999999822, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.074999999999999734, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.099999999999999645, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.12499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.14999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1749999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.17499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1999999999999993, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.19999999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.2249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2499999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.24999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.274999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.274999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2999999999999989, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.29999999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3249999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.32499999999999885, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3499999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.34999999999999876, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3749999999999987, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.37499999999999867, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3999999999999986, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.39999999999999858, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4249999999999985, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.42499999999999849, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4499999999999984, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.4499999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4749999999999983, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.47499999999999831, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4999999999999982, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.49999999999999822, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5249999999999981, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.52499999999999813, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.549999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.549999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.574999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.574999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5999999999999979, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.59999999999999787, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6249999999999978, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.62499999999999778, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6499999999999977, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.64999999999999769, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6749999999999976, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.6749999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6999999999999975, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.69999999999999751, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7249999999999974, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.72499999999999742, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7499999999999973, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.74999999999999734, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7749999999999972, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.77499999999999725, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7999999999999972, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.79999999999999716, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8249999999999971, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.82499999999999707, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.849999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.849999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8749999999999969, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.87499999999999689, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8999999999999968, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.8999999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9249999999999967, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.92499999999999671, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9499999999999966, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.94999999999999662, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9749999999999965, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.97499999999999654, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9999999999999964, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.99999999999999645, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0249999999999964, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0249999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0499999999999963, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0499999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0749999999999962, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0749999999999962, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0999999999999961, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0999999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.124999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.124999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1499999999999959, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1499999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1749999999999958, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1749999999999958, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1999999999999957, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1999999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2249999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2249999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2499999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2749999999999955, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2749999999999955, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2999999999999954, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2999999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3249999999999953, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3249999999999953, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3499999999999952, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3499999999999952, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3749999999999951, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3749999999999951, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.399999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4249999999999949, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4249999999999949, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4499999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4499999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4749999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4749999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4999999999999947, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5249999999999946, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5249999999999946, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5499999999999945, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5499999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5749999999999944, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5749999999999944, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5999999999999943, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5999999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6249999999999942, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.6249999999999942, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6499999999999941, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.6499999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.674999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.674999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.699999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.699999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7249999999999939, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7249999999999939, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7499999999999938, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7749999999999937, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7749999999999937, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7999999999999936, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8249999999999935, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8249999999999935, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8499999999999934, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8499999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8749999999999933, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8749999999999933, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8999999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8999999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9249999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9249999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9499999999999931, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9499999999999931, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.974999999999993, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.974999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9999999999999929, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9999999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0249999999999928, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0249999999999928, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0499999999999927, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0499999999999927, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0749999999999926, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0749999999999926, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0999999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0999999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1249999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1249999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1499999999999924, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1499999999999924, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1749999999999923, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1749999999999923, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1999999999999922, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1999999999999922, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2249999999999921, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2249999999999921, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2749999999999919, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2749999999999919, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2999999999999918, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2999999999999918, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3249999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3249999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3499999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3499999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3749999999999916, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3749999999999916, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3999999999999915, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3999999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4249999999999914, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4249999999999914, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4499999999999913, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4499999999999913, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4749999999999912, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4749999999999912, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4999999999999911, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.524999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.524999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5499999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5499999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5749999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5749999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5999999999999908, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5999999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6249999999999907, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6249999999999907, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6499999999999906, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6499999999999906, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6749999999999905, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6749999999999905, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6999999999999904, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6999999999999904, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7249999999999903, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.7249999999999903, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7499999999999902, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.7499999999999902, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.77499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.77499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.79999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.79999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.82499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.82499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.84999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.84999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.87499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.87499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8999999999999897, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.8999999999999897, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9249999999999896, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9249999999999896, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9499999999999895, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9499999999999895, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9749999999999894, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9749999999999894, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9999999999999893, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9999999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.02499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0249999999999897, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.04999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.04999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.07499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0749999999999904, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0999999999999908, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0999999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1249999999999911, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1249999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1499999999999915, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1499999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1749999999999918, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1749999999999918, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1999999999999922, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1999999999999922, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2249999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2249999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2499999999999929, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2499999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2749999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2749999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2999999999999936, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.324999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.324999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3499999999999943, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.3499999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3749999999999947, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.3749999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.399999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4249999999999954, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4249999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4499999999999957, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4499999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4749999999999961, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4749999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4999999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4999999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5249999999999959, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5249999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5499999999999963, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5499999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5749999999999966, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5749999999999966, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.599999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.599999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6249999999999973, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6249999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6499999999999977, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6499999999999977, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.674999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.674999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6999999999999984, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7249999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7249999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7499999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7749999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7749999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.8, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.825, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.825, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8500000000000005, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.8500000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 154, - "startsAtGearSwitch": false, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "single-turn", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:49571e2eeb2e58f0a5dc0a56e61e530700c3da7d60e2a9c66809721963e5d9f8", - "map": { - "xMinMm": -7000.0, - "xMaxMm": 11000.0, - "yMinMm": -7000.0, - "yMaxMm": 10000.0, - "resolutionMm": 50.0, - "obstacles": [] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 1.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.0249995478901843, - "yMeters": 1.0001302071559617, - "headingRadians": 0.010416666666666666, - "unwrappedHeadingRadians": 0.010416666666666666, - "arcLengthMeters": 0.025, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499963831803427, - "yMeters": 1.0005208144955946, - "headingRadians": 0.020833333333333332, - "unwrappedHeadingRadians": 0.020833333333333332, - "arcLengthMeters": 0.05, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749877935647825, - "yMeters": 1.0011717796356725, - "headingRadians": 0.03125, - "unwrappedHeadingRadians": 0.03125, - "arcLengthMeters": 0.075000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999710673264458, - "yMeters": 1.0020830319425964, - "headingRadians": 0.041666666666666664, - "unwrappedHeadingRadians": 0.041666666666666664, - "arcLengthMeters": 0.1, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249434936311464, - "yMeters": 1.0032544725400563, - "headingRadians": 0.052083333333333329, - "unwrappedHeadingRadians": 0.052083333333333329, - "arcLengthMeters": 0.125, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499023628217124, - "yMeters": 1.004685974319762, - "headingRadians": 0.062499999999999993, - "unwrappedHeadingRadians": 0.062499999999999993, - "arcLengthMeters": 0.15, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1748449667119993, - "yMeters": 1.0063773819552337, - "headingRadians": 0.072916666666666657, - "unwrappedHeadingRadians": 0.072916666666666657, - "arcLengthMeters": 0.175, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1997685988807447, - "yMeters": 1.0083285119186571, - "headingRadians": 0.083333333333333329, - "unwrappedHeadingRadians": 0.083333333333333329, - "arcLengthMeters": 0.2, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.224670554965231, - "yMeters": 1.0105391525007958, - "headingRadians": 0.09375, - "unwrappedHeadingRadians": 0.09375, - "arcLengthMeters": 0.22500000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2495481329547253, - "yMeters": 1.0130090638339635, - "headingRadians": 0.10416666666666667, - "unwrappedHeadingRadians": 0.10416666666666667, - "arcLengthMeters": 0.25000000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2743986334836632, - "yMeters": 1.0157379779180522, - "headingRadians": 0.11458333333333334, - "unwrappedHeadingRadians": 0.11458333333333334, - "arcLengthMeters": 0.27500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2992193601245465, - "yMeters": 1.0187255986496104, - "headingRadians": 0.125, - "unwrappedHeadingRadians": 0.125, - "arcLengthMeters": 0.30000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3240076196805208, - "yMeters": 1.0219716018539722, - "headingRadians": 0.13541666666666666, - "unwrappedHeadingRadians": 0.13541666666666666, - "arcLengthMeters": 0.325, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3487607224776048, - "yMeters": 1.0254756353204335, - "headingRadians": 0.14583333333333331, - "unwrappedHeadingRadians": 0.14583333333333331, - "arcLengthMeters": 0.35, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3734759826565346, - "yMeters": 1.0292373188404673, - "headingRadians": 0.15624999999999997, - "unwrappedHeadingRadians": 0.15624999999999997, - "arcLengthMeters": 0.37499999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3981507184641961, - "yMeters": 1.0332562442489797, - "headingRadians": 0.16666666666666663, - "unwrappedHeadingRadians": 0.16666666666666663, - "arcLengthMeters": 0.39999999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4227822525446114, - "yMeters": 1.0375319754685981, - "headingRadians": 0.17708333333333329, - "unwrappedHeadingRadians": 0.17708333333333329, - "arcLengthMeters": 0.42499999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4473679122294476, - "yMeters": 1.0420640485569872, - "headingRadians": 0.18749999999999994, - "unwrappedHeadingRadians": 0.18749999999999994, - "arcLengthMeters": 0.44999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4719050298280172, - "yMeters": 1.0468519717571914, - "headingRadians": 0.1979166666666666, - "unwrappedHeadingRadians": 0.1979166666666666, - "arcLengthMeters": 0.47499999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4963909429167384, - "yMeters": 1.051895225550991, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.49999999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5208229946280243, - "yMeters": 1.0571932627152754, - "headingRadians": 0.21874999999999986, - "unwrappedHeadingRadians": 0.21874999999999986, - "arcLengthMeters": 0.52499999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5451985339385683, - "yMeters": 1.0627455083814181, - "headingRadians": 0.22916666666666652, - "unwrappedHeadingRadians": 0.22916666666666652, - "arcLengthMeters": 0.54999999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5695149159569968, - "yMeters": 1.0685513600976548, - "headingRadians": 0.23958333333333318, - "unwrappedHeadingRadians": 0.23958333333333318, - "arcLengthMeters": 0.57499999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5937695022108547, - "yMeters": 1.0746101878944523, - "headingRadians": 0.24999999999999983, - "unwrappedHeadingRadians": 0.24999999999999983, - "arcLengthMeters": 0.59999999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6179596609328961, - "yMeters": 1.0809213343528641, - "headingRadians": 0.26041666666666652, - "unwrappedHeadingRadians": 0.26041666666666652, - "arcLengthMeters": 0.62499999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6420827673466467, - "yMeters": 1.0874841146758643, - "headingRadians": 0.2708333333333332, - "unwrappedHeadingRadians": 0.2708333333333332, - "arcLengthMeters": 0.6499999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6661362039512069, - "yMeters": 1.0942978167626514, - "headingRadians": 0.28124999999999989, - "unwrappedHeadingRadians": 0.28124999999999989, - "arcLengthMeters": 0.67499999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6901173608052673, - "yMeters": 1.1013617012859171, - "headingRadians": 0.29166666666666657, - "unwrappedHeadingRadians": 0.29166666666666657, - "arcLengthMeters": 0.69999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7140236358103025, - "yMeters": 1.108675001772067, - "headingRadians": 0.30208333333333326, - "unwrappedHeadingRadians": 0.30208333333333326, - "arcLengthMeters": 0.72499999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7378524349929139, - "yMeters": 1.1162369246843864, - "headingRadians": 0.31249999999999994, - "unwrappedHeadingRadians": 0.31249999999999994, - "arcLengthMeters": 0.74999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7616011727862919, - "yMeters": 1.1240466495091457, - "headingRadians": 0.32291666666666663, - "unwrappedHeadingRadians": 0.32291666666666663, - "arcLengthMeters": 0.77499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7852672723107652, - "yMeters": 1.1321033288446294, - "headingRadians": 0.33333333333333331, - "unwrappedHeadingRadians": 0.33333333333333331, - "arcLengthMeters": 0.79999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8088481656534072, - "yMeters": 1.1404060884930853, - "headingRadians": 0.34375, - "unwrappedHeadingRadians": 0.34375, - "arcLengthMeters": 0.825, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8323412941466704, - "yMeters": 1.1489540275555796, - "headingRadians": 0.35416666666666669, - "unwrappedHeadingRadians": 0.35416666666666669, - "arcLengthMeters": 0.85, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8557441086460165, - "yMeters": 1.15774621852975, - "headingRadians": 0.36458333333333337, - "unwrappedHeadingRadians": 0.36458333333333337, - "arcLengthMeters": 0.875, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8790540698065141, - "yMeters": 1.1667817074104456, - "headingRadians": 0.37500000000000006, - "unwrappedHeadingRadians": 0.37500000000000006, - "arcLengthMeters": 0.9, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9022686483583733, - "yMeters": 1.1760595137932413, - "headingRadians": 0.38541666666666674, - "unwrappedHeadingRadians": 0.38541666666666674, - "arcLengthMeters": 0.925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9253853253813855, - "yMeters": 1.1855786309808183, - "headingRadians": 0.39583333333333343, - "unwrappedHeadingRadians": 0.39583333333333343, - "arcLengthMeters": 0.95000000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9484015925782421, - "yMeters": 1.1953380260921962, - "headingRadians": 0.40625000000000011, - "unwrappedHeadingRadians": 0.40625000000000011, - "arcLengthMeters": 0.97500000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9713149525466993, - "yMeters": 1.2053366401748073, - "headingRadians": 0.41666666666666674, - "unwrappedHeadingRadians": 0.41666666666666674, - "arcLengthMeters": 1.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9941229190505612, - "yMeters": 1.2155733883193989, - "headingRadians": 0.42708333333333343, - "unwrappedHeadingRadians": 0.42708333333333343, - "arcLengthMeters": 1.0250000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0168230172894512, - "yMeters": 1.2260471597777527, - "headingRadians": 0.43750000000000011, - "unwrappedHeadingRadians": 0.43750000000000011, - "arcLengthMeters": 1.0500000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0394127841673431, - "yMeters": 1.2367568180832067, - "headingRadians": 0.4479166666666668, - "unwrappedHeadingRadians": 0.4479166666666668, - "arcLengthMeters": 1.0750000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.061889768559821, - "yMeters": 1.2477012011739703, - "headingRadians": 0.45833333333333348, - "unwrappedHeadingRadians": 0.45833333333333348, - "arcLengthMeters": 1.1000000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0842515315800414, - "yMeters": 1.2588791215192134, - "headingRadians": 0.46875000000000017, - "unwrappedHeadingRadians": 0.46875000000000017, - "arcLengthMeters": 1.1250000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1064956468433662, - "yMeters": 1.2702893662479213, - "headingRadians": 0.47916666666666685, - "unwrappedHeadingRadians": 0.47916666666666685, - "arcLengthMeters": 1.1500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1286197007306416, - "yMeters": 1.281930697280498, - "headingRadians": 0.48958333333333354, - "unwrappedHeadingRadians": 0.48958333333333354, - "arcLengthMeters": 1.1750000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1506212926500878, - "yMeters": 1.2938018514631056, - "headingRadians": 0.50000000000000022, - "unwrappedHeadingRadians": 0.50000000000000022, - "arcLengthMeters": 1.2000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.17249803529778, - "yMeters": 1.3059015407047243, - "headingRadians": 0.51041666666666685, - "unwrappedHeadingRadians": 0.51041666666666685, - "arcLengthMeters": 1.225000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1942475549166827, - "yMeters": 1.3182284521169172, - "headingRadians": 0.52083333333333348, - "unwrappedHeadingRadians": 0.52083333333333348, - "arcLengthMeters": 1.2500000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2158674915542185, - "yMeters": 1.3307812481562866, - "headingRadians": 0.53125000000000011, - "unwrappedHeadingRadians": 0.53125000000000011, - "arcLengthMeters": 1.2750000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.237355499318336, - "yMeters": 1.3435585667696071, - "headingRadians": 0.54166666666666674, - "unwrappedHeadingRadians": 0.54166666666666674, - "arcLengthMeters": 1.3000000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2587092466320535, - "yMeters": 1.3565590215416132, - "headingRadians": 0.55208333333333337, - "unwrappedHeadingRadians": 0.55208333333333337, - "arcLengthMeters": 1.3250000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2799264164864486, - "yMeters": 1.3697812018454369, - "headingRadians": 0.5625, - "unwrappedHeadingRadians": 0.5625, - "arcLengthMeters": 1.3500000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3010047066920678, - "yMeters": 1.3832236729956673, - "headingRadians": 0.57291666666666663, - "unwrappedHeadingRadians": 0.57291666666666663, - "arcLengthMeters": 1.3750000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3219418301287273, - "yMeters": 1.3968849764040232, - "headingRadians": 0.58333333333333326, - "unwrappedHeadingRadians": 0.58333333333333326, - "arcLengthMeters": 1.4000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3427355149936804, - "yMeters": 1.410763629737618, - "headingRadians": 0.59374999999999989, - "unwrappedHeadingRadians": 0.59374999999999989, - "arcLengthMeters": 1.4250000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3633835050481196, - "yMeters": 1.4248581270798029, - "headingRadians": 0.60416666666666652, - "unwrappedHeadingRadians": 0.60416666666666652, - "arcLengthMeters": 1.4500000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3838835598619945, - "yMeters": 1.4391669390935662, - "headingRadians": 0.61458333333333315, - "unwrappedHeadingRadians": 0.61458333333333315, - "arcLengthMeters": 1.475, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4042334550571089, - "yMeters": 1.4536885131874766, - "headingRadians": 0.62499999999999978, - "unwrappedHeadingRadians": 0.62499999999999978, - "arcLengthMeters": 1.5, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4244309825484822, - "yMeters": 1.4684212736841487, - "headingRadians": 0.63541666666666641, - "unwrappedHeadingRadians": 0.63541666666666641, - "arcLengthMeters": 1.525, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4444739507839373, - "yMeters": 1.4833636219912125, - "headingRadians": 0.645833333333333, - "unwrappedHeadingRadians": 0.645833333333333, - "arcLengthMeters": 1.5499999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4643601849818983, - "yMeters": 1.4985139367747708, - "headingRadians": 0.65624999999999967, - "unwrappedHeadingRadians": 0.65624999999999967, - "arcLengthMeters": 1.5749999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4840875273673682, - "yMeters": 1.5138705741353242, - "headingRadians": 0.6666666666666663, - "unwrappedHeadingRadians": 0.6666666666666663, - "arcLengthMeters": 1.5999999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5036538374060568, - "yMeters": 1.5294318677861423, - "headingRadians": 0.67708333333333293, - "unwrappedHeadingRadians": 0.67708333333333293, - "arcLengthMeters": 1.6249999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5230569920366457, - "yMeters": 1.5451961292340677, - "headingRadians": 0.68749999999999956, - "unwrappedHeadingRadians": 0.68749999999999956, - "arcLengthMeters": 1.6499999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5422948859011503, - "yMeters": 1.5611616479627259, - "headingRadians": 0.69791666666666619, - "unwrappedHeadingRadians": 0.69791666666666619, - "arcLengthMeters": 1.6749999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5613654315733649, - "yMeters": 1.5773266916181279, - "headingRadians": 0.70833333333333282, - "unwrappedHeadingRadians": 0.70833333333333282, - "arcLengthMeters": 1.6999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5802665597853611, - "yMeters": 1.5936895061966398, - "headingRadians": 0.71874999999999944, - "unwrappedHeadingRadians": 0.71874999999999944, - "arcLengthMeters": 1.7249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5989962196520144, - "yMeters": 1.6102483162353034, - "headingRadians": 0.72916666666666607, - "unwrappedHeadingRadians": 0.72916666666666607, - "arcLengthMeters": 1.7499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6175523788935391, - "yMeters": 1.6270013250044848, - "headingRadians": 0.7395833333333327, - "unwrappedHeadingRadians": 0.7395833333333327, - "arcLengthMeters": 1.774999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6359330240560008, - "yMeters": 1.6439467147028288, - "headingRadians": 0.74999999999999933, - "unwrappedHeadingRadians": 0.74999999999999933, - "arcLengthMeters": 1.7999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6541361607297906, - "yMeters": 1.661082646654503, - "headingRadians": 0.760416666666666, - "unwrappedHeadingRadians": 0.760416666666666, - "arcLengthMeters": 1.8249999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6721598137660285, - "yMeters": 1.6784072615087038, - "headingRadians": 0.77083333333333259, - "unwrappedHeadingRadians": 0.77083333333333259, - "arcLengthMeters": 1.8499999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6900020274908796, - "yMeters": 1.6959186794414074, - "headingRadians": 0.78124999999999922, - "unwrappedHeadingRadians": 0.78124999999999922, - "arcLengthMeters": 1.8749999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7076608659177559, - "yMeters": 1.7136150003593427, - "headingRadians": 0.79166666666666585, - "unwrappedHeadingRadians": 0.79166666666666585, - "arcLengthMeters": 1.8999999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7251344129573836, - "yMeters": 1.731494304106161, - "headingRadians": 0.80208333333333248, - "unwrappedHeadingRadians": 0.80208333333333248, - "arcLengthMeters": 1.9249999999999985, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7424207726257084, - "yMeters": 1.7495546506707869, - "headingRadians": 0.81249999999999911, - "unwrappedHeadingRadians": 0.81249999999999911, - "arcLengthMeters": 1.9499999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7595180692496233, - "yMeters": 1.767794080397918, - "headingRadians": 0.82291666666666574, - "unwrappedHeadingRadians": 0.82291666666666574, - "arcLengthMeters": 1.9749999999999983, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7764244476704873, - "yMeters": 1.786210614200662, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 1.9999999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7932347537725639, - "yMeters": 1.8047150355305628, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.0249999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8100450598746405, - "yMeters": 1.8232194568604636, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.049999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.826855365976717, - "yMeters": 1.8417238781903644, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.074999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8436656720787936, - "yMeters": 1.8602282995202652, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.0999999999999979, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.86047597818087, - "yMeters": 1.8787327208501661, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.1249999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8772862842829467, - "yMeters": 1.8972371421800669, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.1499999999999977, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8940965903850233, - "yMeters": 1.9157415635099677, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 2.1749999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 24.758836806279895, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 87, - "startsAtGearSwitch": false, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "s-bend", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:810b601e9090644292e4eb5652379f5f0db6fa7158aaf6e4faeae4b337ad5ec5", - "map": { - "xMinMm": 0.0, - "xMaxMm": 6000.0, - "yMinMm": 0.0, - "yMaxMm": 4000.0, - "resolutionMm": 50.0, - "obstacles": [ - { - "kind": "circle", - "xMinMm": 0.0, - "xMaxMm": 0.0, - "yMinMm": 0.0, - "yMaxMm": 0.0, - "centerXMm": 2400.0, - "centerYMm": 1300.0, - "radiusMm": 220.0 - }, - { - "kind": "axis-aligned-rectangle", - "xMinMm": 3000.0, - "xMaxMm": 3600.0, - "yMinMm": 2000.0, - "yMaxMm": 2600.0, - "centerXMm": 0.0, - "centerYMm": 0.0, - "radiusMm": 0.0 - }, - { - "kind": "circle", - "xMinMm": 0.0, - "xMaxMm": 0.0, - "yMinMm": 0.0, - "yMaxMm": 0.0, - "centerXMm": 3720.0, - "centerYMm": 2320.0, - "radiusMm": 140.0 - }, - { - "kind": "circle", - "xMinMm": 0.0, - "xMaxMm": 0.0, - "yMinMm": 0.0, - "yMaxMm": 0.0, - "centerXMm": 3720.0, - "centerYMm": 2680.0, - "radiusMm": 140.0 - } - ] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 1.0, - "headingRadians": 0.78539816339744828, - "unwrappedHeadingRadians": 0.78539816339744828, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.0175852794768074, - "yMeters": 1.0177694202026868, - "headingRadians": 0.79581483006411491, - "unwrappedHeadingRadians": 0.79581483006411491, - "arcLengthMeters": 0.024999999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0349845101200461, - "yMeters": 1.0357210530431966, - "headingRadians": 0.80623149673078154, - "unwrappedHeadingRadians": 0.80623149673078154, - "arcLengthMeters": 0.049999999999999829, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0521958040094341, - "yMeters": 1.0538529506623149, - "headingRadians": 0.81664816339744817, - "unwrappedHeadingRadians": 0.81664816339744817, - "arcLengthMeters": 0.074999999999999747, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.46938177489050215, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0692172936169484, - "yMeters": 1.0721631456410243, - "headingRadians": 0.8270648300641148, - "unwrappedHeadingRadians": 0.8270648300641148, - "arcLengthMeters": 0.099999999999999659, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.46938177489050215, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0860471320094625, - "yMeters": 1.0906496512139807, - "headingRadians": 0.83748149673078143, - "unwrappedHeadingRadians": 0.83748149673078143, - "arcLengthMeters": 0.12499999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.46938177489050215, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1026834930491489, - "yMeters": 1.1093104614850882, - "headingRadians": 0.84789816339744806, - "unwrappedHeadingRadians": 0.84789816339744806, - "arcLengthMeters": 0.14999999999999949, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.41395276481822574, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1191245715916278, - "yMeters": 1.1281435516451528, - "headingRadians": 0.85831483006411469, - "unwrappedHeadingRadians": 0.85831483006411469, - "arcLengthMeters": 0.17499999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.41395276481822574, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1353685836818339, - "yMeters": 1.1471468781915848, - "headingRadians": 0.86873149673078132, - "unwrappedHeadingRadians": 0.86873149673078132, - "arcLengthMeters": 0.19999999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.41395276481822574, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1514137667475874, - "yMeters": 1.1663183791501308, - "headingRadians": 0.879148163397448, - "unwrappedHeadingRadians": 0.879148163397448, - "arcLengthMeters": 0.22499999999999923, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.36045082905681558, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1672583797908438, - "yMeters": 1.1856559742986119, - "headingRadians": 0.88956483006411458, - "unwrappedHeadingRadians": 0.88956483006411458, - "arcLengthMeters": 0.24999999999999914, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.36045082905681558, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.182900703576601, - "yMeters": 1.2051575653926381, - "headingRadians": 0.89998149673078121, - "unwrappedHeadingRadians": 0.89998149673078121, - "arcLengthMeters": 0.27499999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.198339040819449, - "yMeters": 1.2248210363932823, - "headingRadians": 0.91039816339744783, - "unwrappedHeadingRadians": 0.91039816339744783, - "arcLengthMeters": 0.299999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2135717163677331, - "yMeters": 1.2446442536966815, - "headingRadians": 0.92081483006411446, - "unwrappedHeadingRadians": 0.92081483006411446, - "arcLengthMeters": 0.3249999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2285970773853192, - "yMeters": 1.2646250663655463, - "headingRadians": 0.93123149673078109, - "unwrappedHeadingRadians": 0.93123149673078109, - "arcLengthMeters": 0.34999999999999881, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2434134935309367, - "yMeters": 1.2847613063625509, - "headingRadians": 0.94164816339744772, - "unwrappedHeadingRadians": 0.94164816339744772, - "arcLengthMeters": 0.37499999999999872, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2580193571350784, - "yMeters": 1.3050507887855771, - "headingRadians": 0.95206483006411435, - "unwrappedHeadingRadians": 0.95206483006411435, - "arcLengthMeters": 0.39999999999999863, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2724130833744447, - "yMeters": 1.3254913121047895, - "headingRadians": 0.962481496730781, - "unwrappedHeadingRadians": 0.962481496730781, - "arcLengthMeters": 0.42499999999999855, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2865931104439039, - "yMeters": 1.3460806584015148, - "headingRadians": 0.97289816339744761, - "unwrappedHeadingRadians": 0.97289816339744761, - "arcLengthMeters": 0.44999999999999846, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3005578997259597, - "yMeters": 1.3668165936088998, - "headingRadians": 0.98331483006411424, - "unwrappedHeadingRadians": 0.98331483006411424, - "arcLengthMeters": 0.47499999999999837, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3143059359576983, - "yMeters": 1.3876968677543193, - "headingRadians": 0.99373149673078087, - "unwrappedHeadingRadians": 0.99373149673078087, - "arcLengthMeters": 0.49999999999999828, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3278357273952051, - "yMeters": 1.4087192152035137, - "headingRadians": 1.0041481633974476, - "unwrappedHeadingRadians": 1.0041481633974476, - "arcLengthMeters": 0.52499999999999847, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3411458059754271, - "yMeters": 1.4298813549064222, - "headingRadians": 1.0145648300641144, - "unwrappedHeadingRadians": 1.0145648300641144, - "arcLengthMeters": 0.5499999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.21067092762809647, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.354234727475466, - "yMeters": 1.4511809906446904, - "headingRadians": 1.0249814967307811, - "unwrappedHeadingRadians": 1.0249814967307811, - "arcLengthMeters": 0.57499999999999873, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.367101071669286, - "yMeters": 1.4726158112808247, - "headingRadians": 1.0353981633974478, - "unwrappedHeadingRadians": 1.0353981633974478, - "arcLengthMeters": 0.59999999999999887, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3797434424818162, - "yMeters": 1.4941834910089637, - "headingRadians": 1.0458148300641146, - "unwrappedHeadingRadians": 1.0458148300641146, - "arcLengthMeters": 0.624999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3921604681404329, - "yMeters": 1.5158816896072425, - "headingRadians": 1.0562314967307813, - "unwrappedHeadingRadians": 1.0562314967307813, - "arcLengthMeters": 0.64999999999999913, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.404350801323806, - "yMeters": 1.53770805269172, - "headingRadians": 1.0666481633974481, - "unwrappedHeadingRadians": 1.0666481633974481, - "arcLengthMeters": 0.67499999999999927, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.16076258642584618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4163131193080905, - "yMeters": 1.5596602119718443, - "headingRadians": 1.0770648300641148, - "unwrappedHeadingRadians": 1.0770648300641148, - "arcLengthMeters": 0.6999999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.16076258642584618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4280461241104498, - "yMeters": 1.5817357855074268, - "headingRadians": 1.0874814967307815, - "unwrappedHeadingRadians": 1.0874814967307815, - "arcLengthMeters": 0.72499999999999953, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.16542738416163116, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4395485426298957, - "yMeters": 1.6039323779670949, - "headingRadians": 1.0978981633974483, - "unwrappedHeadingRadians": 1.0978981633974483, - "arcLengthMeters": 0.74999999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.16542738416163116, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4508191267854267, - "yMeters": 1.626247580888202, - "headingRadians": 1.108314830064115, - "unwrappedHeadingRadians": 1.108314830064115, - "arcLengthMeters": 0.7749999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.12405453637069408, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4618566536514517, - "yMeters": 1.648678972938159, - "headingRadians": 1.1187314967307818, - "unwrappedHeadingRadians": 1.1187314967307818, - "arcLengthMeters": 0.79999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.12405453637069408, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4726599255904853, - "yMeters": 1.6712241201771632, - "headingRadians": 1.1291481633974485, - "unwrappedHeadingRadians": 1.1291481633974485, - "arcLengthMeters": 0.82500000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.12405453637069408, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4832277703830994, - "yMeters": 1.6938805763222957, - "headingRadians": 1.1395648300641152, - "unwrappedHeadingRadians": 1.1395648300641152, - "arcLengthMeters": 0.8500000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4935590413551147, - "yMeters": 1.7166458830129574, - "headingRadians": 1.149981496730782, - "unwrappedHeadingRadians": 1.149981496730782, - "arcLengthMeters": 0.87500000000000033, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5036526175020224, - "yMeters": 1.7395175700776162, - "headingRadians": 1.1603981633974487, - "unwrappedHeadingRadians": 1.1603981633974487, - "arcLengthMeters": 0.90000000000000047, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.10250504669770155, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5135074036106191, - "yMeters": 1.7624931558018344, - "headingRadians": 1.1708148300641155, - "unwrappedHeadingRadians": 1.1708148300641155, - "arcLengthMeters": 0.9250000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.10250504669770155, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5231223303778454, - "yMeters": 1.7855701471975509, - "headingRadians": 1.1812314967307822, - "unwrappedHeadingRadians": 1.1812314967307822, - "arcLengthMeters": 0.95000000000000073, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.12077891991816714, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5324963545268102, - "yMeters": 1.808746040273584, - "headingRadians": 1.1916481633974489, - "unwrappedHeadingRadians": 1.1916481633974489, - "arcLengthMeters": 0.97500000000000087, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.12077891991816714, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5416284589199927, - "yMeters": 1.83201832030733, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.0000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.14182540145670275, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5506392709464549, - "yMeters": 1.8553379529507434, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.0250000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.12241848545546363, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5596500829729172, - "yMeters": 1.878657585594157, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.0500000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.12241848545546363, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5686608949993794, - "yMeters": 1.9019772182375705, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.0750000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.12241848545546363, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5776717070258417, - "yMeters": 1.925296850880984, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.1, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14977102437387113, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.586682519052304, - "yMeters": 1.9486164835243975, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.125, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14977102437387113, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5956933310787662, - "yMeters": 1.9719361161678111, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.15, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14977102437387113, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6047041431052285, - "yMeters": 1.9952557488112246, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.1749999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14022657692244167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6137149551316907, - "yMeters": 2.0185753814546383, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.2, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14022657692244167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.622725767158153, - "yMeters": 2.0418950140980519, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.2249999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.17314263913676164, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6317365791846152, - "yMeters": 2.0652146467414654, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.2499999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.17314263913676164, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6407473912110775, - "yMeters": 2.0885342793848789, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.2749999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.20772974675563327, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6497582032375397, - "yMeters": 2.1118539120282924, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.2999999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.20772974675563327, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.658769015264002, - "yMeters": 2.1351735446717059, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.3249999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.21067092762809647, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6677798272904643, - "yMeters": 2.1584931773151195, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.3499999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.21067092762809647, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6767906393169265, - "yMeters": 2.181812809958533, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.3749999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.2494263000700192, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6858014513433888, - "yMeters": 2.2051324426019465, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.3999999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.2494263000700192, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.694812263369851, - "yMeters": 2.22845207524536, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.4249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.28925537121869005, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7038230753963133, - "yMeters": 2.2517717078887736, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.4499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.28925537121869005, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7128338874227755, - "yMeters": 2.2750913405321871, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.474999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.30259972253743661, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7218446994492378, - "yMeters": 2.2984109731756006, - "headingRadians": 1.2020648300641155, - "unwrappedHeadingRadians": 1.2020648300641155, - "arcLengthMeters": 1.4999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.30259972253743661, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7309768038424205, - "yMeters": 2.321683253209347, - "headingRadians": 1.1916481633974487, - "unwrappedHeadingRadians": 1.1916481633974487, - "arcLengthMeters": 1.524999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30259972253743661, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7403508279913851, - "yMeters": 2.3448591462853803, - "headingRadians": 1.181231496730782, - "unwrappedHeadingRadians": 1.181231496730782, - "arcLengthMeters": 1.5499999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.34535575549757735, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7499657547586114, - "yMeters": 2.3679361376810966, - "headingRadians": 1.1708148300641152, - "unwrappedHeadingRadians": 1.1708148300641152, - "arcLengthMeters": 1.5749999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.34535575549757735, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7598205408672083, - "yMeters": 2.3909117234053148, - "headingRadians": 1.1603981633974485, - "unwrappedHeadingRadians": 1.1603981633974485, - "arcLengthMeters": 1.5999999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.3654321992066818, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7699141170141159, - "yMeters": 2.4137834104699731, - "headingRadians": 1.1499814967307818, - "unwrappedHeadingRadians": 1.1499814967307818, - "arcLengthMeters": 1.6249999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.3654321992066818, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7802453879861313, - "yMeters": 2.436548717160635, - "headingRadians": 1.139564830064115, - "unwrappedHeadingRadians": 1.139564830064115, - "arcLengthMeters": 1.6499999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.41039141141320834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7908132327787454, - "yMeters": 2.4592051733057674, - "headingRadians": 1.1291481633974483, - "unwrappedHeadingRadians": 1.1291481633974483, - "arcLengthMeters": 1.6749999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.41039141141320834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.801616504717779, - "yMeters": 2.4817503205447715, - "headingRadians": 1.1187314967307815, - "unwrappedHeadingRadians": 1.1187314967307815, - "arcLengthMeters": 1.7, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45578721930484167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.812654031583804, - "yMeters": 2.5041817125947285, - "headingRadians": 1.1083148300641148, - "unwrappedHeadingRadians": 1.1083148300641148, - "arcLengthMeters": 1.725, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.823924615739335, - "yMeters": 2.5264969155158359, - "headingRadians": 1.0978981633974481, - "unwrappedHeadingRadians": 1.0978981633974481, - "arcLengthMeters": 1.7500000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8354270342587808, - "yMeters": 2.548693507975504, - "headingRadians": 1.0874814967307813, - "unwrappedHeadingRadians": 1.0874814967307813, - "arcLengthMeters": 1.7750000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8471600390611402, - "yMeters": 2.5707690815110862, - "headingRadians": 1.0770648300641146, - "unwrappedHeadingRadians": 1.0770648300641146, - "arcLengthMeters": 1.8000000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8591223570454247, - "yMeters": 2.592721240791211, - "headingRadians": 1.0666481633974478, - "unwrappedHeadingRadians": 1.0666481633974478, - "arcLengthMeters": 1.8250000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8713126902287978, - "yMeters": 2.6145476038756885, - "headingRadians": 1.0562314967307811, - "unwrappedHeadingRadians": 1.0562314967307811, - "arcLengthMeters": 1.8500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8837297158874144, - "yMeters": 2.6362458024739674, - "headingRadians": 1.0458148300641144, - "unwrappedHeadingRadians": 1.0458148300641144, - "arcLengthMeters": 1.8750000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8963720866999447, - "yMeters": 2.6578134822021067, - "headingRadians": 1.0353981633974476, - "unwrappedHeadingRadians": 1.0353981633974476, - "arcLengthMeters": 1.900000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9092384308937647, - "yMeters": 2.679248302838241, - "headingRadians": 1.0249814967307809, - "unwrappedHeadingRadians": 1.0249814967307809, - "arcLengthMeters": 1.9250000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.41039141141320834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9223273523938036, - "yMeters": 2.7005479385765092, - "headingRadians": 1.0145648300641141, - "unwrappedHeadingRadians": 1.0145648300641141, - "arcLengthMeters": 1.9500000000000013, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.41039141141320834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9356374309740256, - "yMeters": 2.7217100782794175, - "headingRadians": 1.0041481633974474, - "unwrappedHeadingRadians": 1.0041481633974474, - "arcLengthMeters": 1.9750000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9491672224115324, - "yMeters": 2.7427324257286121, - "headingRadians": 0.99373149673078076, - "unwrappedHeadingRadians": 0.99373149673078076, - "arcLengthMeters": 2.0000000000000013, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9630236351684296, - "yMeters": 2.7635405304050145, - "headingRadians": 0.97289816339744739, - "unwrappedHeadingRadians": 0.97289816339744739, - "arcLengthMeters": 2.0250000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9773105118228422, - "yMeters": 2.7840554652129836, - "headingRadians": 0.952064830064114, - "unwrappedHeadingRadians": 0.952064830064114, - "arcLengthMeters": 2.0500000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9920216516977221, - "yMeters": 2.8042683264229993, - "headingRadians": 0.93123149673078065, - "unwrappedHeadingRadians": 0.93123149673078065, - "arcLengthMeters": 2.0750000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0071506699806574, - "yMeters": 2.8241703414091313, - "headingRadians": 0.91039816339744728, - "unwrappedHeadingRadians": 0.91039816339744728, - "arcLengthMeters": 2.100000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.91000785125885864, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.02269100049496, - "yMeters": 2.8437528724564665, - "headingRadians": 0.88956483006411391, - "unwrappedHeadingRadians": 0.88956483006411391, - "arcLengthMeters": 2.1250000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.91000785125885864, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0386358985494648, - "yMeters": 2.8630074205099802, - "headingRadians": 0.86873149673078054, - "unwrappedHeadingRadians": 0.86873149673078054, - "arcLengthMeters": 2.1500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.91000785125885864, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0549784438658074, - "yMeters": 2.8819256288632285, - "headingRadians": 0.84789816339744717, - "unwrappedHeadingRadians": 0.84789816339744717, - "arcLengthMeters": 2.1750000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.862531247169067, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.071711543581908, - "yMeters": 2.9004992867852604, - "headingRadians": 0.8270648300641138, - "unwrappedHeadingRadians": 0.8270648300641138, - "arcLengthMeters": 2.2000000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.862531247169067, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0888279353303592, - "yMeters": 2.9187203330841744, - "headingRadians": 0.80623149673078043, - "unwrappedHeadingRadians": 0.80623149673078043, - "arcLengthMeters": 2.2250000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.86668295249990968, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1063201903903823, - "yMeters": 2.936580859605773, - "headingRadians": 0.78539816339744706, - "unwrappedHeadingRadians": 0.78539816339744706, - "arcLengthMeters": 2.2500000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.81693958304310133, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1241807169119808, - "yMeters": 2.9540731146657961, - "headingRadians": 0.76456483006411369, - "unwrappedHeadingRadians": 0.76456483006411369, - "arcLengthMeters": 2.2750000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.81693958304310133, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1424017632108949, - "yMeters": 2.9711895064142473, - "headingRadians": 0.74373149673078032, - "unwrappedHeadingRadians": 0.74373149673078032, - "arcLengthMeters": 2.3000000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.82292972616222093, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1609754211329268, - "yMeters": 2.9879226061303479, - "headingRadians": 0.722898163397447, - "unwrappedHeadingRadians": 0.722898163397447, - "arcLengthMeters": 2.325, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.77341517170487128, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.179893629486175, - "yMeters": 3.0042651514466905, - "headingRadians": 0.70206483006411358, - "unwrappedHeadingRadians": 0.70206483006411358, - "arcLengthMeters": 2.35, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.77341517170487128, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1991481775396888, - "yMeters": 3.0202100495011952, - "headingRadians": 0.68123149673078021, - "unwrappedHeadingRadians": 0.68123149673078021, - "arcLengthMeters": 2.375, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.78134787466456534, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2187307085870245, - "yMeters": 3.0357503800154979, - "headingRadians": 0.66039816339744684, - "unwrappedHeadingRadians": 0.66039816339744684, - "arcLengthMeters": 2.4, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.73215463110637435, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2386327235731565, - "yMeters": 3.050879398298433, - "headingRadians": 0.63956483006411347, - "unwrappedHeadingRadians": 0.63956483006411347, - "arcLengthMeters": 2.425, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.73215463110637435, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2588455847831721, - "yMeters": 3.0655905381733128, - "headingRadians": 0.6187314967307801, - "unwrappedHeadingRadians": 0.6187314967307801, - "arcLengthMeters": 2.4499999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.69336801574440965, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2793605195911413, - "yMeters": 3.0798774148277257, - "headingRadians": 0.59789816339744672, - "unwrappedHeadingRadians": 0.59789816339744672, - "arcLengthMeters": 2.4749999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.69336801574440965, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3001686242675436, - "yMeters": 3.0937338275846229, - "headingRadians": 0.57706483006411347, - "unwrappedHeadingRadians": 0.57706483006411347, - "arcLengthMeters": 2.4999999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.64469762255002039, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3212608678435949, - "yMeters": 3.1071537625934873, - "headingRadians": 0.5562314967307801, - "unwrappedHeadingRadians": 0.5562314967307801, - "arcLengthMeters": 2.5249999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.64469762255002039, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3426280960307961, - "yMeters": 3.1201313954404166, - "headingRadians": 0.53539816339744672, - "unwrappedHeadingRadians": 0.53539816339744672, - "arcLengthMeters": 2.5499999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.6572771079064933, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.364261035194005, - "yMeters": 3.13266109367599, - "headingRadians": 0.51456483006411335, - "unwrappedHeadingRadians": 0.51456483006411335, - "arcLengthMeters": 2.5749999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3861502963763059, - "yMeters": 3.144737419259819, - "headingRadians": 0.49373149673078004, - "unwrappedHeadingRadians": 0.49373149673078004, - "arcLengthMeters": 2.5999999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4082863793739309, - "yMeters": 3.1563551309207218, - "headingRadians": 0.47289816339744672, - "unwrappedHeadingRadians": 0.47289816339744672, - "arcLengthMeters": 2.6249999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.57678117552036834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4306596768594626, - "yMeters": 3.1675091864314995, - "headingRadians": 0.45206483006411341, - "unwrappedHeadingRadians": 0.45206483006411341, - "arcLengthMeters": 2.649999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.57678117552036834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4532604785515306, - "yMeters": 3.1781947447973233, - "headingRadians": 0.4312314967307801, - "unwrappedHeadingRadians": 0.4312314967307801, - "arcLengthMeters": 2.6749999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.52967160040373884, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4760789754291905, - "yMeters": 3.1884071683567847, - "headingRadians": 0.41039816339744678, - "unwrappedHeadingRadians": 0.41039816339744678, - "arcLengthMeters": 2.6999999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.52967160040373884, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4991052639891578, - "yMeters": 3.1981420247946963, - "headingRadians": 0.38956483006411347, - "unwrappedHeadingRadians": 0.38956483006411347, - "arcLengthMeters": 2.7249999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.52967160040373884, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5223293505440472, - "yMeters": 3.20739508906577, - "headingRadians": 0.36873149673078015, - "unwrappedHeadingRadians": 0.36873149673078015, - "arcLengthMeters": 2.7499999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.50156757521136253, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5457411555597549, - "yMeters": 3.2161623452283381, - "headingRadians": 0.34789816339744684, - "unwrappedHeadingRadians": 0.34789816339744684, - "arcLengthMeters": 2.7749999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.50156757521136253, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5693305180300978, - "yMeters": 3.2244399881873211, - "headingRadians": 0.32706483006411352, - "unwrappedHeadingRadians": 0.32706483006411352, - "arcLengthMeters": 2.7999999999999985, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.45578721930484167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5930871998868117, - "yMeters": 3.2322244253456835, - "headingRadians": 0.30623149673078021, - "unwrappedHeadingRadians": 0.30623149673078021, - "arcLengthMeters": 2.8249999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.45578721930484167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6170008904429984, - "yMeters": 3.2395122781636649, - "headingRadians": 0.28539816339744689, - "unwrappedHeadingRadians": 0.28539816339744689, - "arcLengthMeters": 2.8499999999999983, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.41039141141320834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6410612108680871, - "yMeters": 3.2463003836251079, - "headingRadians": 0.26456483006411358, - "unwrappedHeadingRadians": 0.26456483006411358, - "arcLengthMeters": 2.8749999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.41039141141320834, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6652577186923732, - "yMeters": 3.2525857956102437, - "headingRadians": 0.24373149673078023, - "unwrappedHeadingRadians": 0.24373149673078023, - "arcLengthMeters": 2.8999999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.3654321992066818, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6895799123391746, - "yMeters": 3.2583657861743456, - "headingRadians": 0.22289816339744689, - "unwrappedHeadingRadians": 0.22289816339744689, - "arcLengthMeters": 2.924999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.38876462343047646, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7140172356826451, - "yMeters": 3.2636378467316884, - "headingRadians": 0.20206483006411355, - "unwrappedHeadingRadians": 0.20206483006411355, - "arcLengthMeters": 2.949999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.34535575549757735, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.738559082629259, - "yMeters": 3.2683996891443052, - "headingRadians": 0.18123149673078021, - "unwrappedHeadingRadians": 0.18123149673078021, - "arcLengthMeters": 2.9749999999999979, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.34535575549757735, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7631948017209842, - "yMeters": 3.2726492467150679, - "headingRadians": 0.160398163397447, - "unwrappedHeadingRadians": 0.160398163397447, - "arcLengthMeters": 2.9999999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.30259972253743661, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7879137007581436, - "yMeters": 3.2763846750846572, - "headingRadians": 0.13956483006411366, - "unwrappedHeadingRadians": 0.13956483006411366, - "arcLengthMeters": 3.0249999999999977, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.30259972253743661, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8127050514399596, - "yMeters": 3.2796043530320382, - "headingRadians": 0.11873149673078032, - "unwrappedHeadingRadians": 0.11873149673078032, - "arcLengthMeters": 3.0499999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.26058942819777364, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8375580940207645, - "yMeters": 3.2823068831780891, - "headingRadians": 0.097898163397446974, - "unwrappedHeadingRadians": 0.097898163397446974, - "arcLengthMeters": 3.0749999999999975, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.26058942819777364, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8624620419798625, - "yMeters": 3.2844910925920803, - "headingRadians": 0.077064830064113632, - "unwrappedHeadingRadians": 0.077064830064113632, - "arcLengthMeters": 3.0999999999999974, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.219434136036039, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8874060867030078, - "yMeters": 3.2861560333007422, - "headingRadians": 0.056231496730780296, - "unwrappedHeadingRadians": 0.056231496730780296, - "arcLengthMeters": 3.1249999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.219434136036039, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9123794021734746, - "yMeters": 3.2873009826996955, - "headingRadians": 0.035398163397446961, - "unwrappedHeadingRadians": 0.035398163397446961, - "arcLengthMeters": 3.1499999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.17926258267461259, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9373711496706796, - "yMeters": 3.28792544386707, - "headingRadians": 0.014564830064113625, - "unwrappedHeadingRadians": 0.014564830064113625, - "arcLengthMeters": 3.1749999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.17926258267461259, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9623704824743173, - "yMeters": 3.2880291457791762, - "headingRadians": -0.0062685032692197107, - "unwrappedHeadingRadians": -0.0062685032692197107, - "arcLengthMeters": 3.1999999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.14022657692244167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9873665505719678, - "yMeters": 3.2876120434281311, - "headingRadians": -0.027101836602553046, - "unwrappedHeadingRadians": -0.027101836602553046, - "arcLengthMeters": 3.224999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.14022657692244167, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.012348505368132, - "yMeters": 3.2866743178413937, - "headingRadians": -0.047935169935886382, - "unwrappedHeadingRadians": -0.047935169935886382, - "arcLengthMeters": 3.2499999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.10250504669770155, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0373055043926551, - "yMeters": 3.2852163760031958, - "headingRadians": -0.068768503269219711, - "unwrappedHeadingRadians": -0.068768503269219711, - "arcLengthMeters": 3.2749999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.10250504669770155, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0622267160064887, - "yMeters": 3.2832388506779071, - "headingRadians": -0.089601836602553053, - "unwrappedHeadingRadians": -0.089601836602553053, - "arcLengthMeters": 3.2999999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.066308390518323823, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0871013241027518, - "yMeters": 3.2807426001354063, - "headingRadians": -0.1104351699358864, - "unwrappedHeadingRadians": -0.1104351699358864, - "arcLengthMeters": 3.3249999999999966, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.066308390518323823, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1119185328010546, - "yMeters": 3.2777287077785831, - "headingRadians": -0.13126850326921974, - "unwrappedHeadingRadians": -0.13126850326921974, - "arcLengthMeters": 3.3499999999999965, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.031882811685461809, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1366675711330378, - "yMeters": 3.2741984816731264, - "headingRadians": -0.15210183660255308, - "unwrappedHeadingRadians": -0.15210183660255308, - "arcLengthMeters": 3.3749999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.031882811685461809, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1613376977171055, - "yMeters": 3.2701534539798107, - "headingRadians": -0.17293516993588642, - "unwrappedHeadingRadians": -0.17293516993588642, - "arcLengthMeters": 3.3999999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.031882811685461809, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1859182054203137, - "yMeters": 3.265595380289517, - "headingRadians": -0.19376850326921977, - "unwrappedHeadingRadians": -0.19376850326921977, - "arcLengthMeters": 3.4249999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2103984260053946, - "yMeters": 3.2605262388612872, - "headingRadians": -0.21460183660255311, - "unwrappedHeadingRadians": -0.21460183660255311, - "arcLengthMeters": 3.4499999999999962, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2347677347609016, - "yMeters": 3.2549482297637344, - "headingRadians": -0.23543516993588645, - "unwrappedHeadingRadians": -0.23543516993588645, - "arcLengthMeters": 3.4749999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.259015555112462, - "yMeters": 3.2488637739201875, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 3.499999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2831656773872813, - "yMeters": 3.2424011163609188, - "headingRadians": -0.26668516993588637, - "unwrappedHeadingRadians": -0.26668516993588637, - "arcLengthMeters": 3.5249999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3072471713138136, - "yMeters": 3.2356872501957739, - "headingRadians": -0.27710183660255305, - "unwrappedHeadingRadians": -0.27710183660255305, - "arcLengthMeters": 3.5499999999999958, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3312574239063628, - "yMeters": 3.2287229039192691, - "headingRadians": -0.28751850326921974, - "unwrappedHeadingRadians": -0.28751850326921974, - "arcLengthMeters": 3.5749999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3551938299093425, - "yMeters": 3.221508833204505, - "headingRadians": -0.29793516993588642, - "unwrappedHeadingRadians": -0.29793516993588642, - "arcLengthMeters": 3.5999999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.379053792079961, - "yMeters": 3.2140458208211746, - "headingRadians": -0.30835183660255311, - "unwrappedHeadingRadians": -0.30835183660255311, - "arcLengthMeters": 3.6249999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4028347214700387, - "yMeters": 3.2063346765506253, - "headingRadians": -0.31876850326921979, - "unwrappedHeadingRadians": -0.31876850326921979, - "arcLengthMeters": 3.6499999999999955, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.426534037706924, - "yMeters": 3.1983762370979947, - "headingRadians": -0.32918516993588648, - "unwrappedHeadingRadians": -0.32918516993588648, - "arcLengthMeters": 3.6749999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4501491692734794, - "yMeters": 3.1901713660014219, - "headingRadians": -0.33960183660255316, - "unwrappedHeadingRadians": -0.33960183660255316, - "arcLengthMeters": 3.6999999999999953, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.473677553787105, - "yMeters": 3.1817209535383486, - "headingRadians": -0.35001850326921985, - "unwrappedHeadingRadians": -0.35001850326921985, - "arcLengthMeters": 3.7249999999999952, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4971166382777747, - "yMeters": 3.17302591662892, - "headingRadians": -0.36043516993588653, - "unwrappedHeadingRadians": -0.36043516993588653, - "arcLengthMeters": 3.7499999999999951, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5204638794650469, - "yMeters": 3.1640871987364911, - "headingRadians": -0.37085183660255322, - "unwrappedHeadingRadians": -0.37085183660255322, - "arcLengthMeters": 3.774999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5437167440340263, - "yMeters": 3.1549057697652581, - "headingRadians": -0.3812685032692199, - "unwrappedHeadingRadians": -0.3812685032692199, - "arcLengthMeters": 3.7999999999999949, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.566872708910243, - "yMeters": 3.1454826259550157, - "headingRadians": -0.39168516993588659, - "unwrappedHeadingRadians": -0.39168516993588659, - "arcLengthMeters": 3.8249999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5899292615334217, - "yMeters": 3.1358187897730607, - "headingRadians": -0.40210183660255328, - "unwrappedHeadingRadians": -0.40210183660255328, - "arcLengthMeters": 3.8499999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6128839001301105, - "yMeters": 3.1259153098032471, - "headingRadians": -0.41251850326921996, - "unwrappedHeadingRadians": -0.41251850326921996, - "arcLengthMeters": 3.8749999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6357341339851357, - "yMeters": 3.1157732606322091, - "headingRadians": -0.42293516993588665, - "unwrappedHeadingRadians": -0.42293516993588665, - "arcLengthMeters": 3.8999999999999946, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6584774837118608, - "yMeters": 3.1053937427327614, - "headingRadians": -0.43335183660255333, - "unwrappedHeadingRadians": -0.43335183660255333, - "arcLengthMeters": 3.9249999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6811114815212158, - "yMeters": 3.0947778823444927, - "headingRadians": -0.44376850326922, - "unwrappedHeadingRadians": -0.44376850326922, - "arcLengthMeters": 3.9499999999999944, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7036336714894644, - "yMeters": 3.0839268313515604, - "headingRadians": -0.4541851699358867, - "unwrappedHeadingRadians": -0.4541851699358867, - "arcLengthMeters": 3.9749999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7260416098246885, - "yMeters": 3.0728417671577062, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 3.9999999999999942, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7483916108321265, - "yMeters": 3.0616400951857408, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.0249999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7707416118395645, - "yMeters": 3.0504384232137753, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.0499999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7930916128470025, - "yMeters": 3.03923675124181, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.0749999999999931, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8154416138544405, - "yMeters": 3.0280350792698445, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.0999999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8377916148618785, - "yMeters": 3.0168334072978791, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.124999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8601416158693165, - "yMeters": 3.0056317353259137, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.1499999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8824916168767545, - "yMeters": 2.9944300633539482, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.1749999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9048416178841925, - "yMeters": 2.9832283913819828, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.19999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9271916188916305, - "yMeters": 2.9720267194100174, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.22499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9495416198990685, - "yMeters": 2.960825047438052, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.2499999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9718916209065065, - "yMeters": 2.9496233754660866, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.2749999999999888, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9942416219139445, - "yMeters": 2.9384217034941211, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.2999999999999883, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0165916229213829, - "yMeters": 2.9272200315221557, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.3249999999999886, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0389416239288209, - "yMeters": 2.9160183595501903, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.3499999999999881, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0612916249362589, - "yMeters": 2.9048166875782249, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.3749999999999876, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.083641625943697, - "yMeters": 2.8936150156062594, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.399999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.105991626951135, - "yMeters": 2.882413343634294, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.4249999999999865, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.128341627958573, - "yMeters": 2.8712116716623286, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.449999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.150691628966011, - "yMeters": 2.8600099996903632, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.4749999999999854, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.173041629973449, - "yMeters": 2.8488083277183978, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 4.4999999999999849, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.195449568308673, - "yMeters": 2.8377232635245431, - "headingRadians": -0.45418516993588665, - "unwrappedHeadingRadians": -0.45418516993588665, - "arcLengthMeters": 4.5249999999999853, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2179717582769216, - "yMeters": 2.8268722125316113, - "headingRadians": -0.44376850326921996, - "unwrappedHeadingRadians": -0.44376850326921996, - "arcLengthMeters": 4.5499999999999856, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2406057560862767, - "yMeters": 2.8162563521433426, - "headingRadians": -0.43335183660255328, - "unwrappedHeadingRadians": -0.43335183660255328, - "arcLengthMeters": 4.574999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2633491058130017, - "yMeters": 2.8058768342438949, - "headingRadians": -0.42293516993588659, - "unwrappedHeadingRadians": -0.42293516993588659, - "arcLengthMeters": 4.5999999999999863, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2861993396680269, - "yMeters": 2.7957347850728569, - "headingRadians": -0.4125185032692199, - "unwrappedHeadingRadians": -0.4125185032692199, - "arcLengthMeters": 4.6249999999999867, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3091539782647157, - "yMeters": 2.7858313051030432, - "headingRadians": -0.40210183660255322, - "unwrappedHeadingRadians": -0.40210183660255322, - "arcLengthMeters": 4.649999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3322105308878944, - "yMeters": 2.7761674689210882, - "headingRadians": -0.39168516993588653, - "unwrappedHeadingRadians": -0.39168516993588653, - "arcLengthMeters": 4.6749999999999874, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3553664957641116, - "yMeters": 2.7667443251108459, - "headingRadians": -0.38126850326921985, - "unwrappedHeadingRadians": -0.38126850326921985, - "arcLengthMeters": 4.6999999999999877, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.378619360333091, - "yMeters": 2.7575628961396128, - "headingRadians": -0.37085183660255316, - "unwrappedHeadingRadians": -0.37085183660255316, - "arcLengthMeters": 4.7249999999999881, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4019666015203631, - "yMeters": 2.7486241782471841, - "headingRadians": -0.36043516993588648, - "unwrappedHeadingRadians": -0.36043516993588648, - "arcLengthMeters": 4.7499999999999885, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4254056860110333, - "yMeters": 2.7399291413377553, - "headingRadians": -0.35001850326921979, - "unwrappedHeadingRadians": -0.35001850326921979, - "arcLengthMeters": 4.7749999999999888, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4489340705246594, - "yMeters": 2.731478728874682, - "headingRadians": -0.33960183660255311, - "unwrappedHeadingRadians": -0.33960183660255311, - "arcLengthMeters": 4.7999999999999892, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4725492020912148, - "yMeters": 2.7232738577781088, - "headingRadians": -0.32918516993588642, - "unwrappedHeadingRadians": -0.32918516993588642, - "arcLengthMeters": 4.8249999999999895, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4962485183281, - "yMeters": 2.7153154183254782, - "headingRadians": -0.31876850326921974, - "unwrappedHeadingRadians": -0.31876850326921974, - "arcLengthMeters": 4.84999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5200294477181782, - "yMeters": 2.7076042740549289, - "headingRadians": -0.30835183660255305, - "unwrappedHeadingRadians": -0.30835183660255305, - "arcLengthMeters": 4.87499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5438894098887967, - "yMeters": 2.7001412616715985, - "headingRadians": -0.29793516993588637, - "unwrappedHeadingRadians": -0.29793516993588637, - "arcLengthMeters": 4.8999999999999906, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5678258158917764, - "yMeters": 2.6929271909568344, - "headingRadians": -0.28751850326921968, - "unwrappedHeadingRadians": -0.28751850326921968, - "arcLengthMeters": 4.9249999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5918360684843256, - "yMeters": 2.6859628446803296, - "headingRadians": -0.277101836602553, - "unwrappedHeadingRadians": -0.277101836602553, - "arcLengthMeters": 4.9499999999999913, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6159175624108579, - "yMeters": 2.6792489785151847, - "headingRadians": -0.26668516993588631, - "unwrappedHeadingRadians": -0.26668516993588631, - "arcLengthMeters": 4.9749999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6400676846856772, - "yMeters": 2.672786320955916, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 4.999999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6400676846856772, - "yMeters": 2.672786320955916, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 4.999999999999992, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 4.615884121107797, - "yMeters": 2.6791231381912954, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.0249999999999915, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5917005575299168, - "yMeters": 2.6854599554266749, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.0499999999999909, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5675169939520366, - "yMeters": 2.6917967726620544, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.07499999999999, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5433334303741564, - "yMeters": 2.6981335898974339, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.09999999999999, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5191498667962762, - "yMeters": 2.7044704071328134, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.1249999999999893, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.494966303218396, - "yMeters": 2.7108072243681929, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.1499999999999888, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4707827396405158, - "yMeters": 2.7171440416035724, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.1749999999999883, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4465991760626356, - "yMeters": 2.7234808588389519, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.1999999999999877, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4224156124847553, - "yMeters": 2.7298176760743313, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.2249999999999872, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3982320489068751, - "yMeters": 2.7361544933097108, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.2499999999999867, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3740484853289949, - "yMeters": 2.7424913105450903, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.2749999999999861, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3498649217511147, - "yMeters": 2.74882812778047, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.2999999999999856, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3256813581732345, - "yMeters": 2.7551649450158493, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.3249999999999851, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3014977945953543, - "yMeters": 2.7615017622512288, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.3499999999999845, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2773142310174741, - "yMeters": 2.7678385794866083, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.374999999999984, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2531306674395939, - "yMeters": 2.7741753967219878, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.3999999999999835, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2289471038617137, - "yMeters": 2.7805122139573673, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.4249999999999829, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2047635402838335, - "yMeters": 2.7868490311927467, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.4499999999999824, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1805799767059533, - "yMeters": 2.7931858484281262, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.4749999999999819, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1563964131280731, - "yMeters": 2.7995226656635057, - "headingRadians": -0.25626850326921968, - "unwrappedHeadingRadians": -0.25626850326921968, - "arcLengthMeters": 5.4999999999999813, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1322462908532538, - "yMeters": 2.8059853232227745, - "headingRadians": -0.26668516993588637, - "unwrappedHeadingRadians": -0.26668516993588637, - "arcLengthMeters": 5.5249999999999817, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1081647969267214, - "yMeters": 2.8126991893879194, - "headingRadians": -0.27710183660255305, - "unwrappedHeadingRadians": -0.27710183660255305, - "arcLengthMeters": 5.5499999999999821, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0841545443341722, - "yMeters": 2.8196635356644242, - "headingRadians": -0.28751850326921974, - "unwrappedHeadingRadians": -0.28751850326921974, - "arcLengthMeters": 5.5749999999999824, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0602181383311926, - "yMeters": 2.8268776063791883, - "headingRadians": -0.29793516993588642, - "unwrappedHeadingRadians": -0.29793516993588642, - "arcLengthMeters": 5.5999999999999828, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.036358176160574, - "yMeters": 2.8343406187625186, - "headingRadians": -0.30835183660255311, - "unwrappedHeadingRadians": -0.30835183660255311, - "arcLengthMeters": 5.6249999999999831, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0125772467704959, - "yMeters": 2.8420517630330679, - "headingRadians": -0.31876850326921979, - "unwrappedHeadingRadians": -0.31876850326921979, - "arcLengthMeters": 5.6499999999999835, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9888779305336106, - "yMeters": 2.8500102024856986, - "headingRadians": -0.32918516993588648, - "unwrappedHeadingRadians": -0.32918516993588648, - "arcLengthMeters": 5.6749999999999838, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9652627989670552, - "yMeters": 2.8582150735822713, - "headingRadians": -0.33960183660255316, - "unwrappedHeadingRadians": -0.33960183660255316, - "arcLengthMeters": 5.6999999999999842, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9417344144534296, - "yMeters": 2.8666654860453447, - "headingRadians": -0.35001850326921985, - "unwrappedHeadingRadians": -0.35001850326921985, - "arcLengthMeters": 5.7249999999999845, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.91829532996276, - "yMeters": 2.8753605229547734, - "headingRadians": -0.36043516993588653, - "unwrappedHeadingRadians": -0.36043516993588653, - "arcLengthMeters": 5.7499999999999849, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8949480887754877, - "yMeters": 2.8842992408472021, - "headingRadians": -0.37085183660255322, - "unwrappedHeadingRadians": -0.37085183660255322, - "arcLengthMeters": 5.7749999999999853, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8716952242065084, - "yMeters": 2.8934806698184352, - "headingRadians": -0.3812685032692199, - "unwrappedHeadingRadians": -0.3812685032692199, - "arcLengthMeters": 5.7999999999999856, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8485392593302916, - "yMeters": 2.9029038136286776, - "headingRadians": -0.39168516993588659, - "unwrappedHeadingRadians": -0.39168516993588659, - "arcLengthMeters": 5.824999999999986, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8254827067071129, - "yMeters": 2.9125676498106325, - "headingRadians": -0.40210183660255328, - "unwrappedHeadingRadians": -0.40210183660255328, - "arcLengthMeters": 5.8499999999999863, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8025280681104241, - "yMeters": 2.9224711297804462, - "headingRadians": -0.41251850326921996, - "unwrappedHeadingRadians": -0.41251850326921996, - "arcLengthMeters": 5.8749999999999867, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7796778342553989, - "yMeters": 2.9326131789514842, - "headingRadians": -0.42293516993588665, - "unwrappedHeadingRadians": -0.42293516993588665, - "arcLengthMeters": 5.899999999999987, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7569344845286738, - "yMeters": 2.9429926968509319, - "headingRadians": -0.43335183660255333, - "unwrappedHeadingRadians": -0.43335183660255333, - "arcLengthMeters": 5.9249999999999874, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7343004867193188, - "yMeters": 2.9536085572392006, - "headingRadians": -0.44376850326922, - "unwrappedHeadingRadians": -0.44376850326922, - "arcLengthMeters": 5.9499999999999877, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.71177829675107, - "yMeters": 2.9644596082321328, - "headingRadians": -0.4541851699358867, - "unwrappedHeadingRadians": -0.4541851699358867, - "arcLengthMeters": 5.9749999999999881, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6893703584158462, - "yMeters": 2.9755446724259871, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 5.9999999999999885, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6893703584158462, - "yMeters": 2.9755446724259871, - "headingRadians": -0.46460183660255333, - "unwrappedHeadingRadians": -0.46460183660255333, - "arcLengthMeters": 5.9999999999999885, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 3.7118354225675825, - "yMeters": 2.9645766148325938, - "headingRadians": -0.44376850326922, - "unwrappedHeadingRadians": -0.44376850326922, - "arcLengthMeters": 6.0249999999999888, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7345240963356225, - "yMeters": 2.9540789256887359, - "headingRadians": -0.4229351699358867, - "unwrappedHeadingRadians": -0.4229351699358867, - "arcLengthMeters": 6.0499999999999892, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7574265325614795, - "yMeters": 2.94405616111831, - "headingRadians": -0.40210183660255339, - "unwrappedHeadingRadians": -0.40210183660255339, - "arcLengthMeters": 6.0749999999999895, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7805327913111775, - "yMeters": 2.9345126711222114, - "headingRadians": -0.38126850326922007, - "unwrappedHeadingRadians": -0.38126850326922007, - "arcLengthMeters": 6.09999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8038328441893032, - "yMeters": 2.9254525976903807, - "headingRadians": -0.36043516993588676, - "unwrappedHeadingRadians": -0.36043516993588676, - "arcLengthMeters": 6.12499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8273165786914509, - "yMeters": 2.9168798730041297, - "headingRadians": -0.33960183660255344, - "unwrappedHeadingRadians": -0.33960183660255344, - "arcLengthMeters": 6.1499999999999906, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8509738025931712, - "yMeters": 2.9087982177295277, - "headingRadians": -0.31876850326922013, - "unwrappedHeadingRadians": -0.31876850326922013, - "arcLengthMeters": 6.1749999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8747942483735192, - "yMeters": 2.9012111394025881, - "headingRadians": -0.29793516993588681, - "unwrappedHeadingRadians": -0.29793516993588681, - "arcLengthMeters": 6.1999999999999913, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8987675776712836, - "yMeters": 2.8941219309069535, - "headingRadians": -0.2771018366025535, - "unwrappedHeadingRadians": -0.2771018366025535, - "arcLengthMeters": 6.2249999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9228833857719594, - "yMeters": 2.8875336690447466, - "headingRadians": -0.25626850326922018, - "unwrappedHeadingRadians": -0.25626850326922018, - "arcLengthMeters": 6.249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9471312061235198, - "yMeters": 2.8814492132011997, - "headingRadians": -0.23543516993588684, - "unwrappedHeadingRadians": -0.23543516993588684, - "arcLengthMeters": 6.2749999999999924, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9715005148790268, - "yMeters": 2.8758712041036469, - "headingRadians": -0.2146018366025535, - "unwrappedHeadingRadians": -0.2146018366025535, - "arcLengthMeters": 6.2999999999999927, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9959807354641077, - "yMeters": 2.8708020626754172, - "headingRadians": -0.19376850326922015, - "unwrappedHeadingRadians": -0.19376850326922015, - "arcLengthMeters": 6.3249999999999931, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0205612431673154, - "yMeters": 2.8662439889851234, - "headingRadians": -0.17293516993588681, - "unwrappedHeadingRadians": -0.17293516993588681, - "arcLengthMeters": 6.3499999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0452313697513835, - "yMeters": 2.8621989612918077, - "headingRadians": -0.15210183660255347, - "unwrappedHeadingRadians": -0.15210183660255347, - "arcLengthMeters": 6.3749999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0699804080833664, - "yMeters": 2.8586687351863511, - "headingRadians": -0.13126850326922013, - "unwrappedHeadingRadians": -0.13126850326922013, - "arcLengthMeters": 6.3999999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0947976167816691, - "yMeters": 2.8556548428295274, - "headingRadians": -0.11043516993588678, - "unwrappedHeadingRadians": -0.11043516993588678, - "arcLengthMeters": 6.4249999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1196722248779327, - "yMeters": 2.8531585922870266, - "headingRadians": -0.089601836602553442, - "unwrappedHeadingRadians": -0.089601836602553442, - "arcLengthMeters": 6.4499999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1445934364917658, - "yMeters": 2.8511810669617379, - "headingRadians": -0.0687685032692201, - "unwrappedHeadingRadians": -0.0687685032692201, - "arcLengthMeters": 6.4749999999999952, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1695504355162889, - "yMeters": 2.8497231251235404, - "headingRadians": -0.047935169935886882, - "unwrappedHeadingRadians": -0.047935169935886882, - "arcLengthMeters": 6.4999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1945323903124532, - "yMeters": 2.8487853995368031, - "headingRadians": -0.027101836602553546, - "unwrappedHeadingRadians": -0.027101836602553546, - "arcLengthMeters": 6.524999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2195284584101032, - "yMeters": 2.8483682971857576, - "headingRadians": -0.00626850326922021, - "unwrappedHeadingRadians": -0.00626850326922021, - "arcLengthMeters": 6.5499999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2445277912137414, - "yMeters": 2.8484719990978635, - "headingRadians": 0.014564830064113125, - "unwrappedHeadingRadians": 0.014564830064113125, - "arcLengthMeters": 6.5749999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2695195387109468, - "yMeters": 2.8490964602652382, - "headingRadians": 0.035398163397446461, - "unwrappedHeadingRadians": 0.035398163397446461, - "arcLengthMeters": 6.5999999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2944928541814136, - "yMeters": 2.8502414096641915, - "headingRadians": 0.0562314967307798, - "unwrappedHeadingRadians": 0.0562314967307798, - "arcLengthMeters": 6.6249999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3194368989045584, - "yMeters": 2.8519063503728534, - "headingRadians": 0.077064830064113132, - "unwrappedHeadingRadians": 0.077064830064113132, - "arcLengthMeters": 6.6499999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3443408468636564, - "yMeters": 2.8540905597868447, - "headingRadians": 0.097898163397446475, - "unwrappedHeadingRadians": 0.097898163397446475, - "arcLengthMeters": 6.6749999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3691938894444613, - "yMeters": 2.8567930899328955, - "headingRadians": 0.11873149673077982, - "unwrappedHeadingRadians": 0.11873149673077982, - "arcLengthMeters": 6.6999999999999975, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3939852401262769, - "yMeters": 2.8600127678802765, - "headingRadians": 0.13956483006411316, - "unwrappedHeadingRadians": 0.13956483006411316, - "arcLengthMeters": 6.7249999999999979, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4187041391634363, - "yMeters": 2.8637481962498659, - "headingRadians": 0.1603981633974465, - "unwrappedHeadingRadians": 0.1603981633974465, - "arcLengthMeters": 6.7499999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.443339858255162, - "yMeters": 2.8679977538206285, - "headingRadians": 0.18123149673077985, - "unwrappedHeadingRadians": 0.18123149673077985, - "arcLengthMeters": 6.7749999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4678817052017763, - "yMeters": 2.8727595962332453, - "headingRadians": 0.20206483006411319, - "unwrappedHeadingRadians": 0.20206483006411319, - "arcLengthMeters": 6.7999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4923190285452472, - "yMeters": 2.8780316567905881, - "headingRadians": 0.22289816339744653, - "unwrappedHeadingRadians": 0.22289816339744653, - "arcLengthMeters": 6.8249999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5166412221920487, - "yMeters": 2.88381164735469, - "headingRadians": 0.24373149673077987, - "unwrappedHeadingRadians": 0.24373149673077987, - "arcLengthMeters": 6.85, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5408377300163343, - "yMeters": 2.8900970593398259, - "headingRadians": 0.26456483006411319, - "unwrappedHeadingRadians": 0.26456483006411319, - "arcLengthMeters": 6.875, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5648980504414229, - "yMeters": 2.8968851648012688, - "headingRadians": 0.2853981633974465, - "unwrappedHeadingRadians": 0.2853981633974465, - "arcLengthMeters": 6.9, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.58881174099761, - "yMeters": 2.90417301761925, - "headingRadians": 0.30623149673077982, - "unwrappedHeadingRadians": 0.30623149673077982, - "arcLengthMeters": 6.9250000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6125684228543236, - "yMeters": 2.9119574547776126, - "headingRadians": 0.32706483006411313, - "unwrappedHeadingRadians": 0.32706483006411313, - "arcLengthMeters": 6.9500000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6361577853246665, - "yMeters": 2.9202350977365956, - "headingRadians": 0.34789816339744645, - "unwrappedHeadingRadians": 0.34789816339744645, - "arcLengthMeters": 6.9750000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6595695903403742, - "yMeters": 2.9290023538991639, - "headingRadians": 0.36873149673077965, - "unwrappedHeadingRadians": 0.36873149673077965, - "arcLengthMeters": 7.0000000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6827936768952636, - "yMeters": 2.9382554181702374, - "headingRadians": 0.38956483006411297, - "unwrappedHeadingRadians": 0.38956483006411297, - "arcLengthMeters": 7.0250000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.70581996545523, - "yMeters": 2.9479902746081486, - "headingRadians": 0.41039816339744628, - "unwrappedHeadingRadians": 0.41039816339744628, - "arcLengthMeters": 7.0500000000000016, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.72863846233289, - "yMeters": 2.95820269816761, - "headingRadians": 0.4312314967307796, - "unwrappedHeadingRadians": 0.4312314967307796, - "arcLengthMeters": 7.075000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7512392640249583, - "yMeters": 2.9688882565334338, - "headingRadians": 0.45206483006411291, - "unwrappedHeadingRadians": 0.45206483006411291, - "arcLengthMeters": 7.1000000000000023, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7736125615104905, - "yMeters": 2.9800423120442114, - "headingRadians": 0.47289816339744623, - "unwrappedHeadingRadians": 0.47289816339744623, - "arcLengthMeters": 7.1250000000000027, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7957486445081159, - "yMeters": 2.9916600237051139, - "headingRadians": 0.49373149673077954, - "unwrappedHeadingRadians": 0.49373149673077954, - "arcLengthMeters": 7.150000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8176379056904173, - "yMeters": 3.0037363492889426, - "headingRadians": 0.51456483006411291, - "unwrappedHeadingRadians": 0.51456483006411291, - "arcLengthMeters": 7.1750000000000034, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8392708448536261, - "yMeters": 3.0162660475245158, - "headingRadians": 0.53539816339744628, - "unwrappedHeadingRadians": 0.53539816339744628, - "arcLengthMeters": 7.2000000000000037, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8606380730408274, - "yMeters": 3.0292436803714451, - "headingRadians": 0.55623149673077965, - "unwrappedHeadingRadians": 0.55623149673077965, - "arcLengthMeters": 7.2250000000000041, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8817303166168786, - "yMeters": 3.0426636153803095, - "headingRadians": 0.577064830064113, - "unwrappedHeadingRadians": 0.577064830064113, - "arcLengthMeters": 7.2500000000000044, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.902538421293281, - "yMeters": 3.0565200281372067, - "headingRadians": 0.59789816339744639, - "unwrappedHeadingRadians": 0.59789816339744639, - "arcLengthMeters": 7.2750000000000048, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.92305335610125, - "yMeters": 3.0708069047916196, - "headingRadians": 0.61873149673077976, - "unwrappedHeadingRadians": 0.61873149673077976, - "arcLengthMeters": 7.3000000000000052, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.9432662173112654, - "yMeters": 3.0855180446664994, - "headingRadians": 0.63956483006411313, - "unwrappedHeadingRadians": 0.63956483006411313, - "arcLengthMeters": 7.3250000000000055, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.9631682322973978, - "yMeters": 3.1006470629494345, - "headingRadians": 0.6603981633974465, - "unwrappedHeadingRadians": 0.6603981633974465, - "arcLengthMeters": 7.3500000000000059, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.9827507633447334, - "yMeters": 3.1161873934637372, - "headingRadians": 0.68123149673077987, - "unwrappedHeadingRadians": 0.68123149673077987, - "arcLengthMeters": 7.3750000000000062, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 5.0020053113982472, - "yMeters": 3.1321322915182419, - "headingRadians": 0.70206483006411324, - "unwrappedHeadingRadians": 0.70206483006411324, - "arcLengthMeters": 7.4000000000000066, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 200, - "startsAtGearSwitch": false, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 1, - "direction": 1, - "startIndex": 201, - "endIndex": 241, - "startsAtGearSwitch": true, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 2, - "direction": 0, - "startIndex": 242, - "endIndex": 298, - "startsAtGearSwitch": true, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "large-heading-change", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:44b6c95a3b553bb04df3912d5cf7c9110f4d629e37551b2c130d8d7f22706fce", - "map": { - "xMinMm": -7000.0, - "xMaxMm": 11000.0, - "yMinMm": -7000.0, - "yMaxMm": 11000.0, - "resolutionMm": 50.0, - "obstacles": [] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 1.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.0249995478901843, - "yMeters": 1.0001302071559617, - "headingRadians": 0.010416666666666666, - "unwrappedHeadingRadians": 0.010416666666666666, - "arcLengthMeters": 0.025, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499963831803427, - "yMeters": 1.0005208144955946, - "headingRadians": 0.020833333333333332, - "unwrappedHeadingRadians": 0.020833333333333332, - "arcLengthMeters": 0.05, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749877935647825, - "yMeters": 1.0011717796356725, - "headingRadians": 0.03125, - "unwrappedHeadingRadians": 0.03125, - "arcLengthMeters": 0.075000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999710673264458, - "yMeters": 1.0020830319425964, - "headingRadians": 0.041666666666666664, - "unwrappedHeadingRadians": 0.041666666666666664, - "arcLengthMeters": 0.1, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249434936311464, - "yMeters": 1.0032544725400563, - "headingRadians": 0.052083333333333329, - "unwrappedHeadingRadians": 0.052083333333333329, - "arcLengthMeters": 0.125, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499023628217124, - "yMeters": 1.004685974319762, - "headingRadians": 0.062499999999999993, - "unwrappedHeadingRadians": 0.062499999999999993, - "arcLengthMeters": 0.15, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1748449667119993, - "yMeters": 1.0063773819552337, - "headingRadians": 0.072916666666666657, - "unwrappedHeadingRadians": 0.072916666666666657, - "arcLengthMeters": 0.175, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1997685988807447, - "yMeters": 1.0083285119186571, - "headingRadians": 0.083333333333333329, - "unwrappedHeadingRadians": 0.083333333333333329, - "arcLengthMeters": 0.2, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.224670554965231, - "yMeters": 1.0105391525007958, - "headingRadians": 0.09375, - "unwrappedHeadingRadians": 0.09375, - "arcLengthMeters": 0.22500000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2495481329547253, - "yMeters": 1.0130090638339635, - "headingRadians": 0.10416666666666667, - "unwrappedHeadingRadians": 0.10416666666666667, - "arcLengthMeters": 0.25000000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2743986334836632, - "yMeters": 1.0157379779180522, - "headingRadians": 0.11458333333333334, - "unwrappedHeadingRadians": 0.11458333333333334, - "arcLengthMeters": 0.27500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2992193601245465, - "yMeters": 1.0187255986496104, - "headingRadians": 0.125, - "unwrappedHeadingRadians": 0.125, - "arcLengthMeters": 0.30000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3240076196805208, - "yMeters": 1.0219716018539722, - "headingRadians": 0.13541666666666666, - "unwrappedHeadingRadians": 0.13541666666666666, - "arcLengthMeters": 0.325, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3487607224776048, - "yMeters": 1.0254756353204335, - "headingRadians": 0.14583333333333331, - "unwrappedHeadingRadians": 0.14583333333333331, - "arcLengthMeters": 0.35, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3734759826565346, - "yMeters": 1.0292373188404673, - "headingRadians": 0.15624999999999997, - "unwrappedHeadingRadians": 0.15624999999999997, - "arcLengthMeters": 0.37499999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3981507184641961, - "yMeters": 1.0332562442489797, - "headingRadians": 0.16666666666666663, - "unwrappedHeadingRadians": 0.16666666666666663, - "arcLengthMeters": 0.39999999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4227822525446114, - "yMeters": 1.0375319754685981, - "headingRadians": 0.17708333333333329, - "unwrappedHeadingRadians": 0.17708333333333329, - "arcLengthMeters": 0.42499999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4473679122294476, - "yMeters": 1.0420640485569872, - "headingRadians": 0.18749999999999994, - "unwrappedHeadingRadians": 0.18749999999999994, - "arcLengthMeters": 0.44999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4719050298280172, - "yMeters": 1.0468519717571914, - "headingRadians": 0.1979166666666666, - "unwrappedHeadingRadians": 0.1979166666666666, - "arcLengthMeters": 0.47499999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4963909429167384, - "yMeters": 1.051895225550991, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.49999999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5207947384276534, - "yMeters": 1.0573203669662046, - "headingRadians": 0.22916666666666655, - "unwrappedHeadingRadians": 0.22916666666666655, - "arcLengthMeters": 0.52499999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5450802225637967, - "yMeters": 1.0632527067227218, - "headingRadians": 0.24999999999999989, - "unwrappedHeadingRadians": 0.24999999999999989, - "arcLengthMeters": 0.54999999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5692368551316926, - "yMeters": 1.0696896701134277, - "headingRadians": 0.2708333333333332, - "unwrappedHeadingRadians": 0.2708333333333332, - "arcLengthMeters": 0.57499999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5932541518610028, - "yMeters": 1.0766284634184542, - "headingRadians": 0.29166666666666652, - "unwrappedHeadingRadians": 0.29166666666666652, - "arcLengthMeters": 0.59999999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6171216889548261, - "yMeters": 1.0840660751176889, - "headingRadians": 0.31249999999999983, - "unwrappedHeadingRadians": 0.31249999999999983, - "arcLengthMeters": 0.62499999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6408291076137518, - "yMeters": 1.0919992771978104, - "headingRadians": 0.33333333333333315, - "unwrappedHeadingRadians": 0.33333333333333315, - "arcLengthMeters": 0.6499999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6643661185317042, - "yMeters": 1.1004246265532853, - "headingRadians": 0.35416666666666646, - "unwrappedHeadingRadians": 0.35416666666666646, - "arcLengthMeters": 0.67499999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6877225063616261, - "yMeters": 1.1093384664807182, - "headingRadians": 0.37499999999999978, - "unwrappedHeadingRadians": 0.37499999999999978, - "arcLengthMeters": 0.69999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7108881341490618, - "yMeters": 1.1187369282659045, - "headingRadians": 0.39583333333333309, - "unwrappedHeadingRadians": 0.39583333333333309, - "arcLengthMeters": 0.72499999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7338529477317186, - "yMeters": 1.1286159328628991, - "headingRadians": 0.41666666666666641, - "unwrappedHeadingRadians": 0.41666666666666641, - "arcLengthMeters": 0.74999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7566069801030946, - "yMeters": 1.1389711926643717, - "headingRadians": 0.43749999999999972, - "unwrappedHeadingRadians": 0.43749999999999972, - "arcLengthMeters": 0.77499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7791403557382794, - "yMeters": 1.1497982133624807, - "headingRadians": 0.45833333333333304, - "unwrappedHeadingRadians": 0.45833333333333304, - "arcLengthMeters": 0.79999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8014432948800518, - "yMeters": 1.1610922958994561, - "headingRadians": 0.47916666666666635, - "unwrappedHeadingRadians": 0.47916666666666635, - "arcLengthMeters": 0.825, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8235061177834124, - "yMeters": 1.1728485385070482, - "headingRadians": 0.49999999999999967, - "unwrappedHeadingRadians": 0.49999999999999967, - "arcLengthMeters": 0.85, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8453192489167098, - "yMeters": 1.185061838833954, - "headingRadians": 0.520833333333333, - "unwrappedHeadingRadians": 0.520833333333333, - "arcLengthMeters": 0.875, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8668732211175367, - "yMeters": 1.1977268961602989, - "headingRadians": 0.54166666666666641, - "unwrappedHeadingRadians": 0.54166666666666641, - "arcLengthMeters": 0.9, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8881586797015932, - "yMeters": 1.2108382136982139, - "headingRadians": 0.56249999999999978, - "unwrappedHeadingRadians": 0.56249999999999978, - "arcLengthMeters": 0.925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9091663865227326, - "yMeters": 1.2243901009775071, - "headingRadians": 0.58333333333333315, - "unwrappedHeadingRadians": 0.58333333333333315, - "arcLengthMeters": 0.95000000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.929887223982429, - "yMeters": 1.2383766763153972, - "headingRadians": 0.60416666666666652, - "unwrappedHeadingRadians": 0.60416666666666652, - "arcLengthMeters": 0.97500000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9503121989869237, - "yMeters": 1.2527918693692341, - "headingRadians": 0.62499999999999978, - "unwrappedHeadingRadians": 0.62499999999999978, - "arcLengthMeters": 1.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9705097264782969, - "yMeters": 1.2675246298659062, - "headingRadians": 0.63541666666666641, - "unwrappedHeadingRadians": 0.63541666666666641, - "arcLengthMeters": 1.025, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9905526947137522, - "yMeters": 1.28246697817297, - "headingRadians": 0.645833333333333, - "unwrappedHeadingRadians": 0.645833333333333, - "arcLengthMeters": 1.0499999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0104389289117135, - "yMeters": 1.2976172929565284, - "headingRadians": 0.65624999999999967, - "unwrappedHeadingRadians": 0.65624999999999967, - "arcLengthMeters": 1.0749999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0301662712971833, - "yMeters": 1.3129739303170818, - "headingRadians": 0.6666666666666663, - "unwrappedHeadingRadians": 0.6666666666666663, - "arcLengthMeters": 1.0999999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.049732581335872, - "yMeters": 1.3285352239678998, - "headingRadians": 0.67708333333333293, - "unwrappedHeadingRadians": 0.67708333333333293, - "arcLengthMeters": 1.1249999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0691357359664608, - "yMeters": 1.3442994854158252, - "headingRadians": 0.68749999999999956, - "unwrappedHeadingRadians": 0.68749999999999956, - "arcLengthMeters": 1.1499999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0883736298309654, - "yMeters": 1.3602650041444835, - "headingRadians": 0.69791666666666619, - "unwrappedHeadingRadians": 0.69791666666666619, - "arcLengthMeters": 1.1749999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.10744417550318, - "yMeters": 1.3764300477998854, - "headingRadians": 0.70833333333333282, - "unwrappedHeadingRadians": 0.70833333333333282, - "arcLengthMeters": 1.1999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1263453037151763, - "yMeters": 1.3927928623783974, - "headingRadians": 0.71874999999999944, - "unwrappedHeadingRadians": 0.71874999999999944, - "arcLengthMeters": 1.2249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1450749635818296, - "yMeters": 1.4093516724170609, - "headingRadians": 0.72916666666666607, - "unwrappedHeadingRadians": 0.72916666666666607, - "arcLengthMeters": 1.2499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1636311228233542, - "yMeters": 1.4261046811862423, - "headingRadians": 0.7395833333333327, - "unwrappedHeadingRadians": 0.7395833333333327, - "arcLengthMeters": 1.274999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.182011767985816, - "yMeters": 1.4430500708845864, - "headingRadians": 0.74999999999999933, - "unwrappedHeadingRadians": 0.74999999999999933, - "arcLengthMeters": 1.2999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2002149046596058, - "yMeters": 1.4601860028362605, - "headingRadians": 0.760416666666666, - "unwrappedHeadingRadians": 0.760416666666666, - "arcLengthMeters": 1.3249999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2182385576958437, - "yMeters": 1.4775106176904613, - "headingRadians": 0.77083333333333259, - "unwrappedHeadingRadians": 0.77083333333333259, - "arcLengthMeters": 1.3499999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2360807714206947, - "yMeters": 1.4950220356231649, - "headingRadians": 0.78124999999999922, - "unwrappedHeadingRadians": 0.78124999999999922, - "arcLengthMeters": 1.3749999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.253739609847571, - "yMeters": 1.5127183565411002, - "headingRadians": 0.79166666666666585, - "unwrappedHeadingRadians": 0.79166666666666585, - "arcLengthMeters": 1.3999999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2712131568871987, - "yMeters": 1.5305976602879185, - "headingRadians": 0.80208333333333248, - "unwrappedHeadingRadians": 0.80208333333333248, - "arcLengthMeters": 1.4249999999999985, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2884995165555235, - "yMeters": 1.5486580068525444, - "headingRadians": 0.81249999999999911, - "unwrappedHeadingRadians": 0.81249999999999911, - "arcLengthMeters": 1.4499999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3055968131794384, - "yMeters": 1.5668974365796755, - "headingRadians": 0.82291666666666574, - "unwrappedHeadingRadians": 0.82291666666666574, - "arcLengthMeters": 1.4749999999999983, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3225031916003025, - "yMeters": 1.5853139703824195, - "headingRadians": 0.83333333333333237, - "unwrappedHeadingRadians": 0.83333333333333237, - "arcLengthMeters": 1.4999999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3392168173752395, - "yMeters": 1.6039056099570359, - "headingRadians": 0.843749999999999, - "unwrappedHeadingRadians": 0.843749999999999, - "arcLengthMeters": 1.5249999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3557358769761847, - "yMeters": 1.6226703379997627, - "headingRadians": 0.85416666666666563, - "unwrappedHeadingRadians": 0.85416666666666563, - "arcLengthMeters": 1.549999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.372058577986663, - "yMeters": 1.6416061184257078, - "headingRadians": 0.86458333333333226, - "unwrappedHeadingRadians": 0.86458333333333226, - "arcLengthMeters": 1.574999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3881831492962777, - "yMeters": 1.660710896589775, - "headingRadians": 0.87499999999999889, - "unwrappedHeadingRadians": 0.87499999999999889, - "arcLengthMeters": 1.5999999999999979, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4041078412928862, - "yMeters": 1.6799825995096063, - "headingRadians": 0.88541666666666552, - "unwrappedHeadingRadians": 0.88541666666666552, - "arcLengthMeters": 1.6249999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4198309260524429, - "yMeters": 1.6994191360905115, - "headingRadians": 0.89583333333333215, - "unwrappedHeadingRadians": 0.89583333333333215, - "arcLengthMeters": 1.6499999999999977, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.43535069752649, - "yMeters": 1.7190183973523663, - "headingRadians": 0.90624999999999878, - "unwrappedHeadingRadians": 0.90624999999999878, - "arcLengthMeters": 1.6749999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4506654717272736, - "yMeters": 1.7387782566584469, - "headingRadians": 0.91666666666666541, - "unwrappedHeadingRadians": 0.91666666666666541, - "arcLengthMeters": 1.6999999999999975, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.465773586910466, - "yMeters": 1.7586965699461847, - "headingRadians": 0.927083333333332, - "unwrappedHeadingRadians": 0.927083333333332, - "arcLengthMeters": 1.7249999999999974, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4806734037554756, - "yMeters": 1.778771175959809, - "headingRadians": 0.93749999999999867, - "unwrappedHeadingRadians": 0.93749999999999867, - "arcLengthMeters": 1.7499999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4953633055433229, - "yMeters": 1.7989998964848568, - "headingRadians": 0.9479166666666653, - "unwrappedHeadingRadians": 0.9479166666666653, - "arcLengthMeters": 1.7749999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5098416983320635, - "yMeters": 1.8193805365845206, - "headingRadians": 0.95833333333333193, - "unwrappedHeadingRadians": 0.95833333333333193, - "arcLengthMeters": 1.7999999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5241070111297406, - "yMeters": 1.8399108848378143, - "headingRadians": 0.96874999999999856, - "unwrappedHeadingRadians": 0.96874999999999856, - "arcLengthMeters": 1.8249999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5381576960648475, - "yMeters": 1.8605887135795232, - "headingRadians": 0.97916666666666519, - "unwrappedHeadingRadians": 0.97916666666666519, - "arcLengthMeters": 1.849999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.55199222855428, - "yMeters": 1.8814117791419207, - "headingRadians": 0.98958333333333182, - "unwrappedHeadingRadians": 0.98958333333333182, - "arcLengthMeters": 1.8749999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5656091074687639, - "yMeters": 1.9023778220982197, - "headingRadians": 0.99999999999999845, - "unwrappedHeadingRadians": 0.99999999999999845, - "arcLengthMeters": 1.8999999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5790068552957353, - "yMeters": 1.9234845675077321, - "headingRadians": 1.0104166666666652, - "unwrappedHeadingRadians": 1.0104166666666652, - "arcLengthMeters": 1.9249999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.59218401829966, - "yMeters": 1.9447297251627149, - "headingRadians": 1.0208333333333319, - "unwrappedHeadingRadians": 1.0208333333333319, - "arcLengthMeters": 1.9499999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.605139166679773, - "yMeters": 1.966110989836872, - "headingRadians": 1.0312499999999987, - "unwrappedHeadingRadians": 1.0312499999999987, - "arcLengthMeters": 1.9749999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6178708947252187, - "yMeters": 1.9876260415354823, - "headingRadians": 1.0416666666666652, - "unwrappedHeadingRadians": 1.0416666666666652, - "arcLengthMeters": 1.9999999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6303778209675825, - "yMeters": 2.0092725457471365, - "headingRadians": 1.0520833333333319, - "unwrappedHeadingRadians": 1.0520833333333319, - "arcLengthMeters": 2.0249999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6426585883307836, - "yMeters": 2.0310481536970433, - "headingRadians": 1.0624999999999987, - "unwrappedHeadingRadians": 1.0624999999999987, - "arcLengthMeters": 2.0499999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6547118642783287, - "yMeters": 2.0529505026018851, - "headingRadians": 1.0729166666666654, - "unwrappedHeadingRadians": 1.0729166666666654, - "arcLengthMeters": 2.074999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6665363409579013, - "yMeters": 2.0749772159261952, - "headingRadians": 1.0833333333333321, - "unwrappedHeadingRadians": 1.0833333333333321, - "arcLengthMeters": 2.0999999999999983, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6781307353432684, - "yMeters": 2.0971259036402263, - "headingRadians": 1.0937499999999989, - "unwrappedHeadingRadians": 1.0937499999999989, - "arcLengthMeters": 2.1249999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6894937893734978, - "yMeters": 2.1193941624792818, - "headingRadians": 1.1041666666666656, - "unwrappedHeadingRadians": 1.1041666666666656, - "arcLengthMeters": 2.149999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7006242700894663, - "yMeters": 2.1417795762044856, - "headingRadians": 1.1145833333333324, - "unwrappedHeadingRadians": 1.1145833333333324, - "arcLengthMeters": 2.1749999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7115209697676415, - "yMeters": 2.1642797158649576, - "headingRadians": 1.1249999999999991, - "unwrappedHeadingRadians": 1.1249999999999991, - "arcLengthMeters": 2.1999999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7221827060511288, - "yMeters": 2.1868921400613694, - "headingRadians": 1.1354166666666659, - "unwrappedHeadingRadians": 1.1354166666666659, - "arcLengthMeters": 2.225, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7326083220779616, - "yMeters": 2.2096143952108509, - "headingRadians": 1.1458333333333326, - "unwrappedHeadingRadians": 1.1458333333333326, - "arcLengthMeters": 2.2500000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7427966866066296, - "yMeters": 2.2324440158132188, - "headingRadians": 1.1562499999999993, - "unwrappedHeadingRadians": 1.1562499999999993, - "arcLengthMeters": 2.2750000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7527466941388261, - "yMeters": 2.2553785247184979, - "headingRadians": 1.1666666666666661, - "unwrappedHeadingRadians": 1.1666666666666661, - "arcLengthMeters": 2.3000000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7624572650393984, - "yMeters": 2.2784154333957067, - "headingRadians": 1.1770833333333328, - "unwrappedHeadingRadians": 1.1770833333333328, - "arcLengthMeters": 2.3250000000000015, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7719273456534967, - "yMeters": 2.3015522422028774, - "headingRadians": 1.1874999999999996, - "unwrappedHeadingRadians": 1.1874999999999996, - "arcLengthMeters": 2.3500000000000019, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7811559084209012, - "yMeters": 2.3247864406582828, - "headingRadians": 1.1979166666666663, - "unwrappedHeadingRadians": 1.1979166666666663, - "arcLengthMeters": 2.3750000000000022, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7901419519875197, - "yMeters": 2.3481155077128379, - "headingRadians": 1.208333333333333, - "unwrappedHeadingRadians": 1.208333333333333, - "arcLengthMeters": 2.4000000000000026, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.798884501314038, - "yMeters": 2.3715369120236494, - "headingRadians": 1.2187499999999998, - "unwrappedHeadingRadians": 1.2187499999999998, - "arcLengthMeters": 2.4250000000000029, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.80738260778172, - "yMeters": 2.39504811222868, - "headingRadians": 1.2291666666666665, - "unwrappedHeadingRadians": 1.2291666666666665, - "arcLengthMeters": 2.4500000000000033, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8156353492953374, - "yMeters": 2.418646557222504, - "headingRadians": 1.2395833333333333, - "unwrappedHeadingRadians": 1.2395833333333333, - "arcLengthMeters": 2.4750000000000036, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8236418303832216, - "yMeters": 2.4423296864331134, - "headingRadians": 1.2499999999999998, - "unwrappedHeadingRadians": 1.2499999999999998, - "arcLengthMeters": 2.5000000000000031, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8314011822944303, - "yMeters": 2.4660949300997608, - "headingRadians": 1.2604166666666665, - "unwrappedHeadingRadians": 1.2604166666666665, - "arcLengthMeters": 2.5250000000000035, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8389125630930097, - "yMeters": 2.4899397095517881, - "headingRadians": 1.2708333333333333, - "unwrappedHeadingRadians": 1.2708333333333333, - "arcLengthMeters": 2.5500000000000038, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.84617515774935, - "yMeters": 2.5138614374884316, - "headingRadians": 1.28125, - "unwrappedHeadingRadians": 1.28125, - "arcLengthMeters": 2.5750000000000042, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8531881782286228, - "yMeters": 2.5378575182595577, - "headingRadians": 1.2916666666666667, - "unwrappedHeadingRadians": 1.2916666666666667, - "arcLengthMeters": 2.6000000000000045, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8599508635762847, - "yMeters": 2.5619253481473065, - "headingRadians": 1.3020833333333335, - "unwrappedHeadingRadians": 1.3020833333333335, - "arcLengthMeters": 2.6250000000000049, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8664624800006475, - "yMeters": 2.5860623156486118, - "headingRadians": 1.3125000000000002, - "unwrappedHeadingRadians": 1.3125000000000002, - "arcLengthMeters": 2.6500000000000052, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8727223209524988, - "yMeters": 2.6102658017585632, - "headingRadians": 1.322916666666667, - "unwrappedHeadingRadians": 1.322916666666667, - "arcLengthMeters": 2.6750000000000056, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8787297072017655, - "yMeters": 2.6345331802545857, - "headingRadians": 1.3333333333333337, - "unwrappedHeadingRadians": 1.3333333333333337, - "arcLengthMeters": 2.700000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8844839869112162, - "yMeters": 2.6588618179813985, - "headingRadians": 1.3437500000000004, - "unwrappedHeadingRadians": 1.3437500000000004, - "arcLengthMeters": 2.7250000000000063, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8899845357071881, - "yMeters": 2.6832490751367293, - "headingRadians": 1.3541666666666672, - "unwrappedHeadingRadians": 1.3541666666666672, - "arcLengthMeters": 2.7500000000000067, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8952307567473343, - "yMeters": 2.7076923055577482, - "headingRadians": 1.3645833333333339, - "unwrappedHeadingRadians": 1.3645833333333339, - "arcLengthMeters": 2.775000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9002220807853885, - "yMeters": 2.7321888570081918, - "headingRadians": 1.3750000000000007, - "unwrappedHeadingRadians": 1.3750000000000007, - "arcLengthMeters": 2.8000000000000074, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.904957966232927, - "yMeters": 2.7567360714661477, - "headingRadians": 1.3854166666666674, - "unwrappedHeadingRadians": 1.3854166666666674, - "arcLengthMeters": 2.8250000000000077, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9094378992181373, - "yMeters": 2.781331285412465, - "headingRadians": 1.3958333333333341, - "unwrappedHeadingRadians": 1.3958333333333341, - "arcLengthMeters": 2.8500000000000081, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9136613936415756, - "yMeters": 2.8059718301197614, - "headingRadians": 1.4062500000000009, - "unwrappedHeadingRadians": 1.4062500000000009, - "arcLengthMeters": 2.8750000000000084, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9176279912289109, - "yMeters": 2.8306550319419976, - "headingRadians": 1.4166666666666676, - "unwrappedHeadingRadians": 1.4166666666666676, - "arcLengthMeters": 2.9000000000000088, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9213372615806508, - "yMeters": 2.8553782126045824, - "headingRadians": 1.4270833333333344, - "unwrappedHeadingRadians": 1.4270833333333344, - "arcLengthMeters": 2.9250000000000091, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9247888022188429, - "yMeters": 2.8801386894949816, - "headingRadians": 1.4375000000000011, - "unwrappedHeadingRadians": 1.4375000000000011, - "arcLengthMeters": 2.9500000000000095, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.927982238630745, - "yMeters": 2.9049337759537992, - "headingRadians": 1.4479166666666679, - "unwrappedHeadingRadians": 1.4479166666666679, - "arcLengthMeters": 2.97500000000001, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.930917224309463, - "yMeters": 2.9297607815662929, - "headingRadians": 1.4583333333333344, - "unwrappedHeadingRadians": 1.4583333333333344, - "arcLengthMeters": 3.0000000000000093, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9335934407915487, - "yMeters": 2.9546170124543036, - "headingRadians": 1.4687500000000011, - "unwrappedHeadingRadians": 1.4687500000000011, - "arcLengthMeters": 3.0250000000000097, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9360105976915549, - "yMeters": 2.9794997715685545, - "headingRadians": 1.4791666666666679, - "unwrappedHeadingRadians": 1.4791666666666679, - "arcLengthMeters": 3.05000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9381684327335438, - "yMeters": 3.0044063589812984, - "headingRadians": 1.4895833333333346, - "unwrappedHeadingRadians": 1.4895833333333346, - "arcLengthMeters": 3.0750000000000104, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 25.45584412271571, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 123, - "startsAtGearSwitch": false, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "rectangle-detour", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:6ad46b2782bc519b17d62dac9e238542a88f4265fb7dd3a1ebe84c04313cf6ce", - "map": { - "xMinMm": 0.0, - "xMaxMm": 6000.0, - "yMinMm": 0.0, - "yMaxMm": 4000.0, - "resolutionMm": 50.0, - "obstacles": [ - { - "kind": "axis-aligned-rectangle", - "xMinMm": 2700.0, - "xMaxMm": 3300.0, - "yMinMm": 1200.0, - "yMaxMm": 2800.0, - "centerXMm": 0.0, - "centerYMm": 0.0, - "radiusMm": 0.0 - } - ] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.0249995478901843, - "yMeters": 2.0001302071559617, - "headingRadians": 0.010416666666666666, - "unwrappedHeadingRadians": 0.010416666666666666, - "arcLengthMeters": 0.025, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499963831803427, - "yMeters": 2.0005208144955944, - "headingRadians": 0.020833333333333332, - "unwrappedHeadingRadians": 0.020833333333333332, - "arcLengthMeters": 0.05, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749877935647825, - "yMeters": 2.0011717796356723, - "headingRadians": 0.03125, - "unwrappedHeadingRadians": 0.03125, - "arcLengthMeters": 0.075000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999710673264458, - "yMeters": 2.002083031942596, - "headingRadians": 0.041666666666666664, - "unwrappedHeadingRadians": 0.041666666666666664, - "arcLengthMeters": 0.1, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249434936311464, - "yMeters": 2.0032544725400561, - "headingRadians": 0.052083333333333329, - "unwrappedHeadingRadians": 0.052083333333333329, - "arcLengthMeters": 0.125, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499023628217124, - "yMeters": 2.0046859743197616, - "headingRadians": 0.062499999999999993, - "unwrappedHeadingRadians": 0.062499999999999993, - "arcLengthMeters": 0.15, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1748449667119993, - "yMeters": 2.0063773819552333, - "headingRadians": 0.072916666666666657, - "unwrappedHeadingRadians": 0.072916666666666657, - "arcLengthMeters": 0.175, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1997685988807447, - "yMeters": 2.0083285119186565, - "headingRadians": 0.083333333333333329, - "unwrappedHeadingRadians": 0.083333333333333329, - "arcLengthMeters": 0.2, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.224670554965231, - "yMeters": 2.0105391525007952, - "headingRadians": 0.09375, - "unwrappedHeadingRadians": 0.09375, - "arcLengthMeters": 0.22500000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2495481329547253, - "yMeters": 2.0130090638339628, - "headingRadians": 0.10416666666666667, - "unwrappedHeadingRadians": 0.10416666666666667, - "arcLengthMeters": 0.25000000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2743986334836632, - "yMeters": 2.0157379779180515, - "headingRadians": 0.11458333333333334, - "unwrappedHeadingRadians": 0.11458333333333334, - "arcLengthMeters": 0.27500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2992193601245465, - "yMeters": 2.0187255986496098, - "headingRadians": 0.125, - "unwrappedHeadingRadians": 0.125, - "arcLengthMeters": 0.30000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3240076196805208, - "yMeters": 2.0219716018539717, - "headingRadians": 0.13541666666666666, - "unwrappedHeadingRadians": 0.13541666666666666, - "arcLengthMeters": 0.325, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3487607224776048, - "yMeters": 2.0254756353204333, - "headingRadians": 0.14583333333333331, - "unwrappedHeadingRadians": 0.14583333333333331, - "arcLengthMeters": 0.35, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3734759826565346, - "yMeters": 2.0292373188404671, - "headingRadians": 0.15624999999999997, - "unwrappedHeadingRadians": 0.15624999999999997, - "arcLengthMeters": 0.37499999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3981507184641961, - "yMeters": 2.0332562442489794, - "headingRadians": 0.16666666666666663, - "unwrappedHeadingRadians": 0.16666666666666663, - "arcLengthMeters": 0.39999999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4227822525446114, - "yMeters": 2.0375319754685979, - "headingRadians": 0.17708333333333329, - "unwrappedHeadingRadians": 0.17708333333333329, - "arcLengthMeters": 0.42499999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4473679122294476, - "yMeters": 2.0420640485569868, - "headingRadians": 0.18749999999999994, - "unwrappedHeadingRadians": 0.18749999999999994, - "arcLengthMeters": 0.44999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4719050298280172, - "yMeters": 2.0468519717571909, - "headingRadians": 0.1979166666666666, - "unwrappedHeadingRadians": 0.1979166666666666, - "arcLengthMeters": 0.47499999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4963909429167384, - "yMeters": 2.0518952255509908, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.49999999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5208503676505822, - "yMeters": 2.0570659645397069, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.52499999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5453097923844261, - "yMeters": 2.062236703528423, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.54999999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.56976921711827, - "yMeters": 2.0674074425171391, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.57499999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5942286418521139, - "yMeters": 2.0725781815058553, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.59999999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6186880665859578, - "yMeters": 2.0777489204945714, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.62499999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6431474913198016, - "yMeters": 2.0829196594832875, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.6499999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6676069160536455, - "yMeters": 2.0880903984720036, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.67499999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6920663407874894, - "yMeters": 2.0932611374607197, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.69999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7165257655213333, - "yMeters": 2.0984318764494359, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.72499999999999987, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7409851902551772, - "yMeters": 2.103602615438152, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.74999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7654446149890211, - "yMeters": 2.1087733544268681, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.77499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7899040397228649, - "yMeters": 2.1139440934155842, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.79999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8143634644567088, - "yMeters": 2.1191148324043003, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.825, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8388228891905527, - "yMeters": 2.1242855713930164, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.85, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8632823139243966, - "yMeters": 2.1294563103817326, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.875, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8877417386582405, - "yMeters": 2.1346270493704487, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.9, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9122011633920843, - "yMeters": 2.1397977883591648, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9366605881259282, - "yMeters": 2.1449685273478809, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.95000000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9611200128597721, - "yMeters": 2.150139266336597, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.97500000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9855794375936158, - "yMeters": 2.1553100053253131, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.99999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9855794375936158, - "yMeters": 2.1553100053253131, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.99999999999999989, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 1.9611473858823298, - "yMeters": 2.1500119681610288, - "headingRadians": 0.21874999999999986, - "unwrappedHeadingRadians": 0.21874999999999986, - "arcLengthMeters": 1.025, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9367718465717858, - "yMeters": 2.144459722494886, - "headingRadians": 0.22916666666666652, - "unwrappedHeadingRadians": 0.22916666666666652, - "arcLengthMeters": 1.0499999999999998, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9124554645533574, - "yMeters": 2.1386538707786493, - "headingRadians": 0.23958333333333318, - "unwrappedHeadingRadians": 0.23958333333333318, - "arcLengthMeters": 1.0749999999999997, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8882008782994995, - "yMeters": 2.1325950429818517, - "headingRadians": 0.24999999999999983, - "unwrappedHeadingRadians": 0.24999999999999983, - "arcLengthMeters": 1.0999999999999996, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.864010719577458, - "yMeters": 2.12628389652344, - "headingRadians": 0.26041666666666652, - "unwrappedHeadingRadians": 0.26041666666666652, - "arcLengthMeters": 1.1249999999999998, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8398876131637074, - "yMeters": 2.1197211162004397, - "headingRadians": 0.2708333333333332, - "unwrappedHeadingRadians": 0.2708333333333332, - "arcLengthMeters": 1.15, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8158341765591473, - "yMeters": 2.1129074141136526, - "headingRadians": 0.28124999999999989, - "unwrappedHeadingRadians": 0.28124999999999989, - "arcLengthMeters": 1.175, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7918530197050868, - "yMeters": 2.1058435295903868, - "headingRadians": 0.29166666666666657, - "unwrappedHeadingRadians": 0.29166666666666657, - "arcLengthMeters": 1.2000000000000002, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7679467447000516, - "yMeters": 2.0985302291042367, - "headingRadians": 0.30208333333333326, - "unwrappedHeadingRadians": 0.30208333333333326, - "arcLengthMeters": 1.2250000000000003, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7441179455174403, - "yMeters": 2.0909683061919173, - "headingRadians": 0.31249999999999994, - "unwrappedHeadingRadians": 0.31249999999999994, - "arcLengthMeters": 1.2500000000000004, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7203692077240622, - "yMeters": 2.083158581367158, - "headingRadians": 0.32291666666666663, - "unwrappedHeadingRadians": 0.32291666666666663, - "arcLengthMeters": 1.2750000000000006, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6967031081995889, - "yMeters": 2.0751019020316743, - "headingRadians": 0.33333333333333331, - "unwrappedHeadingRadians": 0.33333333333333331, - "arcLengthMeters": 1.3000000000000007, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6731222148569469, - "yMeters": 2.0667991423832186, - "headingRadians": 0.34375, - "unwrappedHeadingRadians": 0.34375, - "arcLengthMeters": 1.3250000000000008, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6496290863636838, - "yMeters": 2.0582512033207241, - "headingRadians": 0.35416666666666669, - "unwrappedHeadingRadians": 0.35416666666666669, - "arcLengthMeters": 1.350000000000001, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6262262718643377, - "yMeters": 2.0494590123465537, - "headingRadians": 0.36458333333333337, - "unwrappedHeadingRadians": 0.36458333333333337, - "arcLengthMeters": 1.3750000000000011, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.60291631070384, - "yMeters": 2.0404235234658583, - "headingRadians": 0.37500000000000006, - "unwrappedHeadingRadians": 0.37500000000000006, - "arcLengthMeters": 1.4000000000000012, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5797017321519808, - "yMeters": 2.0311457170830627, - "headingRadians": 0.38541666666666674, - "unwrappedHeadingRadians": 0.38541666666666674, - "arcLengthMeters": 1.4250000000000014, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5565850551289686, - "yMeters": 2.0216265998954857, - "headingRadians": 0.39583333333333343, - "unwrappedHeadingRadians": 0.39583333333333343, - "arcLengthMeters": 1.4500000000000015, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.533568787932112, - "yMeters": 2.011867204784108, - "headingRadians": 0.40625000000000011, - "unwrappedHeadingRadians": 0.40625000000000011, - "arcLengthMeters": 1.4750000000000016, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5106554279636548, - "yMeters": 2.001868590701497, - "headingRadians": 0.41666666666666674, - "unwrappedHeadingRadians": 0.41666666666666674, - "arcLengthMeters": 1.5000000000000016, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4879013955922789, - "yMeters": 1.9915133309000244, - "headingRadians": 0.43750000000000006, - "unwrappedHeadingRadians": 0.43750000000000006, - "arcLengthMeters": 1.5250000000000015, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.465368019957094, - "yMeters": 1.9806863102019154, - "headingRadians": 0.45833333333333337, - "unwrappedHeadingRadians": 0.45833333333333337, - "arcLengthMeters": 1.5500000000000014, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4430650808153214, - "yMeters": 1.96939222766494, - "headingRadians": 0.47916666666666669, - "unwrappedHeadingRadians": 0.47916666666666669, - "arcLengthMeters": 1.5750000000000013, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4210022579119608, - "yMeters": 1.9576359850573479, - "headingRadians": 0.5, - "unwrappedHeadingRadians": 0.5, - "arcLengthMeters": 1.6000000000000012, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3991891267786634, - "yMeters": 1.9454226847304421, - "headingRadians": 0.52083333333333337, - "unwrappedHeadingRadians": 0.52083333333333337, - "arcLengthMeters": 1.6250000000000013, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3776351545778365, - "yMeters": 1.932757627404097, - "headingRadians": 0.54166666666666674, - "unwrappedHeadingRadians": 0.54166666666666674, - "arcLengthMeters": 1.6500000000000015, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.35634969599378, - "yMeters": 1.919646309866182, - "headingRadians": 0.56250000000000011, - "unwrappedHeadingRadians": 0.56250000000000011, - "arcLengthMeters": 1.6750000000000016, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3353419891726404, - "yMeters": 1.9060944225868888, - "headingRadians": 0.58333333333333348, - "unwrappedHeadingRadians": 0.58333333333333348, - "arcLengthMeters": 1.7000000000000017, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.314621151712944, - "yMeters": 1.8921078472489987, - "headingRadians": 0.60416666666666685, - "unwrappedHeadingRadians": 0.60416666666666685, - "arcLengthMeters": 1.7250000000000019, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2941961767084493, - "yMeters": 1.8776926541951617, - "headingRadians": 0.62500000000000022, - "unwrappedHeadingRadians": 0.62500000000000022, - "arcLengthMeters": 1.750000000000002, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.274075928845035, - "yMeters": 1.8628550997932936, - "headingRadians": 0.64583333333333359, - "unwrappedHeadingRadians": 0.64583333333333359, - "arcLengthMeters": 1.7750000000000021, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2542691405533195, - "yMeters": 1.8476016237212376, - "headingRadians": 0.666666666666667, - "unwrappedHeadingRadians": 0.666666666666667, - "arcLengthMeters": 1.8000000000000023, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2347844082186807, - "yMeters": 1.8319388461718658, - "headingRadians": 0.68750000000000033, - "unwrappedHeadingRadians": 0.68750000000000033, - "arcLengthMeters": 1.8250000000000024, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2156301884503209, - "yMeters": 1.8158735649798357, - "headingRadians": 0.7083333333333337, - "unwrappedHeadingRadians": 0.7083333333333337, - "arcLengthMeters": 1.8500000000000025, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1968147944109959, - "yMeters": 1.7994127526712478, - "headingRadians": 0.72916666666666707, - "unwrappedHeadingRadians": 0.72916666666666707, - "arcLengthMeters": 1.8750000000000027, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1783463922090027, - "yMeters": 1.7825635534374848, - "headingRadians": 0.75000000000000044, - "unwrappedHeadingRadians": 0.75000000000000044, - "arcLengthMeters": 1.9000000000000028, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1602329973539887, - "yMeters": 1.7653332800345474, - "headingRadians": 0.77083333333333381, - "unwrappedHeadingRadians": 0.77083333333333381, - "arcLengthMeters": 1.9250000000000029, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.142482471278125, - "yMeters": 1.7477294106092278, - "headingRadians": 0.79166666666666718, - "unwrappedHeadingRadians": 0.79166666666666718, - "arcLengthMeters": 1.9500000000000031, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1251025179241485, - "yMeters": 1.7297595854535055, - "headingRadians": 0.81250000000000056, - "unwrappedHeadingRadians": 0.81250000000000056, - "arcLengthMeters": 1.9750000000000032, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.108100680401759, - "yMeters": 1.7114316036885677, - "headingRadians": 0.83333333333333381, - "unwrappedHeadingRadians": 0.83333333333333381, - "arcLengthMeters": 2.0000000000000031, - "direction": 1, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0913870546268221, - "yMeters": 1.6928399641139515, - "headingRadians": 0.84375000000000044, - "unwrappedHeadingRadians": 0.84375000000000044, - "arcLengthMeters": 2.025000000000003, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0748679950258768, - "yMeters": 1.6740752360712245, - "headingRadians": 0.85416666666666707, - "unwrappedHeadingRadians": 0.85416666666666707, - "arcLengthMeters": 2.0500000000000029, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0585452940153983, - "yMeters": 1.6551394556452796, - "headingRadians": 0.8645833333333337, - "unwrappedHeadingRadians": 0.8645833333333337, - "arcLengthMeters": 2.0750000000000028, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0424207227057833, - "yMeters": 1.6360346774812122, - "headingRadians": 0.87500000000000033, - "unwrappedHeadingRadians": 0.87500000000000033, - "arcLengthMeters": 2.1000000000000028, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0264960307091748, - "yMeters": 1.616762974561381, - "headingRadians": 0.885416666666667, - "unwrappedHeadingRadians": 0.885416666666667, - "arcLengthMeters": 2.1250000000000027, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0107729459496182, - "yMeters": 1.5973264379804757, - "headingRadians": 0.89583333333333359, - "unwrappedHeadingRadians": 0.89583333333333359, - "arcLengthMeters": 2.1500000000000026, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.99525317447557116, - "yMeters": 1.5777271767186209, - "headingRadians": 0.90625000000000022, - "unwrappedHeadingRadians": 0.90625000000000022, - "arcLengthMeters": 2.1750000000000025, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.97993840027478751, - "yMeters": 1.5579673174125404, - "headingRadians": 0.91666666666666685, - "unwrappedHeadingRadians": 0.91666666666666685, - "arcLengthMeters": 2.2000000000000024, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.9648302850915953, - "yMeters": 1.5380490041248025, - "headingRadians": 0.92708333333333348, - "unwrappedHeadingRadians": 0.92708333333333348, - "arcLengthMeters": 2.2250000000000023, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.94993046824658556, - "yMeters": 1.517974398111178, - "headingRadians": 0.93750000000000011, - "unwrappedHeadingRadians": 0.93750000000000011, - "arcLengthMeters": 2.2500000000000022, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.93524056645873832, - "yMeters": 1.4977456775861304, - "headingRadians": 0.94791666666666674, - "unwrappedHeadingRadians": 0.94791666666666674, - "arcLengthMeters": 2.2750000000000021, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.92076217366999757, - "yMeters": 1.4773650374864664, - "headingRadians": 0.95833333333333337, - "unwrappedHeadingRadians": 0.95833333333333337, - "arcLengthMeters": 2.300000000000002, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.90649686087232073, - "yMeters": 1.4568346892331732, - "headingRadians": 0.96875, - "unwrappedHeadingRadians": 0.96875, - "arcLengthMeters": 2.325000000000002, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.89244617593721387, - "yMeters": 1.4361568604914643, - "headingRadians": 0.97916666666666663, - "unwrappedHeadingRadians": 0.97916666666666663, - "arcLengthMeters": 2.3500000000000019, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.87861164344778131, - "yMeters": 1.4153337949290665, - "headingRadians": 0.98958333333333326, - "unwrappedHeadingRadians": 0.98958333333333326, - "arcLengthMeters": 2.3750000000000018, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.86499476453329749, - "yMeters": 1.3943677519727675, - "headingRadians": 0.99999999999999989, - "unwrappedHeadingRadians": 0.99999999999999989, - "arcLengthMeters": 2.4000000000000017, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.85159701670632626, - "yMeters": 1.3732610065632553, - "headingRadians": 1.0104166666666665, - "unwrappedHeadingRadians": 1.0104166666666665, - "arcLengthMeters": 2.4250000000000016, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.83841985370240135, - "yMeters": 1.3520158489082721, - "headingRadians": 1.0208333333333333, - "unwrappedHeadingRadians": 1.0208333333333333, - "arcLengthMeters": 2.450000000000002, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.82546470532228866, - "yMeters": 1.3306345842341154, - "headingRadians": 1.03125, - "unwrappedHeadingRadians": 1.03125, - "arcLengthMeters": 2.4750000000000023, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.2092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.81273297727684268, - "yMeters": 1.3091195325355052, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.5000000000000018, - "direction": 1, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 1.2092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.81273297727684268, - "yMeters": 1.3091195325355052, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.5000000000000018, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.2092016093317763, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 0.82535253263774133, - "yMeters": 1.3307007007729572, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.5250000000000017, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.2092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.83797208799864, - "yMeters": 1.3522818690104093, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.5500000000000016, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.2092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.85059164335953863, - "yMeters": 1.3738630372478613, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.5750000000000015, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.86321119872043728, - "yMeters": 1.3954442054853133, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.6000000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.87583075408133593, - "yMeters": 1.4170253737227654, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.6250000000000013, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.88845030944223458, - "yMeters": 1.4386065419602174, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.6500000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1592016093317761, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.90106986480313322, - "yMeters": 1.4601877101976695, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.6750000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.91368942016403187, - "yMeters": 1.4817688784351215, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.7000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.92630897552493052, - "yMeters": 1.5033500466725735, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.725000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.93892853088582917, - "yMeters": 1.5249312149100256, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.7500000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.1092016093317763, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.95154808624672782, - "yMeters": 1.5465123831474776, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.7750000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.96416764160762647, - "yMeters": 1.5680935513849297, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.8000000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.97678719696852512, - "yMeters": 1.5896747196223817, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.8250000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.98940675232942377, - "yMeters": 1.6112558878598338, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.8500000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0592016093317764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0020263076903224, - "yMeters": 1.6328370560972858, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.8750000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0146458630512212, - "yMeters": 1.6544182243347378, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.9000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.02726541841212, - "yMeters": 1.6759993925721899, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.9250000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0398849737730187, - "yMeters": 1.6975805608096419, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.95, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 1.0092016093317762, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0525045291339175, - "yMeters": 1.719161729047094, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 2.975, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.065124084494816, - "yMeters": 1.740742897284546, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0777436398557148, - "yMeters": 1.7623240655219981, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.025, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0903631952166135, - "yMeters": 1.78390523375945, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.05, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.95920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1029827505775123, - "yMeters": 1.8054864019969021, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.0749999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.115602305938411, - "yMeters": 1.8270675702343542, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.0999999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1282218612993098, - "yMeters": 1.8486487384718062, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.1249999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1408414166602086, - "yMeters": 1.8702299067092583, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.1499999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.90920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1534609720211073, - "yMeters": 1.8918110749467103, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.1749999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1660805273820061, - "yMeters": 1.9133922431841623, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.1999999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1787000827429048, - "yMeters": 1.9349734114216144, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.2249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1913196381038036, - "yMeters": 1.9565545796590664, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.2499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.85920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2039391934647024, - "yMeters": 1.9781357478965185, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.274999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2165587488256011, - "yMeters": 1.9997169161339705, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.2999999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2291783041864999, - "yMeters": 2.0212980843714226, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.3249999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2417978595473986, - "yMeters": 2.0428792526088748, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.3499999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.80920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2544174149082974, - "yMeters": 2.0644604208463271, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.3749999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2670369702691961, - "yMeters": 2.0860415890837793, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.4, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2796565256300949, - "yMeters": 2.1076227573212316, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.4250000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2922760809909937, - "yMeters": 2.1292039255586839, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.4500000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.75920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3048956363518924, - "yMeters": 2.1507850937961361, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.475000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.317515191712791, - "yMeters": 2.172366262033588, - "headingRadians": 1.0416666666666665, - "unwrappedHeadingRadians": 1.0416666666666665, - "arcLengthMeters": 3.5000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3302469197582372, - "yMeters": 2.1938813137321986, - "headingRadians": 1.0312499999999998, - "unwrappedHeadingRadians": 1.0312499999999998, - "arcLengthMeters": 3.5250000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3432020681383499, - "yMeters": 2.2152625784063558, - "headingRadians": 1.020833333333333, - "unwrappedHeadingRadians": 1.020833333333333, - "arcLengthMeters": 3.5500000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.70920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3563792311422747, - "yMeters": 2.236507736061339, - "headingRadians": 1.0104166666666663, - "unwrappedHeadingRadians": 1.0104166666666663, - "arcLengthMeters": 3.5750000000000015, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.369776978969246, - "yMeters": 2.2576144814708514, - "headingRadians": 0.99999999999999967, - "unwrappedHeadingRadians": 0.99999999999999967, - "arcLengthMeters": 3.6000000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3833938578837297, - "yMeters": 2.2785805244271504, - "headingRadians": 0.989583333333333, - "unwrappedHeadingRadians": 0.989583333333333, - "arcLengthMeters": 3.6250000000000013, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3972283903731624, - "yMeters": 2.2994035899895477, - "headingRadians": 0.97916666666666641, - "unwrappedHeadingRadians": 0.97916666666666641, - "arcLengthMeters": 3.6500000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.65920160933177618, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4112790753082693, - "yMeters": 2.3200814187312564, - "headingRadians": 0.96874999999999978, - "unwrappedHeadingRadians": 0.96874999999999978, - "arcLengthMeters": 3.6750000000000012, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4255443881059464, - "yMeters": 2.34061176698455, - "headingRadians": 0.95833333333333315, - "unwrappedHeadingRadians": 0.95833333333333315, - "arcLengthMeters": 3.7000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4400227808946868, - "yMeters": 2.3609924070842139, - "headingRadians": 0.94791666666666652, - "unwrappedHeadingRadians": 0.94791666666666652, - "arcLengthMeters": 3.725000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.60920160933177614, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4547126826825341, - "yMeters": 2.3812211276092614, - "headingRadians": 0.93749999999999989, - "unwrappedHeadingRadians": 0.93749999999999989, - "arcLengthMeters": 3.7500000000000009, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4696124995275437, - "yMeters": 2.4012957336228857, - "headingRadians": 0.92708333333333326, - "unwrappedHeadingRadians": 0.92708333333333326, - "arcLengthMeters": 3.7750000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4847206147107361, - "yMeters": 2.4212140469106234, - "headingRadians": 0.91666666666666663, - "unwrappedHeadingRadians": 0.91666666666666663, - "arcLengthMeters": 3.8000000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.55920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5000353889115194, - "yMeters": 2.440973906216704, - "headingRadians": 0.90625, - "unwrappedHeadingRadians": 0.90625, - "arcLengthMeters": 3.8250000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5155551603855666, - "yMeters": 2.4605731674785587, - "headingRadians": 0.89583333333333337, - "unwrappedHeadingRadians": 0.89583333333333337, - "arcLengthMeters": 3.8500000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5312782451451232, - "yMeters": 2.4800097040594644, - "headingRadians": 0.88541666666666674, - "unwrappedHeadingRadians": 0.88541666666666674, - "arcLengthMeters": 3.8750000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.547202937141732, - "yMeters": 2.4992814069792955, - "headingRadians": 0.87500000000000011, - "unwrappedHeadingRadians": 0.87500000000000011, - "arcLengthMeters": 3.9000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5633275084513467, - "yMeters": 2.5183861851433629, - "headingRadians": 0.86458333333333348, - "unwrappedHeadingRadians": 0.86458333333333348, - "arcLengthMeters": 3.9250000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5796502094618252, - "yMeters": 2.537321965569308, - "headingRadians": 0.85416666666666685, - "unwrappedHeadingRadians": 0.85416666666666685, - "arcLengthMeters": 3.95, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5961692690627705, - "yMeters": 2.5560866936120346, - "headingRadians": 0.84375000000000022, - "unwrappedHeadingRadians": 0.84375000000000022, - "arcLengthMeters": 3.975, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6128828948377079, - "yMeters": 2.5746783331866507, - "headingRadians": 0.83333333333333359, - "unwrappedHeadingRadians": 0.83333333333333359, - "arcLengthMeters": 4.0, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6297892732585719, - "yMeters": 2.5930948669893947, - "headingRadians": 0.822916666666667, - "unwrappedHeadingRadians": 0.822916666666667, - "arcLengthMeters": 4.025, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6468865698824864, - "yMeters": 2.6113342967165258, - "headingRadians": 0.81250000000000033, - "unwrappedHeadingRadians": 0.81250000000000033, - "arcLengthMeters": 4.0500000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6641729295508114, - "yMeters": 2.6293946432811515, - "headingRadians": 0.8020833333333337, - "unwrappedHeadingRadians": 0.8020833333333337, - "arcLengthMeters": 4.0750000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6816464765904391, - "yMeters": 2.64727394702797, - "headingRadians": 0.79166666666666707, - "unwrappedHeadingRadians": 0.79166666666666707, - "arcLengthMeters": 4.1000000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6993053150173156, - "yMeters": 2.6649702679459053, - "headingRadians": 0.78125000000000044, - "unwrappedHeadingRadians": 0.78125000000000044, - "arcLengthMeters": 4.1250000000000018, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7171475287421665, - "yMeters": 2.6824816858786091, - "headingRadians": 0.77083333333333381, - "unwrappedHeadingRadians": 0.77083333333333381, - "arcLengthMeters": 4.1500000000000021, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7351711817784043, - "yMeters": 2.69980630073281, - "headingRadians": 0.76041666666666718, - "unwrappedHeadingRadians": 0.76041666666666718, - "arcLengthMeters": 4.1750000000000025, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7533743184521942, - "yMeters": 2.7169422326844841, - "headingRadians": 0.75000000000000056, - "unwrappedHeadingRadians": 0.75000000000000056, - "arcLengthMeters": 4.2000000000000028, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7717549636146559, - "yMeters": 2.7338876223828286, - "headingRadians": 0.73958333333333393, - "unwrappedHeadingRadians": 0.73958333333333393, - "arcLengthMeters": 4.2250000000000032, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7903111228561805, - "yMeters": 2.75064063115201, - "headingRadians": 0.7291666666666673, - "unwrappedHeadingRadians": 0.7291666666666673, - "arcLengthMeters": 4.2500000000000036, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8090407827228339, - "yMeters": 2.767199441190674, - "headingRadians": 0.71875000000000067, - "unwrappedHeadingRadians": 0.71875000000000067, - "arcLengthMeters": 4.2750000000000039, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8279419109348298, - "yMeters": 2.7835622557691857, - "headingRadians": 0.708333333333334, - "unwrappedHeadingRadians": 0.708333333333334, - "arcLengthMeters": 4.3000000000000043, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8470124566070447, - "yMeters": 2.7997272994245876, - "headingRadians": 0.69791666666666741, - "unwrappedHeadingRadians": 0.69791666666666741, - "arcLengthMeters": 4.3250000000000046, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8662503504715493, - "yMeters": 2.8156928181532459, - "headingRadians": 0.68750000000000078, - "unwrappedHeadingRadians": 0.68750000000000078, - "arcLengthMeters": 4.350000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8856535051021381, - "yMeters": 2.8314570796011713, - "headingRadians": 0.67708333333333415, - "unwrappedHeadingRadians": 0.67708333333333415, - "arcLengthMeters": 4.3750000000000053, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9052198151408268, - "yMeters": 2.8470183732519896, - "headingRadians": 0.66666666666666752, - "unwrappedHeadingRadians": 0.66666666666666752, - "arcLengthMeters": 4.4000000000000057, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9249471575262962, - "yMeters": 2.862375010612543, - "headingRadians": 0.65625000000000089, - "unwrappedHeadingRadians": 0.65625000000000089, - "arcLengthMeters": 4.425000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9448333917242575, - "yMeters": 2.8775253253961015, - "headingRadians": 0.64583333333333426, - "unwrappedHeadingRadians": 0.64583333333333426, - "arcLengthMeters": 4.4500000000000064, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.1108664282504217, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9648763599597128, - "yMeters": 2.8924676737031652, - "headingRadians": 0.63541666666666763, - "unwrappedHeadingRadians": 0.63541666666666763, - "arcLengthMeters": 4.4750000000000068, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0609850517126862, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.985073887451086, - "yMeters": 2.907200434199837, - "headingRadians": 0.625000000000001, - "unwrappedHeadingRadians": 0.625000000000001, - "arcLengthMeters": 4.5000000000000071, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0609850517126862, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0054988624555805, - "yMeters": 2.921615627253674, - "headingRadians": 0.60416666666666763, - "unwrappedHeadingRadians": 0.60416666666666763, - "arcLengthMeters": 4.5250000000000075, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0262196999152766, - "yMeters": 2.9356022025915638, - "headingRadians": 0.58333333333333426, - "unwrappedHeadingRadians": 0.58333333333333426, - "arcLengthMeters": 4.5500000000000078, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0472274067364165, - "yMeters": 2.949154089870857, - "headingRadians": 0.56250000000000089, - "unwrappedHeadingRadians": 0.56250000000000089, - "arcLengthMeters": 4.5750000000000082, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0685128653204732, - "yMeters": 2.962265407408772, - "headingRadians": 0.54166666666666752, - "unwrappedHeadingRadians": 0.54166666666666752, - "arcLengthMeters": 4.6000000000000085, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0900668375213, - "yMeters": 2.9749304647351171, - "headingRadians": 0.52083333333333415, - "unwrappedHeadingRadians": 0.52083333333333415, - "arcLengthMeters": 4.6250000000000089, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1118799686545975, - "yMeters": 2.9871437650620232, - "headingRadians": 0.50000000000000078, - "unwrappedHeadingRadians": 0.50000000000000078, - "arcLengthMeters": 4.6500000000000092, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1339427915579581, - "yMeters": 2.9989000076696155, - "headingRadians": 0.47916666666666746, - "unwrappedHeadingRadians": 0.47916666666666746, - "arcLengthMeters": 4.67500000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1562457306997307, - "yMeters": 3.0101940902065909, - "headingRadians": 0.45833333333333415, - "unwrappedHeadingRadians": 0.45833333333333415, - "arcLengthMeters": 4.70000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1787791063349156, - "yMeters": 3.0210211109047, - "headingRadians": 0.43750000000000083, - "unwrappedHeadingRadians": 0.43750000000000083, - "arcLengthMeters": 4.72500000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2015331387062913, - "yMeters": 3.0313763707061723, - "headingRadians": 0.41666666666666752, - "unwrappedHeadingRadians": 0.41666666666666752, - "arcLengthMeters": 4.7500000000000107, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2244979522889481, - "yMeters": 3.0412553753031668, - "headingRadians": 0.3958333333333342, - "unwrappedHeadingRadians": 0.3958333333333342, - "arcLengthMeters": 4.775000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2476635800763836, - "yMeters": 3.0506538370883529, - "headingRadians": 0.37500000000000089, - "unwrappedHeadingRadians": 0.37500000000000089, - "arcLengthMeters": 4.8000000000000114, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2710199679063052, - "yMeters": 3.059567677015786, - "headingRadians": 0.35416666666666757, - "unwrappedHeadingRadians": 0.35416666666666757, - "arcLengthMeters": 4.8250000000000117, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2945569788242577, - "yMeters": 3.0679930263712611, - "headingRadians": 0.33333333333333426, - "unwrappedHeadingRadians": 0.33333333333333426, - "arcLengthMeters": 4.8500000000000121, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3182643974831834, - "yMeters": 3.0759262284513826, - "headingRadians": 0.31250000000000094, - "unwrappedHeadingRadians": 0.31250000000000094, - "arcLengthMeters": 4.8750000000000124, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3421319345770066, - "yMeters": 3.0833638401506174, - "headingRadians": 0.29166666666666763, - "unwrappedHeadingRadians": 0.29166666666666763, - "arcLengthMeters": 4.9000000000000128, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3661492313063168, - "yMeters": 3.0903026334556438, - "headingRadians": 0.27083333333333431, - "unwrappedHeadingRadians": 0.27083333333333431, - "arcLengthMeters": 4.9250000000000131, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3903058638742127, - "yMeters": 3.0967395968463496, - "headingRadians": 0.250000000000001, - "unwrappedHeadingRadians": 0.250000000000001, - "arcLengthMeters": 4.9500000000000135, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4145913480103558, - "yMeters": 3.1026719366028668, - "headingRadians": 0.22916666666666766, - "unwrappedHeadingRadians": 0.22916666666666766, - "arcLengthMeters": 4.9750000000000139, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4389951435212707, - "yMeters": 3.1080970780180803, - "headingRadians": 0.20833333333333445, - "unwrappedHeadingRadians": 0.20833333333333445, - "arcLengthMeters": 5.0000000000000133, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4635066588649162, - "yMeters": 3.1130126665150821, - "headingRadians": 0.18750000000000111, - "unwrappedHeadingRadians": 0.18750000000000111, - "arcLengthMeters": 5.0250000000000137, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4881152557475419, - "yMeters": 3.117416568669086, - "headingRadians": 0.16666666666666777, - "unwrappedHeadingRadians": 0.16666666666666777, - "arcLengthMeters": 5.050000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5128102537408377, - "yMeters": 3.1213068731333591, - "headingRadians": 0.14583333333333443, - "unwrappedHeadingRadians": 0.14583333333333443, - "arcLengthMeters": 5.0750000000000144, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5375809349173668, - "yMeters": 3.1246818914687706, - "headingRadians": 0.12500000000000108, - "unwrappedHeadingRadians": 0.12500000000000108, - "arcLengthMeters": 5.1000000000000147, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5624165485022772, - "yMeters": 3.1275401588765939, - "headingRadians": 0.10416666666666774, - "unwrappedHeadingRadians": 0.10416666666666774, - "arcLengthMeters": 5.1250000000000151, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5873063155392675, - "yMeters": 3.1298804348342468, - "headingRadians": 0.0833333333333344, - "unwrappedHeadingRadians": 0.0833333333333344, - "arcLengthMeters": 5.1500000000000155, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6122394335687837, - "yMeters": 3.1317017036336945, - "headingRadians": 0.062500000000001055, - "unwrappedHeadingRadians": 0.062500000000001055, - "arcLengthMeters": 5.1750000000000158, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.637205081316417, - "yMeters": 3.1330031748222771, - "headingRadians": 0.041666666666667719, - "unwrappedHeadingRadians": 0.041666666666667719, - "arcLengthMeters": 5.2000000000000162, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6621924233894685, - "yMeters": 3.1337842835457779, - "headingRadians": 0.020833333333334383, - "unwrappedHeadingRadians": 0.020833333333334383, - "arcLengthMeters": 5.2250000000000165, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.68719061497964, - "yMeters": 3.1340446907935751, - "headingRadians": 1.0477729794899915E-15, - "unwrappedHeadingRadians": 1.0477729794899915E-15, - "arcLengthMeters": 5.2500000000000169, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7121888065698112, - "yMeters": 3.1337842835457779, - "headingRadians": -0.020833333333332288, - "unwrappedHeadingRadians": -0.020833333333332288, - "arcLengthMeters": 5.2750000000000172, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7371761486428627, - "yMeters": 3.1330031748222771, - "headingRadians": -0.041666666666665624, - "unwrappedHeadingRadians": -0.041666666666665624, - "arcLengthMeters": 5.3000000000000176, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.762141796390496, - "yMeters": 3.131701703633694, - "headingRadians": -0.062499999999998959, - "unwrappedHeadingRadians": -0.062499999999998959, - "arcLengthMeters": 5.3250000000000179, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7870749144200122, - "yMeters": 3.1298804348342464, - "headingRadians": -0.083333333333332288, - "unwrappedHeadingRadians": -0.083333333333332288, - "arcLengthMeters": 5.3500000000000183, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8119646814570025, - "yMeters": 3.1275401588765934, - "headingRadians": -0.10416666666666563, - "unwrappedHeadingRadians": -0.10416666666666563, - "arcLengthMeters": 5.3750000000000187, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8368002950419129, - "yMeters": 3.12468189146877, - "headingRadians": -0.12499999999999897, - "unwrappedHeadingRadians": -0.12499999999999897, - "arcLengthMeters": 5.400000000000019, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.861570976218442, - "yMeters": 3.1213068731333586, - "headingRadians": -0.14583333333333232, - "unwrappedHeadingRadians": -0.14583333333333232, - "arcLengthMeters": 5.4250000000000194, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8862659742117378, - "yMeters": 3.1174165686690856, - "headingRadians": -0.16666666666666566, - "unwrappedHeadingRadians": -0.16666666666666566, - "arcLengthMeters": 5.45000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9108745710943635, - "yMeters": 3.1130126665150817, - "headingRadians": -0.187499999999999, - "unwrappedHeadingRadians": -0.187499999999999, - "arcLengthMeters": 5.47500000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9353860864380086, - "yMeters": 3.10809707801808, - "headingRadians": -0.2083333333333322, - "unwrappedHeadingRadians": -0.2083333333333322, - "arcLengthMeters": 5.5000000000000195, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9598181381492945, - "yMeters": 3.1027990408537955, - "headingRadians": -0.21874999999999886, - "unwrappedHeadingRadians": -0.21874999999999886, - "arcLengthMeters": 5.52500000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9841936774598388, - "yMeters": 3.0972467951876528, - "headingRadians": -0.22916666666666552, - "unwrappedHeadingRadians": -0.22916666666666552, - "arcLengthMeters": 5.55000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0085100594782674, - "yMeters": 3.0914409434714161, - "headingRadians": -0.23958333333333218, - "unwrappedHeadingRadians": -0.23958333333333218, - "arcLengthMeters": 5.5750000000000206, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0327646457321253, - "yMeters": 3.0853821156746184, - "headingRadians": -0.24999999999999883, - "unwrappedHeadingRadians": -0.24999999999999883, - "arcLengthMeters": 5.600000000000021, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.056954804454167, - "yMeters": 3.0790709692162066, - "headingRadians": -0.26041666666666552, - "unwrappedHeadingRadians": -0.26041666666666552, - "arcLengthMeters": 5.6250000000000213, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0810779108679176, - "yMeters": 3.0725081888932069, - "headingRadians": -0.2708333333333322, - "unwrappedHeadingRadians": -0.2708333333333322, - "arcLengthMeters": 5.6500000000000217, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.105131347472478, - "yMeters": 3.0656944868064198, - "headingRadians": -0.28124999999999889, - "unwrappedHeadingRadians": -0.28124999999999889, - "arcLengthMeters": 5.675000000000022, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1291125043265384, - "yMeters": 3.0586306022831535, - "headingRadians": -0.29166666666666557, - "unwrappedHeadingRadians": -0.29166666666666557, - "arcLengthMeters": 5.7000000000000224, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1530187793315738, - "yMeters": 3.0513173017970039, - "headingRadians": -0.30208333333333226, - "unwrappedHeadingRadians": -0.30208333333333226, - "arcLengthMeters": 5.7250000000000227, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1768475785141854, - "yMeters": 3.0437553788846845, - "headingRadians": -0.31249999999999895, - "unwrappedHeadingRadians": -0.31249999999999895, - "arcLengthMeters": 5.7500000000000231, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2005963163075637, - "yMeters": 3.0359456540599252, - "headingRadians": -0.32291666666666563, - "unwrappedHeadingRadians": -0.32291666666666563, - "arcLengthMeters": 5.7750000000000234, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2242624158320368, - "yMeters": 3.0278889747244415, - "headingRadians": -0.33333333333333232, - "unwrappedHeadingRadians": -0.33333333333333232, - "arcLengthMeters": 5.8000000000000238, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2478433091746788, - "yMeters": 3.0195862150759858, - "headingRadians": -0.343749999999999, - "unwrappedHeadingRadians": -0.343749999999999, - "arcLengthMeters": 5.8250000000000242, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2713364376679417, - "yMeters": 3.0110382760134913, - "headingRadians": -0.35416666666666569, - "unwrappedHeadingRadians": -0.35416666666666569, - "arcLengthMeters": 5.8500000000000245, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2947392521672878, - "yMeters": 3.0022460850393209, - "headingRadians": -0.36458333333333237, - "unwrappedHeadingRadians": -0.36458333333333237, - "arcLengthMeters": 5.8750000000000249, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3180492133277855, - "yMeters": 2.9932105961586255, - "headingRadians": -0.37499999999999906, - "unwrappedHeadingRadians": -0.37499999999999906, - "arcLengthMeters": 5.9000000000000252, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3412637918796446, - "yMeters": 2.98393278977583, - "headingRadians": -0.38541666666666574, - "unwrappedHeadingRadians": -0.38541666666666574, - "arcLengthMeters": 5.9250000000000256, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3643804689026569, - "yMeters": 2.9744136725882528, - "headingRadians": -0.39583333333333243, - "unwrappedHeadingRadians": -0.39583333333333243, - "arcLengthMeters": 5.9500000000000259, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3873967360995136, - "yMeters": 2.9646542774768752, - "headingRadians": -0.40624999999999911, - "unwrappedHeadingRadians": -0.40624999999999911, - "arcLengthMeters": 5.9750000000000263, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4103100960679704, - "yMeters": 2.9546556633942642, - "headingRadians": -0.41666666666666574, - "unwrappedHeadingRadians": -0.41666666666666574, - "arcLengthMeters": 6.0000000000000266, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4331180625718325, - "yMeters": 2.9444189152496723, - "headingRadians": -0.42708333333333243, - "unwrappedHeadingRadians": -0.42708333333333243, - "arcLengthMeters": 6.025000000000027, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4558181608107228, - "yMeters": 2.9339451437913189, - "headingRadians": -0.43749999999999911, - "unwrappedHeadingRadians": -0.43749999999999911, - "arcLengthMeters": 6.0500000000000274, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4784079276886146, - "yMeters": 2.9232354854858649, - "headingRadians": -0.4479166666666658, - "unwrappedHeadingRadians": -0.4479166666666658, - "arcLengthMeters": 6.0750000000000277, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5008849120810925, - "yMeters": 2.912291102395101, - "headingRadians": -0.45833333333333248, - "unwrappedHeadingRadians": -0.45833333333333248, - "arcLengthMeters": 6.1000000000000281, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5232466751013125, - "yMeters": 2.9011131820498579, - "headingRadians": -0.46874999999999917, - "unwrappedHeadingRadians": -0.46874999999999917, - "arcLengthMeters": 6.1250000000000284, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5454907903646373, - "yMeters": 2.8897029373211502, - "headingRadians": -0.47916666666666585, - "unwrappedHeadingRadians": -0.47916666666666585, - "arcLengthMeters": 6.1500000000000288, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5676148442519127, - "yMeters": 2.8780616062885733, - "headingRadians": -0.48958333333333254, - "unwrappedHeadingRadians": -0.48958333333333254, - "arcLengthMeters": 6.1750000000000291, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5896164361713589, - "yMeters": 2.8661904521059656, - "headingRadians": -0.49999999999999922, - "unwrappedHeadingRadians": -0.49999999999999922, - "arcLengthMeters": 6.2000000000000295, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6114931788190505, - "yMeters": 2.854090762864347, - "headingRadians": -0.51041666666666585, - "unwrappedHeadingRadians": -0.51041666666666585, - "arcLengthMeters": 6.22500000000003, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6332426984379533, - "yMeters": 2.8417638514521544, - "headingRadians": -0.52083333333333248, - "unwrappedHeadingRadians": -0.52083333333333248, - "arcLengthMeters": 6.25000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6548626350754891, - "yMeters": 2.8292110554127845, - "headingRadians": -0.53124999999999911, - "unwrappedHeadingRadians": -0.53124999999999911, - "arcLengthMeters": 6.2750000000000306, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6763506428396067, - "yMeters": 2.8164337367994645, - "headingRadians": -0.54166666666666574, - "unwrappedHeadingRadians": -0.54166666666666574, - "arcLengthMeters": 6.3000000000000309, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6977043901533242, - "yMeters": 2.8034332820274583, - "headingRadians": -0.55208333333333237, - "unwrappedHeadingRadians": -0.55208333333333237, - "arcLengthMeters": 6.3250000000000313, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7189215600077197, - "yMeters": 2.7902111017236346, - "headingRadians": -0.562499999999999, - "unwrappedHeadingRadians": -0.562499999999999, - "arcLengthMeters": 6.3500000000000316, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7399998502133389, - "yMeters": 2.7767686305734043, - "headingRadians": -0.57291666666666563, - "unwrappedHeadingRadians": -0.57291666666666563, - "arcLengthMeters": 6.375000000000032, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7609369736499989, - "yMeters": 2.7631073271650486, - "headingRadians": -0.58333333333333226, - "unwrappedHeadingRadians": -0.58333333333333226, - "arcLengthMeters": 6.4000000000000323, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7817306585149519, - "yMeters": 2.7492286738314538, - "headingRadians": -0.59374999999999889, - "unwrappedHeadingRadians": -0.59374999999999889, - "arcLengthMeters": 6.4250000000000327, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8023786485693911, - "yMeters": 2.7351341764892689, - "headingRadians": -0.60416666666666552, - "unwrappedHeadingRadians": -0.60416666666666552, - "arcLengthMeters": 6.450000000000033, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8228787033832656, - "yMeters": 2.7208253644755054, - "headingRadians": -0.61458333333333215, - "unwrappedHeadingRadians": -0.61458333333333215, - "arcLengthMeters": 6.4750000000000334, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8432285985783805, - "yMeters": 2.706303790381595, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.5000000000000338, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8635026765660108, - "yMeters": 2.6916763585580834, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.5250000000000332, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.883776754553641, - "yMeters": 2.6770489267345718, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.5500000000000327, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9040508325412713, - "yMeters": 2.6624214949110603, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.5750000000000322, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9243249105289015, - "yMeters": 2.6477940630875487, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.6000000000000316, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9445989885165318, - "yMeters": 2.6331666312640372, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.6250000000000311, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.964873066504162, - "yMeters": 2.6185391994405256, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.6500000000000306, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9851471444917923, - "yMeters": 2.603911767617014, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.67500000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0054212224794226, - "yMeters": 2.5892843357935025, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.7000000000000295, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0256953004670528, - "yMeters": 2.5746569039699909, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.725000000000029, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0459693784546831, - "yMeters": 2.5600294721464794, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.7500000000000284, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0662434564423133, - "yMeters": 2.5454020403229678, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.7750000000000279, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0865175344299436, - "yMeters": 2.5307746084994562, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.8000000000000274, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1067916124175738, - "yMeters": 2.5161471766759447, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.8250000000000268, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1270656904052041, - "yMeters": 2.5015197448524331, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.8500000000000263, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1473397683928344, - "yMeters": 2.4868923130289216, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.8750000000000258, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1676138463804646, - "yMeters": 2.47226488120541, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.9000000000000252, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1878879243680949, - "yMeters": 2.4576374493818984, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.9250000000000247, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2081620023557251, - "yMeters": 2.4430100175583869, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.9500000000000242, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2284360803433554, - "yMeters": 2.4283825857348753, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 6.9750000000000236, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2487101583309856, - "yMeters": 2.4137551539113637, - "headingRadians": -0.62499999999999878, - "unwrappedHeadingRadians": -0.62499999999999878, - "arcLengthMeters": 7.0000000000000231, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2690600535261005, - "yMeters": 2.3992335798174533, - "headingRadians": -0.61458333333333215, - "unwrappedHeadingRadians": -0.61458333333333215, - "arcLengthMeters": 7.0250000000000234, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.289560108339975, - "yMeters": 2.38492476780369, - "headingRadians": -0.60416666666666552, - "unwrappedHeadingRadians": -0.60416666666666552, - "arcLengthMeters": 7.0500000000000238, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3102080983944147, - "yMeters": 2.370830270461505, - "headingRadians": -0.59374999999999889, - "unwrappedHeadingRadians": -0.59374999999999889, - "arcLengthMeters": 7.0750000000000242, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3310017832593672, - "yMeters": 2.35695161712791, - "headingRadians": -0.58333333333333226, - "unwrappedHeadingRadians": -0.58333333333333226, - "arcLengthMeters": 7.1000000000000245, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3519389066960272, - "yMeters": 2.3432903137195544, - "headingRadians": -0.57291666666666563, - "unwrappedHeadingRadians": -0.57291666666666563, - "arcLengthMeters": 7.1250000000000249, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.373017196901646, - "yMeters": 2.3298478425693241, - "headingRadians": -0.562499999999999, - "unwrappedHeadingRadians": -0.562499999999999, - "arcLengthMeters": 7.1500000000000252, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3942343667560415, - "yMeters": 2.3166256622655004, - "headingRadians": -0.55208333333333237, - "unwrappedHeadingRadians": -0.55208333333333237, - "arcLengthMeters": 7.1750000000000256, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4155881140697586, - "yMeters": 2.3036252074934942, - "headingRadians": -0.54166666666666574, - "unwrappedHeadingRadians": -0.54166666666666574, - "arcLengthMeters": 7.2000000000000259, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4370761218338766, - "yMeters": 2.2908478888801742, - "headingRadians": -0.53124999999999911, - "unwrappedHeadingRadians": -0.53124999999999911, - "arcLengthMeters": 7.2250000000000263, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4586960584714124, - "yMeters": 2.2782950928408043, - "headingRadians": -0.52083333333333248, - "unwrappedHeadingRadians": -0.52083333333333248, - "arcLengthMeters": 7.2500000000000266, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4804455780903147, - "yMeters": 2.2659681814286117, - "headingRadians": -0.51041666666666585, - "unwrappedHeadingRadians": -0.51041666666666585, - "arcLengthMeters": 7.275000000000027, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5023223207380063, - "yMeters": 2.2538684921869931, - "headingRadians": -0.49999999999999917, - "unwrappedHeadingRadians": -0.49999999999999917, - "arcLengthMeters": 7.3000000000000274, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5243239126574526, - "yMeters": 2.2419973380043854, - "headingRadians": -0.48958333333333248, - "unwrappedHeadingRadians": -0.48958333333333248, - "arcLengthMeters": 7.3250000000000277, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5464479665447275, - "yMeters": 2.2303560069718089, - "headingRadians": -0.4791666666666658, - "unwrappedHeadingRadians": -0.4791666666666658, - "arcLengthMeters": 7.3500000000000281, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5686920818080523, - "yMeters": 2.2189457622431012, - "headingRadians": -0.46874999999999911, - "unwrappedHeadingRadians": -0.46874999999999911, - "arcLengthMeters": 7.3750000000000284, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5910538448282727, - "yMeters": 2.2077678418978581, - "headingRadians": -0.45833333333333243, - "unwrappedHeadingRadians": -0.45833333333333243, - "arcLengthMeters": 7.4000000000000288, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6135308292207506, - "yMeters": 2.1968234588070943, - "headingRadians": -0.44791666666666574, - "unwrappedHeadingRadians": -0.44791666666666574, - "arcLengthMeters": 7.4250000000000291, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6361205960986425, - "yMeters": 2.1861138005016403, - "headingRadians": -0.43749999999999906, - "unwrappedHeadingRadians": -0.43749999999999906, - "arcLengthMeters": 7.4500000000000295, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6588206943375328, - "yMeters": 2.1756400290432865, - "headingRadians": -0.42708333333333237, - "unwrappedHeadingRadians": -0.42708333333333237, - "arcLengthMeters": 7.47500000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6816286608413948, - "yMeters": 2.1654032808986949, - "headingRadians": -0.41666666666666574, - "unwrappedHeadingRadians": -0.41666666666666574, - "arcLengthMeters": 7.50000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7045934744240521, - "yMeters": 2.1555242763017004, - "headingRadians": -0.39583333333333243, - "unwrappedHeadingRadians": -0.39583333333333243, - "arcLengthMeters": 7.5250000000000306, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.727759102211488, - "yMeters": 2.1461258145165143, - "headingRadians": -0.37499999999999911, - "unwrappedHeadingRadians": -0.37499999999999911, - "arcLengthMeters": 7.5500000000000309, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.75111549004141, - "yMeters": 2.1372119745890812, - "headingRadians": -0.3541666666666658, - "unwrappedHeadingRadians": -0.3541666666666658, - "arcLengthMeters": 7.5750000000000313, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7746525009593626, - "yMeters": 2.1287866252336061, - "headingRadians": -0.33333333333333248, - "unwrappedHeadingRadians": -0.33333333333333248, - "arcLengthMeters": 7.6000000000000316, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7983599196182878, - "yMeters": 2.1208534231534846, - "headingRadians": -0.31249999999999917, - "unwrappedHeadingRadians": -0.31249999999999917, - "arcLengthMeters": 7.625000000000032, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8222274567121106, - "yMeters": 2.11341581145425, - "headingRadians": -0.29166666666666585, - "unwrappedHeadingRadians": -0.29166666666666585, - "arcLengthMeters": 7.6500000000000323, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8462447534414208, - "yMeters": 2.1064770181492234, - "headingRadians": -0.27083333333333254, - "unwrappedHeadingRadians": -0.27083333333333254, - "arcLengthMeters": 7.6750000000000327, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8704013860093172, - "yMeters": 2.1000400547585176, - "headingRadians": -0.2499999999999992, - "unwrappedHeadingRadians": -0.2499999999999992, - "arcLengthMeters": 7.700000000000033, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8946868701454607, - "yMeters": 2.0941077150020004, - "headingRadians": -0.22916666666666585, - "unwrappedHeadingRadians": -0.22916666666666585, - "arcLengthMeters": 7.7250000000000334, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.919090665656376, - "yMeters": 2.0886825735867869, - "headingRadians": -0.20833333333333251, - "unwrappedHeadingRadians": -0.20833333333333251, - "arcLengthMeters": 7.7500000000000338, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.9436021810000215, - "yMeters": 2.0837669850897851, - "headingRadians": -0.18749999999999917, - "unwrappedHeadingRadians": -0.18749999999999917, - "arcLengthMeters": 7.7750000000000341, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.9682107778826472, - "yMeters": 2.0793630829357812, - "headingRadians": -0.16666666666666582, - "unwrappedHeadingRadians": -0.16666666666666582, - "arcLengthMeters": 7.8000000000000345, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.9929057758759425, - "yMeters": 2.0754727784715081, - "headingRadians": -0.14583333333333248, - "unwrappedHeadingRadians": -0.14583333333333248, - "arcLengthMeters": 7.8250000000000348, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 5.0176764570524721, - "yMeters": 2.0720977601360966, - "headingRadians": -0.12499999999999914, - "unwrappedHeadingRadians": -0.12499999999999914, - "arcLengthMeters": 7.8500000000000352, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 5.0425120706373825, - "yMeters": 2.0692394927282733, - "headingRadians": -0.1041666666666658, - "unwrappedHeadingRadians": -0.1041666666666658, - "arcLengthMeters": 7.8750000000000355, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 5.0674018376743728, - "yMeters": 2.0668992167706204, - "headingRadians": -0.083333333333332454, - "unwrappedHeadingRadians": -0.083333333333332454, - "arcLengthMeters": 7.9000000000000359, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 40, - "startsAtGearSwitch": false, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 1, - "direction": 1, - "startIndex": 41, - "endIndex": 101, - "startsAtGearSwitch": true, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 2, - "direction": 0, - "startIndex": 102, - "endIndex": 318, - "startsAtGearSwitch": true, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "multi-obstacle-detour", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:d062cfcf4fec6c0119575bb0c139697d8e4d5164e958cf511c0200d0f8d9fb5d", - "map": { - "xMinMm": 0.0, - "xMaxMm": 6000.0, - "yMinMm": 0.0, - "yMaxMm": 4000.0, - "resolutionMm": 50.0, - "obstacles": [ - { - "kind": "circle", - "xMinMm": 0.0, - "xMaxMm": 0.0, - "yMinMm": 0.0, - "yMaxMm": 0.0, - "centerXMm": 2400.0, - "centerYMm": 1300.0, - "radiusMm": 220.0 - }, - { - "kind": "axis-aligned-rectangle", - "xMinMm": 3000.0, - "xMaxMm": 3600.0, - "yMinMm": 2000.0, - "yMaxMm": 2600.0, - "centerXMm": 0.0, - "centerYMm": 0.0, - "radiusMm": 0.0 - }, - { - "kind": "circle", - "xMinMm": 0.0, - "xMaxMm": 0.0, - "yMinMm": 0.0, - "yMaxMm": 0.0, - "centerXMm": 3720.0, - "centerYMm": 2320.0, - "radiusMm": 140.0 - }, - { - "kind": "circle", - "xMinMm": 0.0, - "xMaxMm": 0.0, - "yMinMm": 0.0, - "yMaxMm": 0.0, - "centerXMm": 3720.0, - "centerYMm": 2680.0, - "radiusMm": 140.0 - } - ] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 1.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.0249995478901843, - "yMeters": 1.0001302071559617, - "headingRadians": 0.010416666666666666, - "unwrappedHeadingRadians": 0.010416666666666666, - "arcLengthMeters": 0.025, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499963831803427, - "yMeters": 1.0005208144955946, - "headingRadians": 0.020833333333333332, - "unwrappedHeadingRadians": 0.020833333333333332, - "arcLengthMeters": 0.05, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749877935647825, - "yMeters": 1.0011717796356725, - "headingRadians": 0.03125, - "unwrappedHeadingRadians": 0.03125, - "arcLengthMeters": 0.075000000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.47723559808167104, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999710673264458, - "yMeters": 1.0020830319425964, - "headingRadians": 0.041666666666666664, - "unwrappedHeadingRadians": 0.041666666666666664, - "arcLengthMeters": 0.1, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.47723559808167104, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249434936311464, - "yMeters": 1.0032544725400563, - "headingRadians": 0.052083333333333329, - "unwrappedHeadingRadians": 0.052083333333333329, - "arcLengthMeters": 0.125, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.42807952565337382, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499023628217124, - "yMeters": 1.004685974319762, - "headingRadians": 0.062499999999999993, - "unwrappedHeadingRadians": 0.062499999999999993, - "arcLengthMeters": 0.15, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.42807952565337382, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1748449667119993, - "yMeters": 1.0063773819552337, - "headingRadians": 0.072916666666666657, - "unwrappedHeadingRadians": 0.072916666666666657, - "arcLengthMeters": 0.175, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.37900551205033328, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1997685988807447, - "yMeters": 1.0083285119186571, - "headingRadians": 0.083333333333333329, - "unwrappedHeadingRadians": 0.083333333333333329, - "arcLengthMeters": 0.2, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.37900551205033328, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.224670554965231, - "yMeters": 1.0105391525007958, - "headingRadians": 0.09375, - "unwrappedHeadingRadians": 0.09375, - "arcLengthMeters": 0.22500000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.33002600127915627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2495481329547253, - "yMeters": 1.0130090638339635, - "headingRadians": 0.10416666666666667, - "unwrappedHeadingRadians": 0.10416666666666667, - "arcLengthMeters": 0.25000000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.33002600127915627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2743986334836632, - "yMeters": 1.0157379779180522, - "headingRadians": 0.11458333333333334, - "unwrappedHeadingRadians": 0.11458333333333334, - "arcLengthMeters": 0.27500000000000008, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.281156055061065, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2992193601245465, - "yMeters": 1.0187255986496104, - "headingRadians": 0.125, - "unwrappedHeadingRadians": 0.125, - "arcLengthMeters": 0.30000000000000004, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.281156055061065, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3240076196805208, - "yMeters": 1.0219716018539722, - "headingRadians": 0.13541666666666666, - "unwrappedHeadingRadians": 0.13541666666666666, - "arcLengthMeters": 0.325, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.23241406916042529, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3487607224776048, - "yMeters": 1.0254756353204335, - "headingRadians": 0.14583333333333331, - "unwrappedHeadingRadians": 0.14583333333333331, - "arcLengthMeters": 0.35, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.23241406916042529, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3734759826565346, - "yMeters": 1.0292373188404673, - "headingRadians": 0.15624999999999997, - "unwrappedHeadingRadians": 0.15624999999999997, - "arcLengthMeters": 0.37499999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.18382273445530839, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3981507184641961, - "yMeters": 1.0332562442489797, - "headingRadians": 0.16666666666666663, - "unwrappedHeadingRadians": 0.16666666666666663, - "arcLengthMeters": 0.39999999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.18382273445530839, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4227822525446114, - "yMeters": 1.0375319754685981, - "headingRadians": 0.17708333333333329, - "unwrappedHeadingRadians": 0.17708333333333329, - "arcLengthMeters": 0.42499999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4473679122294476, - "yMeters": 1.0420640485569872, - "headingRadians": 0.18749999999999994, - "unwrappedHeadingRadians": 0.18749999999999994, - "arcLengthMeters": 0.44999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4719050298280172, - "yMeters": 1.0468519717571914, - "headingRadians": 0.1979166666666666, - "unwrappedHeadingRadians": 0.1979166666666666, - "arcLengthMeters": 0.47499999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.087212598259828078, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4963909429167384, - "yMeters": 1.051895225550991, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.49999999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.075092662495593987, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4963909429167384, - "yMeters": 1.051895225550991, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.49999999999999967, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.075092662495593987, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 1.4719315181828945, - "yMeters": 1.0467244865622749, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.52499999999999969, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.075092662495593987, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4474720934490506, - "yMeters": 1.0415537475735588, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.54999999999999971, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.087212598259828078, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4230126687152067, - "yMeters": 1.0363830085848427, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.57499999999999973, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3985532439813628, - "yMeters": 1.0312122695961266, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.59999999999999976, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3740938192475189, - "yMeters": 1.0260415306074104, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.62499999999999978, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.18382273445530839, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3496343945136751, - "yMeters": 1.0208707916186943, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.6499999999999998, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.18382273445530839, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3251749697798312, - "yMeters": 1.0157000526299782, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.67499999999999982, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.23241406916042529, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3007155450459873, - "yMeters": 1.0105293136412621, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.69999999999999984, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.23241406916042529, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2762561203121434, - "yMeters": 1.005358574652546, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.72499999999999987, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.23241406916042529, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2517966955782995, - "yMeters": 1.0001878356638298, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.74999999999999989, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.281156055061065, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2273372708444557, - "yMeters": 0.99501709667511384, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.77499999999999991, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.281156055061065, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2028778461106118, - "yMeters": 0.98984635768639784, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.79999999999999993, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.34154574455120124, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1784184213767679, - "yMeters": 0.98467561869768183, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.825, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.34154574455120124, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.153958996642924, - "yMeters": 0.97950487970896583, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.85, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.3899780157361914, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1294995719090801, - "yMeters": 0.97433414072024982, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.875, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.3899780157361914, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1050401471752362, - "yMeters": 0.96916340173153381, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.9, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.43855326657792137, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0805807224413924, - "yMeters": 0.96399266274281781, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.925, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.43855326657792137, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0561212977075485, - "yMeters": 0.9588219237541018, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.95000000000000007, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.48725302659962388, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0316618729737046, - "yMeters": 0.9536511847653858, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.97500000000000009, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.48725302659962388, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0072024482398609, - "yMeters": 0.94848044577666979, - "headingRadians": 0.2083333333333332, - "unwrappedHeadingRadians": 0.2083333333333332, - "arcLengthMeters": 0.99999999999999989, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.53606183892575821, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.98271653515113966, - "yMeters": 0.94343719198287013, - "headingRadians": 0.19791666666666655, - "unwrappedHeadingRadians": 0.19791666666666655, - "arcLengthMeters": 1.025, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.5476880417322475, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.95817941755257008, - "yMeters": 0.93864926878266608, - "headingRadians": 0.18749999999999989, - "unwrappedHeadingRadians": 0.18749999999999989, - "arcLengthMeters": 1.0499999999999998, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.5961332970170744, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.93359375786773391, - "yMeters": 0.934117195694277, - "headingRadians": 0.17708333333333323, - "unwrappedHeadingRadians": 0.17708333333333323, - "arcLengthMeters": 1.0749999999999997, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.5961332970170744, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.9089622237873185, - "yMeters": 0.92984146447465865, - "headingRadians": 0.16666666666666657, - "unwrappedHeadingRadians": 0.16666666666666657, - "arcLengthMeters": 1.0999999999999996, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.64469762255002039, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.884287487979657, - "yMeters": 0.925822539066146, - "headingRadians": 0.15624999999999992, - "unwrappedHeadingRadians": 0.15624999999999992, - "arcLengthMeters": 1.1249999999999996, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.64469762255002039, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.85957222780072728, - "yMeters": 0.92206085554611239, - "headingRadians": 0.14583333333333326, - "unwrappedHeadingRadians": 0.14583333333333326, - "arcLengthMeters": 1.1499999999999995, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.69336801574440965, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.83481912500364341, - "yMeters": 0.91855682207965106, - "headingRadians": 0.1354166666666666, - "unwrappedHeadingRadians": 0.1354166666666666, - "arcLengthMeters": 1.1749999999999994, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.69336801574440965, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.810030865447669, - "yMeters": 0.915310818875289, - "headingRadians": 0.12499999999999993, - "unwrappedHeadingRadians": 0.12499999999999993, - "arcLengthMeters": 1.1999999999999993, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.74213327792570938, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.78521013880678581, - "yMeters": 0.91232319814373108, - "headingRadians": 0.11458333333333326, - "unwrappedHeadingRadians": 0.11458333333333326, - "arcLengthMeters": 1.2249999999999992, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.74213327792570938, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.7603596382778478, - "yMeters": 0.90959428405964238, - "headingRadians": 0.10416666666666659, - "unwrappedHeadingRadians": 0.10416666666666659, - "arcLengthMeters": 1.2499999999999991, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.79098371565941161, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.73548206028835339, - "yMeters": 0.90712437272647473, - "headingRadians": 0.093749999999999917, - "unwrappedHeadingRadians": 0.093749999999999917, - "arcLengthMeters": 1.274999999999999, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.79098371565941161, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.71058010420386708, - "yMeters": 0.90491373214433612, - "headingRadians": 0.083333333333333245, - "unwrappedHeadingRadians": 0.083333333333333245, - "arcLengthMeters": 1.2999999999999989, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.83991089882786107, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.6856564720351217, - "yMeters": 0.90296260218091273, - "headingRadians": 0.072916666666666574, - "unwrappedHeadingRadians": 0.072916666666666574, - "arcLengthMeters": 1.3249999999999988, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.83991089882786107, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.6607138681448349, - "yMeters": 0.90127119454544091, - "headingRadians": 0.06249999999999991, - "unwrappedHeadingRadians": 0.06249999999999991, - "arcLengthMeters": 1.3499999999999988, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.8889074634096118, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.6357549989542689, - "yMeters": 0.89983969276573517, - "headingRadians": 0.052083333333333245, - "unwrappedHeadingRadians": 0.052083333333333245, - "arcLengthMeters": 1.3749999999999987, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.8889074634096118, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.61078257264956826, - "yMeters": 0.89866825216827528, - "headingRadians": 0.041666666666666581, - "unwrappedHeadingRadians": 0.041666666666666581, - "arcLengthMeters": 1.3999999999999986, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.94822646753884665, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.585799298887905, - "yMeters": 0.89775699986135138, - "headingRadians": 0.031249999999999917, - "unwrappedHeadingRadians": 0.031249999999999917, - "arcLengthMeters": 1.4249999999999985, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.94822646753884665, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.56080788850346519, - "yMeters": 0.89710603472127337, - "headingRadians": 0.020833333333333252, - "unwrappedHeadingRadians": 0.020833333333333252, - "arcLengthMeters": 1.4499999999999984, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.99703554474769607, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.5358110532133068, - "yMeters": 0.89671542738164045, - "headingRadians": 0.010416666666666586, - "unwrappedHeadingRadians": 0.010416666666666586, - "arcLengthMeters": 1.4749999999999983, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.99703554474769607, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.51081150532312258, - "yMeters": 0.89658522022567866, - "headingRadians": -1.9081958235744878E-17, - "unwrappedHeadingRadians": -1.9081958235744878E-17, - "arcLengthMeters": 1.4999999999999982, - "direction": 1, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 1.0459143886750764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.51081150532312258, - "yMeters": 0.89658522022567866, - "headingRadians": -1.9081958235744878E-17, - "unwrappedHeadingRadians": -1.9081958235744878E-17, - "arcLengthMeters": 1.4999999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 1.0459143886750764, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 0.53580969691329394, - "yMeters": 0.896845627473476, - "headingRadians": 0.020833333333333315, - "unwrappedHeadingRadians": 0.020833333333333315, - "arcLengthMeters": 1.5249999999999981, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 1.0459143886750764, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.56079703898634548, - "yMeters": 0.89762673619697686, - "headingRadians": 0.04166666666666665, - "unwrappedHeadingRadians": 0.04166666666666665, - "arcLengthMeters": 1.549999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.99703554474769607, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.5857626867339788, - "yMeters": 0.89892820738555967, - "headingRadians": 0.062499999999999986, - "unwrappedHeadingRadians": 0.062499999999999986, - "arcLengthMeters": 1.574999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.99703554474769607, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.61069580476349483, - "yMeters": 0.90074947618500723, - "headingRadians": 0.083333333333333315, - "unwrappedHeadingRadians": 0.083333333333333315, - "arcLengthMeters": 1.5999999999999979, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.93796694971647054, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.63558557180048514, - "yMeters": 0.90308975214266041, - "headingRadians": 0.10416666666666666, - "unwrappedHeadingRadians": 0.10416666666666666, - "arcLengthMeters": 1.6249999999999978, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.93796694971647054, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.66042118538539574, - "yMeters": 0.90594801955048387, - "headingRadians": 0.125, - "unwrappedHeadingRadians": 0.125, - "arcLengthMeters": 1.6499999999999977, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.8889074634096118, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.68519186656192488, - "yMeters": 0.90932303788589541, - "headingRadians": 0.14583333333333334, - "unwrappedHeadingRadians": 0.14583333333333334, - "arcLengthMeters": 1.6749999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.8889074634096118, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.70988686455522054, - "yMeters": 0.9132133423501686, - "headingRadians": 0.16666666666666669, - "unwrappedHeadingRadians": 0.16666666666666669, - "arcLengthMeters": 1.6999999999999975, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.83991089882786107, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.73449546143784639, - "yMeters": 0.91761724450417226, - "headingRadians": 0.18750000000000003, - "unwrappedHeadingRadians": 0.18750000000000003, - "arcLengthMeters": 1.7249999999999974, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.83991089882786107, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.75900697678149176, - "yMeters": 0.92253283300117417, - "headingRadians": 0.20833333333333337, - "unwrappedHeadingRadians": 0.20833333333333337, - "arcLengthMeters": 1.7499999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.79098371565941161, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.78341077229240685, - "yMeters": 0.92795797441638772, - "headingRadians": 0.22916666666666671, - "unwrappedHeadingRadians": 0.22916666666666671, - "arcLengthMeters": 1.7749999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.79098371565941161, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.80769625642855014, - "yMeters": 0.933890314172905, - "headingRadians": 0.25000000000000006, - "unwrappedHeadingRadians": 0.25000000000000006, - "arcLengthMeters": 1.7999999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.74213327792570938, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.831852888996446, - "yMeters": 0.94032727756361079, - "headingRadians": 0.27083333333333337, - "unwrappedHeadingRadians": 0.27083333333333337, - "arcLengthMeters": 1.8249999999999971, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.74213327792570938, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.85587018572575635, - "yMeters": 0.94726607086863746, - "headingRadians": 0.29166666666666669, - "unwrappedHeadingRadians": 0.29166666666666669, - "arcLengthMeters": 1.849999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.69336801574440965, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.87973772281957963, - "yMeters": 0.954703682567872, - "headingRadians": 0.3125, - "unwrappedHeadingRadians": 0.3125, - "arcLengthMeters": 1.8749999999999969, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.6830218388191488, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.9034451414785053, - "yMeters": 0.96263688464799346, - "headingRadians": 0.33333333333333331, - "unwrappedHeadingRadians": 0.33333333333333331, - "arcLengthMeters": 1.8999999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.63395648772997248, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.92698215239645787, - "yMeters": 0.97106223400346858, - "headingRadians": 0.35416666666666663, - "unwrappedHeadingRadians": 0.35416666666666663, - "arcLengthMeters": 1.9249999999999967, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.63395648772997248, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.95033854022637976, - "yMeters": 0.97997607393090158, - "headingRadians": 0.37499999999999994, - "unwrappedHeadingRadians": 0.37499999999999994, - "arcLengthMeters": 1.9499999999999966, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.58496667654490253, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.97350416801381545, - "yMeters": 0.9893745357160878, - "headingRadians": 0.39583333333333326, - "unwrappedHeadingRadians": 0.39583333333333326, - "arcLengthMeters": 1.9749999999999965, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.58496667654490253, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 0.99646898159647213, - "yMeters": 0.99925354031308233, - "headingRadians": 0.41666666666666646, - "unwrappedHeadingRadians": 0.41666666666666646, - "arcLengthMeters": 1.9999999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.58496667654490253, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.019223013967848, - "yMeters": 1.0096088001145549, - "headingRadians": 0.43749999999999978, - "unwrappedHeadingRadians": 0.43749999999999978, - "arcLengthMeters": 2.0249999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0417563896030329, - "yMeters": 1.0204358208126638, - "headingRadians": 0.45833333333333309, - "unwrappedHeadingRadians": 0.45833333333333309, - "arcLengthMeters": 2.0499999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.52646336232465141, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0640593287448055, - "yMeters": 1.0317299033496392, - "headingRadians": 0.47916666666666641, - "unwrappedHeadingRadians": 0.47916666666666641, - "arcLengthMeters": 2.0749999999999962, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.47723559808167104, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0861221516481661, - "yMeters": 1.0434861459572313, - "headingRadians": 0.49999999999999972, - "unwrappedHeadingRadians": 0.49999999999999972, - "arcLengthMeters": 2.0999999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.47723559808167104, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1079352827814632, - "yMeters": 1.0556994462841371, - "headingRadians": 0.520833333333333, - "unwrappedHeadingRadians": 0.520833333333333, - "arcLengthMeters": 2.124999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.41986178111159767, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1294892549822901, - "yMeters": 1.0683645036104821, - "headingRadians": 0.54166666666666641, - "unwrappedHeadingRadians": 0.54166666666666641, - "arcLengthMeters": 2.1499999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.41986178111159767, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1507747135663466, - "yMeters": 1.081475821148397, - "headingRadians": 0.56249999999999978, - "unwrappedHeadingRadians": 0.56249999999999978, - "arcLengthMeters": 2.1749999999999958, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.37038903013961044, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.171782420387486, - "yMeters": 1.0950277084276903, - "headingRadians": 0.58333333333333315, - "unwrappedHeadingRadians": 0.58333333333333315, - "arcLengthMeters": 2.1999999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.37038903013961044, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1925032578471824, - "yMeters": 1.1090142837655803, - "headingRadians": 0.60416666666666652, - "unwrappedHeadingRadians": 0.60416666666666652, - "arcLengthMeters": 2.2249999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.3641891714438652, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2129282328516771, - "yMeters": 1.1234294768194173, - "headingRadians": 0.62499999999999989, - "unwrappedHeadingRadians": 0.62499999999999989, - "arcLengthMeters": 2.2499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.31445026805891629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2330484807150914, - "yMeters": 1.1382670312212853, - "headingRadians": 0.64583333333333326, - "unwrappedHeadingRadians": 0.64583333333333326, - "arcLengthMeters": 2.2749999999999955, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.31445026805891629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2528552690068069, - "yMeters": 1.1535205072933412, - "headingRadians": 0.66666666666666663, - "unwrappedHeadingRadians": 0.66666666666666663, - "arcLengthMeters": 2.2999999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.26058942819777364, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2723400013414456, - "yMeters": 1.1691832848427131, - "headingRadians": 0.6875, - "unwrappedHeadingRadians": 0.6875, - "arcLengthMeters": 2.3249999999999953, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.26058942819777364, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2914942211098055, - "yMeters": 1.1852485660347432, - "headingRadians": 0.70833333333333337, - "unwrappedHeadingRadians": 0.70833333333333337, - "arcLengthMeters": 2.3499999999999952, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.26058942819777364, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3103096151491305, - "yMeters": 1.201709378343331, - "headingRadians": 0.72916666666666674, - "unwrappedHeadingRadians": 0.72916666666666674, - "arcLengthMeters": 2.3749999999999951, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3287780173511237, - "yMeters": 1.218558577577094, - "headingRadians": 0.75000000000000011, - "unwrappedHeadingRadians": 0.75000000000000011, - "arcLengthMeters": 2.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3468914122061377, - "yMeters": 1.2357888509800314, - "headingRadians": 0.77083333333333348, - "unwrappedHeadingRadians": 0.77083333333333348, - "arcLengthMeters": 2.4249999999999949, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3646419382820014, - "yMeters": 1.253392720405351, - "headingRadians": 0.79166666666666685, - "unwrappedHeadingRadians": 0.79166666666666685, - "arcLengthMeters": 2.4499999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3820218916359779, - "yMeters": 1.2713625455610733, - "headingRadians": 0.81250000000000022, - "unwrappedHeadingRadians": 0.81250000000000022, - "arcLengthMeters": 2.4749999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3990237291583674, - "yMeters": 1.2896905273260111, - "headingRadians": 0.83333333333333348, - "unwrappedHeadingRadians": 0.83333333333333348, - "arcLengthMeters": 2.4999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.83333333333333337, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4157373549333043, - "yMeters": 1.3082821669006273, - "headingRadians": 0.84375000000000011, - "unwrappedHeadingRadians": 0.84375000000000011, - "arcLengthMeters": 2.5249999999999946, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4322564145342496, - "yMeters": 1.3270468949433543, - "headingRadians": 0.85416666666666674, - "unwrappedHeadingRadians": 0.85416666666666674, - "arcLengthMeters": 2.5499999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4485791155447281, - "yMeters": 1.3459826753692992, - "headingRadians": 0.86458333333333337, - "unwrappedHeadingRadians": 0.86458333333333337, - "arcLengthMeters": 2.5749999999999944, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.464703686854343, - "yMeters": 1.3650874535333666, - "headingRadians": 0.875, - "unwrappedHeadingRadians": 0.875, - "arcLengthMeters": 2.5999999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4806283788509516, - "yMeters": 1.3843591564531978, - "headingRadians": 0.88541666666666663, - "unwrappedHeadingRadians": 0.88541666666666663, - "arcLengthMeters": 2.6249999999999942, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4963514636105082, - "yMeters": 1.4037956930341031, - "headingRadians": 0.89583333333333326, - "unwrappedHeadingRadians": 0.89583333333333326, - "arcLengthMeters": 2.6499999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5118712350845553, - "yMeters": 1.4233949542959579, - "headingRadians": 0.90624999999999989, - "unwrappedHeadingRadians": 0.90624999999999989, - "arcLengthMeters": 2.674999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.011121849852041232, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5271860092853389, - "yMeters": 1.4431548136020385, - "headingRadians": 0.91666666666666652, - "unwrappedHeadingRadians": 0.91666666666666652, - "arcLengthMeters": 2.699999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.011121849852041232, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5422941244685313, - "yMeters": 1.4630731268897763, - "headingRadians": 0.92708333333333315, - "unwrappedHeadingRadians": 0.92708333333333315, - "arcLengthMeters": 2.7249999999999939, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.011121849852041232, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5571939413135409, - "yMeters": 1.4831477329034006, - "headingRadians": 0.93749999999999978, - "unwrappedHeadingRadians": 0.93749999999999978, - "arcLengthMeters": 2.7499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0092016093317762726, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5718838431013882, - "yMeters": 1.5033764534284484, - "headingRadians": 0.94791666666666641, - "unwrappedHeadingRadians": 0.94791666666666641, - "arcLengthMeters": 2.7749999999999937, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0092016093317762726, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5863622358901286, - "yMeters": 1.5237570935281122, - "headingRadians": 0.958333333333333, - "unwrappedHeadingRadians": 0.958333333333333, - "arcLengthMeters": 2.7999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.011121849852041232, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6006275486878057, - "yMeters": 1.5442874417814059, - "headingRadians": 0.96874999999999967, - "unwrappedHeadingRadians": 0.96874999999999967, - "arcLengthMeters": 2.8249999999999935, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6146782336229126, - "yMeters": 1.5649652705231147, - "headingRadians": 0.9791666666666663, - "unwrappedHeadingRadians": 0.9791666666666663, - "arcLengthMeters": 2.8499999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6285127661123453, - "yMeters": 1.5857883360855125, - "headingRadians": 0.98958333333333293, - "unwrappedHeadingRadians": 0.98958333333333293, - "arcLengthMeters": 2.8749999999999933, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.642129645026829, - "yMeters": 1.6067543790418111, - "headingRadians": 0.99999999999999956, - "unwrappedHeadingRadians": 0.99999999999999956, - "arcLengthMeters": 2.8999999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6555273928538004, - "yMeters": 1.6278611244513235, - "headingRadians": 1.0104166666666663, - "unwrappedHeadingRadians": 1.0104166666666663, - "arcLengthMeters": 2.9249999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6687045558577251, - "yMeters": 1.6491062821063067, - "headingRadians": 1.020833333333333, - "unwrappedHeadingRadians": 1.020833333333333, - "arcLengthMeters": 2.949999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6816597042378378, - "yMeters": 1.6704875467804639, - "headingRadians": 1.0312499999999998, - "unwrappedHeadingRadians": 1.0312499999999998, - "arcLengthMeters": 2.9749999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6943914322832838, - "yMeters": 1.6920025984790739, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 2.9999999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7070109876441826, - "yMeters": 1.7135837667165259, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.0249999999999937, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7196305430050813, - "yMeters": 1.7351649349539779, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.0499999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.73225009836598, - "yMeters": 1.75674610319143, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.0749999999999935, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7448696537268789, - "yMeters": 1.778327271428882, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.0999999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7574892090877776, - "yMeters": 1.7999084396663341, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.1249999999999933, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7701087644486764, - "yMeters": 1.8214896079037861, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.1499999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7827283198095751, - "yMeters": 1.8430707761412382, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.1749999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7953478751704739, - "yMeters": 1.8646519443786902, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.1999999999999931, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8079674305313727, - "yMeters": 1.8862331126161422, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.224999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8205869858922714, - "yMeters": 1.9078142808535943, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.2499999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8332065412531702, - "yMeters": 1.9293954490910463, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.2749999999999928, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8458260966140689, - "yMeters": 1.9509766173284984, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.2999999999999927, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8584456519749677, - "yMeters": 1.9725577855659504, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.3249999999999926, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8710652073358665, - "yMeters": 1.9941389538034024, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.3499999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8836847626967652, - "yMeters": 2.0157201220408547, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.3749999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.896304318057664, - "yMeters": 2.037301290278307, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.3999999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.011121849852041232, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9089238734185627, - "yMeters": 2.0588824585157592, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.4249999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.011121849852041232, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9215434287794615, - "yMeters": 2.0804636267532115, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.449999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.030022002581713281, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9341629841403603, - "yMeters": 2.1020447949906638, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.4749999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.030022002581713281, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9467825395012588, - "yMeters": 2.1236259632281156, - "headingRadians": 1.0416666666666663, - "unwrappedHeadingRadians": 1.0416666666666663, - "arcLengthMeters": 3.4999999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.075092662495593987, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.959514267546705, - "yMeters": 2.1451410149267263, - "headingRadians": 1.0312499999999996, - "unwrappedHeadingRadians": 1.0312499999999996, - "arcLengthMeters": 3.5249999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.053823808804266515, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9724694159268177, - "yMeters": 2.1665222796008834, - "headingRadians": 1.0208333333333328, - "unwrappedHeadingRadians": 1.0208333333333328, - "arcLengthMeters": 3.5499999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.053823808804266515, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9856465789307425, - "yMeters": 2.1877674372558666, - "headingRadians": 1.0104166666666661, - "unwrappedHeadingRadians": 1.0104166666666661, - "arcLengthMeters": 3.5749999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9990443267577138, - "yMeters": 2.208874182665379, - "headingRadians": 0.99999999999999944, - "unwrappedHeadingRadians": 0.99999999999999944, - "arcLengthMeters": 3.5999999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.35920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0126612056721975, - "yMeters": 2.2298402256216776, - "headingRadians": 0.98958333333333282, - "unwrappedHeadingRadians": 0.98958333333333282, - "arcLengthMeters": 3.6249999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.02649573816163, - "yMeters": 2.2506632911840754, - "headingRadians": 0.97916666666666619, - "unwrappedHeadingRadians": 0.97916666666666619, - "arcLengthMeters": 3.6499999999999946, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0405464230967372, - "yMeters": 2.271341119925784, - "headingRadians": 0.96874999999999956, - "unwrappedHeadingRadians": 0.96874999999999956, - "arcLengthMeters": 3.6749999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.30920160933177632, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0548117358944142, - "yMeters": 2.2918714681790773, - "headingRadians": 0.95833333333333293, - "unwrappedHeadingRadians": 0.95833333333333293, - "arcLengthMeters": 3.6999999999999944, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0692901286831549, - "yMeters": 2.3122521082787411, - "headingRadians": 0.9479166666666663, - "unwrappedHeadingRadians": 0.9479166666666663, - "arcLengthMeters": 3.7249999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0839800304710021, - "yMeters": 2.3324808288037886, - "headingRadians": 0.93749999999999967, - "unwrappedHeadingRadians": 0.93749999999999967, - "arcLengthMeters": 3.7499999999999942, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0988798473160117, - "yMeters": 2.3525554348174129, - "headingRadians": 0.927083333333333, - "unwrappedHeadingRadians": 0.927083333333333, - "arcLengthMeters": 3.7749999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.25920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1139879624992042, - "yMeters": 2.3724737481051505, - "headingRadians": 0.91666666666666641, - "unwrappedHeadingRadians": 0.91666666666666641, - "arcLengthMeters": 3.799999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1293027366999877, - "yMeters": 2.3922336074112311, - "headingRadians": 0.90624999999999978, - "unwrappedHeadingRadians": 0.90624999999999978, - "arcLengthMeters": 3.824999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1448225081740349, - "yMeters": 2.4118328686730859, - "headingRadians": 0.89583333333333315, - "unwrappedHeadingRadians": 0.89583333333333315, - "arcLengthMeters": 3.8499999999999939, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1605455929335919, - "yMeters": 2.4312694052539912, - "headingRadians": 0.88541666666666652, - "unwrappedHeadingRadians": 0.88541666666666652, - "arcLengthMeters": 3.8749999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1764702849302004, - "yMeters": 2.4505411081738222, - "headingRadians": 0.87499999999999989, - "unwrappedHeadingRadians": 0.87499999999999989, - "arcLengthMeters": 3.8999999999999937, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1925948562398152, - "yMeters": 2.4696458863378896, - "headingRadians": 0.86458333333333326, - "unwrappedHeadingRadians": 0.86458333333333326, - "arcLengthMeters": 3.9249999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.15920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2089175572502935, - "yMeters": 2.4885816667638347, - "headingRadians": 0.85416666666666663, - "unwrappedHeadingRadians": 0.85416666666666663, - "arcLengthMeters": 3.9499999999999935, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2254366168512387, - "yMeters": 2.5073463948065617, - "headingRadians": 0.84375, - "unwrappedHeadingRadians": 0.84375, - "arcLengthMeters": 3.9749999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2421502426261757, - "yMeters": 2.5259380343811779, - "headingRadians": 0.83333333333333337, - "unwrappedHeadingRadians": 0.83333333333333337, - "arcLengthMeters": 3.9999999999999933, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2590566210470397, - "yMeters": 2.5443545681839219, - "headingRadians": 0.82291666666666674, - "unwrappedHeadingRadians": 0.82291666666666674, - "arcLengthMeters": 4.0249999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2761539176709542, - "yMeters": 2.5625939979110535, - "headingRadians": 0.81250000000000011, - "unwrappedHeadingRadians": 0.81250000000000011, - "arcLengthMeters": 4.0499999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2934402773392795, - "yMeters": 2.5806543444756791, - "headingRadians": 0.80208333333333348, - "unwrappedHeadingRadians": 0.80208333333333348, - "arcLengthMeters": 4.074999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.059201609331776317, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3109138243789071, - "yMeters": 2.5985336482224977, - "headingRadians": 0.79166666666666685, - "unwrappedHeadingRadians": 0.79166666666666685, - "arcLengthMeters": 4.0999999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0092016093317762726, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3285726628057835, - "yMeters": 2.6162299691404329, - "headingRadians": 0.78125000000000022, - "unwrappedHeadingRadians": 0.78125000000000022, - "arcLengthMeters": 4.1249999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0092016093317762726, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3464148765306345, - "yMeters": 2.6337413870731368, - "headingRadians": 0.77083333333333359, - "unwrappedHeadingRadians": 0.77083333333333359, - "arcLengthMeters": 4.149999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0092016093317762726, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3644385295668724, - "yMeters": 2.6510660019273375, - "headingRadians": 0.760416666666667, - "unwrappedHeadingRadians": 0.760416666666667, - "arcLengthMeters": 4.1749999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3826416662406622, - "yMeters": 2.6682019338790117, - "headingRadians": 0.75000000000000033, - "unwrappedHeadingRadians": 0.75000000000000033, - "arcLengthMeters": 4.1999999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4010223114031244, - "yMeters": 2.6851473235773557, - "headingRadians": 0.7395833333333337, - "unwrappedHeadingRadians": 0.7395833333333337, - "arcLengthMeters": 4.2249999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4195784706446486, - "yMeters": 2.7019003323465371, - "headingRadians": 0.72916666666666707, - "unwrappedHeadingRadians": 0.72916666666666707, - "arcLengthMeters": 4.2499999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4383081305113019, - "yMeters": 2.7184591423852007, - "headingRadians": 0.71875000000000044, - "unwrappedHeadingRadians": 0.71875000000000044, - "arcLengthMeters": 4.2749999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.50920160933177627, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4572092587232981, - "yMeters": 2.7348219569637124, - "headingRadians": 0.70833333333333381, - "unwrappedHeadingRadians": 0.70833333333333381, - "arcLengthMeters": 4.2999999999999972, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4762798043955128, - "yMeters": 2.7509870006191144, - "headingRadians": 0.69791666666666718, - "unwrappedHeadingRadians": 0.69791666666666718, - "arcLengthMeters": 4.3249999999999975, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4955176982600173, - "yMeters": 2.7669525193477726, - "headingRadians": 0.68750000000000056, - "unwrappedHeadingRadians": 0.68750000000000056, - "arcLengthMeters": 4.3499999999999979, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.45920160933177623, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5149208528906062, - "yMeters": 2.7827167807956981, - "headingRadians": 0.67708333333333393, - "unwrappedHeadingRadians": 0.67708333333333393, - "arcLengthMeters": 4.3749999999999982, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5344871629292949, - "yMeters": 2.7982780744465163, - "headingRadians": 0.6666666666666673, - "unwrappedHeadingRadians": 0.6666666666666673, - "arcLengthMeters": 4.3999999999999986, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.40920160933177629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5542145053147642, - "yMeters": 2.8136347118070697, - "headingRadians": 0.65625000000000067, - "unwrappedHeadingRadians": 0.65625000000000067, - "arcLengthMeters": 4.4249999999999989, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.36045082905681558, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5741007395127258, - "yMeters": 2.8287850265906282, - "headingRadians": 0.645833333333334, - "unwrappedHeadingRadians": 0.645833333333334, - "arcLengthMeters": 4.4499999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.36045082905681558, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5941437077481808, - "yMeters": 2.8437273748976919, - "headingRadians": 0.63541666666666741, - "unwrappedHeadingRadians": 0.63541666666666741, - "arcLengthMeters": 4.475, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.36045082905681558, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6143412352395541, - "yMeters": 2.8584601353943642, - "headingRadians": 0.62500000000000078, - "unwrappedHeadingRadians": 0.62500000000000078, - "arcLengthMeters": 4.5, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.31445026805891629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6346911304346685, - "yMeters": 2.8729817094882746, - "headingRadians": 0.61458333333333415, - "unwrappedHeadingRadians": 0.61458333333333415, - "arcLengthMeters": 4.525, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.31445026805891629, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.655191185248543, - "yMeters": 2.8872905215020381, - "headingRadians": 0.60416666666666752, - "unwrappedHeadingRadians": 0.60416666666666752, - "arcLengthMeters": 4.5500000000000007, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.264740123145518, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6758391753029822, - "yMeters": 2.901385018844223, - "headingRadians": 0.59375000000000089, - "unwrappedHeadingRadians": 0.59375000000000089, - "arcLengthMeters": 4.5750000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.264740123145518, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6966328601679352, - "yMeters": 2.9152636721778178, - "headingRadians": 0.58333333333333426, - "unwrappedHeadingRadians": 0.58333333333333426, - "arcLengthMeters": 4.6000000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.27161598887650928, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7175699836045948, - "yMeters": 2.9289249755861735, - "headingRadians": 0.57291666666666763, - "unwrappedHeadingRadians": 0.57291666666666763, - "arcLengthMeters": 4.6250000000000018, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.22233543441337966, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.738648273810214, - "yMeters": 2.9423674467364038, - "headingRadians": 0.562500000000001, - "unwrappedHeadingRadians": 0.562500000000001, - "arcLengthMeters": 4.6500000000000021, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.22233543441337966, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7598654436646091, - "yMeters": 2.9555896270402275, - "headingRadians": 0.55208333333333437, - "unwrappedHeadingRadians": 0.55208333333333437, - "arcLengthMeters": 4.6750000000000025, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.18382273445530839, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7812191909783266, - "yMeters": 2.9685900818122337, - "headingRadians": 0.54166666666666774, - "unwrappedHeadingRadians": 0.54166666666666774, - "arcLengthMeters": 4.7000000000000028, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.18382273445530839, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8027071987424441, - "yMeters": 2.9813674004255537, - "headingRadians": 0.53125000000000111, - "unwrappedHeadingRadians": 0.53125000000000111, - "arcLengthMeters": 4.7250000000000032, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.82432713537998, - "yMeters": 2.9939201964649236, - "headingRadians": 0.52083333333333448, - "unwrappedHeadingRadians": 0.52083333333333448, - "arcLengthMeters": 4.7500000000000036, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8460766549988827, - "yMeters": 3.0062471078771162, - "headingRadians": 0.51041666666666785, - "unwrappedHeadingRadians": 0.51041666666666785, - "arcLengthMeters": 4.7750000000000039, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8679533976465748, - "yMeters": 3.0183467971187348, - "headingRadians": 0.50000000000000122, - "unwrappedHeadingRadians": 0.50000000000000122, - "arcLengthMeters": 4.8000000000000043, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10250504669770155, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.889954989566021, - "yMeters": 3.0302179513013425, - "headingRadians": 0.48958333333333454, - "unwrappedHeadingRadians": 0.48958333333333454, - "arcLengthMeters": 4.8250000000000046, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.10250504669770155, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9120790434532959, - "yMeters": 3.0418592823339194, - "headingRadians": 0.47916666666666785, - "unwrappedHeadingRadians": 0.47916666666666785, - "arcLengthMeters": 4.850000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.055621023190982277, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9343231587166207, - "yMeters": 3.0532695270626271, - "headingRadians": 0.46875000000000117, - "unwrappedHeadingRadians": 0.46875000000000117, - "arcLengthMeters": 4.8750000000000053, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.055621023190982277, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9566849217368412, - "yMeters": 3.0644474474078702, - "headingRadians": 0.45833333333333448, - "unwrappedHeadingRadians": 0.45833333333333448, - "arcLengthMeters": 4.9000000000000057, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.030022002581713281, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9791619061293191, - "yMeters": 3.0753918304986341, - "headingRadians": 0.4479166666666678, - "unwrappedHeadingRadians": 0.4479166666666678, - "arcLengthMeters": 4.925000000000006, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.030022002581713281, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0017516730072109, - "yMeters": 3.0861014888040881, - "headingRadians": 0.43750000000000111, - "unwrappedHeadingRadians": 0.43750000000000111, - "arcLengthMeters": 4.9500000000000064, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0244517712461012, - "yMeters": 3.0965752602624419, - "headingRadians": 0.42708333333333443, - "unwrappedHeadingRadians": 0.42708333333333443, - "arcLengthMeters": 4.9750000000000068, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0472597377499628, - "yMeters": 3.1068120084070334, - "headingRadians": 0.4166666666666678, - "unwrappedHeadingRadians": 0.4166666666666678, - "arcLengthMeters": 5.0000000000000071, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0702245513326196, - "yMeters": 3.116691013004028, - "headingRadians": 0.39583333333333448, - "unwrappedHeadingRadians": 0.39583333333333448, - "arcLengthMeters": 5.0250000000000075, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0933901791200555, - "yMeters": 3.1260894747892141, - "headingRadians": 0.37500000000000117, - "unwrappedHeadingRadians": 0.37500000000000117, - "arcLengthMeters": 5.0500000000000078, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1167465669499772, - "yMeters": 3.1350033147166472, - "headingRadians": 0.35416666666666785, - "unwrappedHeadingRadians": 0.35416666666666785, - "arcLengthMeters": 5.0750000000000082, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1402835778679297, - "yMeters": 3.1434286640721223, - "headingRadians": 0.33333333333333454, - "unwrappedHeadingRadians": 0.33333333333333454, - "arcLengthMeters": 5.1000000000000085, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1639909965268553, - "yMeters": 3.1513618661522438, - "headingRadians": 0.31250000000000122, - "unwrappedHeadingRadians": 0.31250000000000122, - "arcLengthMeters": 5.1250000000000089, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1878585336206786, - "yMeters": 3.1587994778514785, - "headingRadians": 0.29166666666666791, - "unwrappedHeadingRadians": 0.29166666666666791, - "arcLengthMeters": 5.1500000000000092, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2118758303499888, - "yMeters": 3.165738271156505, - "headingRadians": 0.27083333333333459, - "unwrappedHeadingRadians": 0.27083333333333459, - "arcLengthMeters": 5.17500000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2360324629178847, - "yMeters": 3.1721752345472107, - "headingRadians": 0.25000000000000128, - "unwrappedHeadingRadians": 0.25000000000000128, - "arcLengthMeters": 5.20000000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2603179470540278, - "yMeters": 3.1781075743037279, - "headingRadians": 0.22916666666666793, - "unwrappedHeadingRadians": 0.22916666666666793, - "arcLengthMeters": 5.22500000000001, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 0.0, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2847217425649426, - "yMeters": 3.1835327157189415, - "headingRadians": 0.20833333333333459, - "unwrappedHeadingRadians": 0.20833333333333459, - "arcLengthMeters": 5.2500000000000107, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3092332579085881, - "yMeters": 3.1884483042159433, - "headingRadians": 0.18750000000000125, - "unwrappedHeadingRadians": 0.18750000000000125, - "arcLengthMeters": 5.275000000000011, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3338418547912139, - "yMeters": 3.1928522063699472, - "headingRadians": 0.16666666666666791, - "unwrappedHeadingRadians": 0.16666666666666791, - "arcLengthMeters": 5.3000000000000114, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3585368527845096, - "yMeters": 3.1967425108342207, - "headingRadians": 0.14583333333333456, - "unwrappedHeadingRadians": 0.14583333333333456, - "arcLengthMeters": 5.3250000000000117, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3833075339610388, - "yMeters": 3.2001175291696322, - "headingRadians": 0.12500000000000122, - "unwrappedHeadingRadians": 0.12500000000000122, - "arcLengthMeters": 5.3500000000000121, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4081431475459492, - "yMeters": 3.2029757965774555, - "headingRadians": 0.10416666666666788, - "unwrappedHeadingRadians": 0.10416666666666788, - "arcLengthMeters": 5.3750000000000124, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4330329145829395, - "yMeters": 3.2053160725351084, - "headingRadians": 0.083333333333334536, - "unwrappedHeadingRadians": 0.083333333333334536, - "arcLengthMeters": 5.4000000000000128, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4579660326124557, - "yMeters": 3.2071373413345561, - "headingRadians": 0.0625000000000012, - "unwrappedHeadingRadians": 0.0625000000000012, - "arcLengthMeters": 5.4250000000000131, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4829316803600889, - "yMeters": 3.2084388125231387, - "headingRadians": 0.041666666666667858, - "unwrappedHeadingRadians": 0.041666666666667858, - "arcLengthMeters": 5.4500000000000135, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5079190224331405, - "yMeters": 3.2092199212466395, - "headingRadians": 0.020833333333334522, - "unwrappedHeadingRadians": 0.020833333333334522, - "arcLengthMeters": 5.4750000000000139, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5329172140233118, - "yMeters": 3.2094803284944367, - "headingRadians": 1.3079815008865125E-15, - "unwrappedHeadingRadians": 1.3079815008865125E-15, - "arcLengthMeters": 5.5000000000000133, - "direction": 0, - "vehicleCurvaturePerMeter": -0.83333333333333337, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5579167619134964, - "yMeters": 3.209350121338475, - "headingRadians": -0.01041666666666536, - "unwrappedHeadingRadians": -0.01041666666666536, - "arcLengthMeters": 5.5250000000000137, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.582913597203655, - "yMeters": 3.2089595139988423, - "headingRadians": -0.020833333333332028, - "unwrappedHeadingRadians": -0.020833333333332028, - "arcLengthMeters": 5.550000000000014, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6079050075880947, - "yMeters": 3.2083085488587644, - "headingRadians": -0.031249999999998695, - "unwrappedHeadingRadians": -0.031249999999998695, - "arcLengthMeters": 5.5750000000000144, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6328882813497581, - "yMeters": 3.2073972965518407, - "headingRadians": -0.041666666666665367, - "unwrappedHeadingRadians": -0.041666666666665367, - "arcLengthMeters": 5.6000000000000147, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6578607076544589, - "yMeters": 3.2062258559543806, - "headingRadians": -0.052083333333332038, - "unwrappedHeadingRadians": -0.052083333333332038, - "arcLengthMeters": 5.6250000000000151, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6828195768450249, - "yMeters": 3.2047943541746751, - "headingRadians": -0.062499999999998709, - "unwrappedHeadingRadians": -0.062499999999998709, - "arcLengthMeters": 5.6500000000000155, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7077621807353118, - "yMeters": 3.2031029465392034, - "headingRadians": -0.072916666666665381, - "unwrappedHeadingRadians": -0.072916666666665381, - "arcLengthMeters": 5.6750000000000158, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.732685812904057, - "yMeters": 3.20115181657578, - "headingRadians": -0.083333333333332052, - "unwrappedHeadingRadians": -0.083333333333332052, - "arcLengthMeters": 5.7000000000000162, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7575877689885435, - "yMeters": 3.1989411759936415, - "headingRadians": -0.093749999999998723, - "unwrappedHeadingRadians": -0.093749999999998723, - "arcLengthMeters": 5.7250000000000165, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.782465346978038, - "yMeters": 3.1964712646604738, - "headingRadians": -0.10416666666666539, - "unwrappedHeadingRadians": -0.10416666666666539, - "arcLengthMeters": 5.7500000000000169, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8073158475069762, - "yMeters": 3.1937423505763851, - "headingRadians": -0.11458333333333207, - "unwrappedHeadingRadians": -0.11458333333333207, - "arcLengthMeters": 5.7750000000000172, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8321365741478592, - "yMeters": 3.1907547298448273, - "headingRadians": -0.12499999999999874, - "unwrappedHeadingRadians": -0.12499999999999874, - "arcLengthMeters": 5.8000000000000176, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8569248337038338, - "yMeters": 3.1875087266404654, - "headingRadians": -0.13541666666666541, - "unwrappedHeadingRadians": -0.13541666666666541, - "arcLengthMeters": 5.8250000000000179, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8816779365009175, - "yMeters": 3.1840046931740043, - "headingRadians": -0.14583333333333207, - "unwrappedHeadingRadians": -0.14583333333333207, - "arcLengthMeters": 5.8500000000000183, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9063931966798471, - "yMeters": 3.1802430096539704, - "headingRadians": -0.15624999999999872, - "unwrappedHeadingRadians": -0.15624999999999872, - "arcLengthMeters": 5.8750000000000187, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9310679324875086, - "yMeters": 3.1762240842454581, - "headingRadians": -0.16666666666666538, - "unwrappedHeadingRadians": -0.16666666666666538, - "arcLengthMeters": 5.900000000000019, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9556994665679239, - "yMeters": 3.1719483530258397, - "headingRadians": -0.17708333333333204, - "unwrappedHeadingRadians": -0.17708333333333204, - "arcLengthMeters": 5.9250000000000194, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.98028512625276, - "yMeters": 3.1674162799374508, - "headingRadians": -0.1874999999999987, - "unwrappedHeadingRadians": -0.1874999999999987, - "arcLengthMeters": 5.95000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0048222438513292, - "yMeters": 3.1626283567372466, - "headingRadians": -0.19791666666666535, - "unwrappedHeadingRadians": -0.19791666666666535, - "arcLengthMeters": 5.97500000000002, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0293081569400506, - "yMeters": 3.1575851029434472, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.0000000000000195, - "direction": 0, - "vehicleCurvaturePerMeter": -0.41666666666666669, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0537675816738945, - "yMeters": 3.1524143639547311, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.02500000000002, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0782270064077384, - "yMeters": 3.147243624966015, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.05000000000002, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1026864311415823, - "yMeters": 3.1420728859772988, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.0750000000000206, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1271458558754262, - "yMeters": 3.1369021469885827, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.100000000000021, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.15160528060927, - "yMeters": 3.1317314079998666, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.1250000000000213, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1760647053431139, - "yMeters": 3.1265606690111505, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.1500000000000217, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2005241300769578, - "yMeters": 3.1213899300224344, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.175000000000022, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2249835548108017, - "yMeters": 3.1162191910337182, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.2000000000000224, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2494429795446456, - "yMeters": 3.1110484520450021, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.2250000000000227, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2739024042784894, - "yMeters": 3.105877713056286, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.2500000000000231, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2983618290123333, - "yMeters": 3.10070697406757, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.2750000000000234, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3228212537461772, - "yMeters": 3.0955362350788538, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.3000000000000238, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3472806784800211, - "yMeters": 3.0903654960901377, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.3250000000000242, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.371740103213865, - "yMeters": 3.0851947571014215, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.3500000000000245, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3961995279477089, - "yMeters": 3.0800240181127054, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.3750000000000249, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4206589526815527, - "yMeters": 3.0748532791239893, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.4000000000000252, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4451183774153966, - "yMeters": 3.0696825401352732, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.4250000000000256, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4695778021492405, - "yMeters": 3.0645118011465571, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.4500000000000259, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4940372268830844, - "yMeters": 3.059341062157841, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.4750000000000263, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5184966516169283, - "yMeters": 3.0541703231691248, - "headingRadians": -0.20833333333333195, - "unwrappedHeadingRadians": -0.20833333333333195, - "arcLengthMeters": 6.5000000000000266, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.54298256470565, - "yMeters": 3.0491270693753254, - "headingRadians": -0.1979166666666653, - "unwrappedHeadingRadians": -0.1979166666666653, - "arcLengthMeters": 6.525000000000027, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5675196823042192, - "yMeters": 3.0443391461751212, - "headingRadians": -0.18749999999999864, - "unwrappedHeadingRadians": -0.18749999999999864, - "arcLengthMeters": 6.5500000000000274, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5921053419890558, - "yMeters": 3.0398070730867324, - "headingRadians": -0.17708333333333198, - "unwrappedHeadingRadians": -0.17708333333333198, - "arcLengthMeters": 6.5750000000000277, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6167368760694716, - "yMeters": 3.0355313418671139, - "headingRadians": -0.16666666666666533, - "unwrappedHeadingRadians": -0.16666666666666533, - "arcLengthMeters": 6.6000000000000281, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6414116118771336, - "yMeters": 3.0315124164586016, - "headingRadians": -0.15624999999999867, - "unwrappedHeadingRadians": -0.15624999999999867, - "arcLengthMeters": 6.6250000000000284, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6661268720560631, - "yMeters": 3.0277507329385678, - "headingRadians": -0.145833333333332, - "unwrappedHeadingRadians": -0.145833333333332, - "arcLengthMeters": 6.6500000000000288, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6908799748531473, - "yMeters": 3.0242466994721062, - "headingRadians": -0.13541666666666535, - "unwrappedHeadingRadians": -0.13541666666666535, - "arcLengthMeters": 6.6750000000000291, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7156682344091214, - "yMeters": 3.0210006962677443, - "headingRadians": -0.12499999999999868, - "unwrappedHeadingRadians": -0.12499999999999868, - "arcLengthMeters": 6.7000000000000295, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7404889610500049, - "yMeters": 3.0180130755361865, - "headingRadians": -0.11458333333333201, - "unwrappedHeadingRadians": -0.11458333333333201, - "arcLengthMeters": 6.72500000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7653394615789431, - "yMeters": 3.0152841614520978, - "headingRadians": -0.10416666666666534, - "unwrappedHeadingRadians": -0.10416666666666534, - "arcLengthMeters": 6.75000000000003, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7902170395684376, - "yMeters": 3.01281425011893, - "headingRadians": -0.093749999999998668, - "unwrappedHeadingRadians": -0.093749999999998668, - "arcLengthMeters": 6.7750000000000306, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8151189956529237, - "yMeters": 3.0106036095367914, - "headingRadians": -0.083333333333332, - "unwrappedHeadingRadians": -0.083333333333332, - "arcLengthMeters": 6.8000000000000309, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8400426278216688, - "yMeters": 3.0086524795733682, - "headingRadians": -0.072916666666665325, - "unwrappedHeadingRadians": -0.072916666666665325, - "arcLengthMeters": 6.8250000000000313, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8649852317119553, - "yMeters": 3.0069610719378965, - "headingRadians": -0.062499999999998661, - "unwrappedHeadingRadians": -0.062499999999998661, - "arcLengthMeters": 6.8500000000000316, - "direction": 0, - "vehicleCurvaturePerMeter": 0.41666666666666663, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 20, - "startsAtGearSwitch": false, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 1, - "direction": 1, - "startIndex": 21, - "endIndex": 61, - "startsAtGearSwitch": true, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 2, - "direction": 0, - "startIndex": 62, - "endIndex": 276, - "startsAtGearSwitch": true, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "narrow-corridor", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:5ec976b32eec84abdd781b8cedeb21d07dfd6809ea61c108bcce8cff0b9d3a37", - "map": { - "xMinMm": 0.0, - "xMaxMm": 6000.0, - "yMinMm": 0.0, - "yMaxMm": 4000.0, - "resolutionMm": 50.0, - "obstacles": [ - { - "kind": "axis-aligned-rectangle", - "xMinMm": 1500.0, - "xMaxMm": 4500.0, - "yMinMm": 500.0, - "yMaxMm": 1200.0, - "centerXMm": 0.0, - "centerYMm": 0.0, - "radiusMm": 0.0 - }, - { - "kind": "axis-aligned-rectangle", - "xMinMm": 1500.0, - "xMaxMm": 4500.0, - "yMinMm": 2800.0, - "yMaxMm": 3500.0, - "centerXMm": 0.0, - "centerYMm": 0.0, - "radiusMm": 0.0 - } - ] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.23384439355857134, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.025, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.024999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.23384439355857134, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.049999999999999822, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.23384439355857134, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.074999999999999734, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.099999999999999645, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.20920160933177634, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.12499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.18684887719411869, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.14999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.18684887719411869, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1749999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.17499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.16697633040195181, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1999999999999993, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.19999999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.16697633040195181, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.2249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14977102437387113, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2499999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.24999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.14977102437387113, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.274999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.274999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2999999999999989, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.29999999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.13541034414477748, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3249999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.32499999999999885, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.12405453637069408, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3499999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.34999999999999876, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.12405453637069408, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3749999999999987, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.37499999999999867, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.11583890685285414, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3999999999999986, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.39999999999999858, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.11583890685285414, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4249999999999985, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.42499999999999849, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.1108664282504217, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4499999999999984, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.4499999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.1108664282504217, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4749999999999983, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.47499999999999831, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4999999999999982, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.49999999999999822, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5249999999999981, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.52499999999999813, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.549999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.549999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.574999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.574999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5999999999999979, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.59999999999999787, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6249999999999978, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.62499999999999778, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6499999999999977, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.64999999999999769, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6749999999999976, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.6749999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6999999999999975, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.69999999999999751, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7249999999999974, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.72499999999999742, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7499999999999973, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.74999999999999734, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7749999999999972, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.77499999999999725, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7999999999999972, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.79999999999999716, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8249999999999971, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.82499999999999707, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.849999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.849999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8749999999999969, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.87499999999999689, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8999999999999968, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.8999999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9249999999999967, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.92499999999999671, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9499999999999966, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.94999999999999662, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9749999999999965, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.97499999999999654, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9999999999999964, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.99999999999999645, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0249999999999964, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0249999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0499999999999963, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0499999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0749999999999962, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0749999999999962, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0999999999999961, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0999999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.124999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.124999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1499999999999959, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1499999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1749999999999958, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1749999999999958, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1999999999999957, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1999999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2249999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2249999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2499999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2749999999999955, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2749999999999955, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2999999999999954, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2999999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3249999999999953, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3249999999999953, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3499999999999952, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3499999999999952, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3749999999999951, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3749999999999951, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.399999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4249999999999949, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4249999999999949, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4499999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4499999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4749999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4749999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4999999999999947, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5249999999999946, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5249999999999946, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5499999999999945, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5499999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5749999999999944, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5749999999999944, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5999999999999943, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5999999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6249999999999942, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.6249999999999942, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6499999999999941, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.6499999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.674999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.674999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.699999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.699999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7249999999999939, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7249999999999939, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7499999999999938, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7749999999999937, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7749999999999937, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7999999999999936, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8249999999999935, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8249999999999935, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8499999999999934, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8499999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8749999999999933, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8749999999999933, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8999999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8999999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9249999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9249999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9499999999999931, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9499999999999931, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.974999999999993, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.974999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9999999999999929, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9999999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0249999999999928, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0249999999999928, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0499999999999927, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0499999999999927, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0749999999999926, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0749999999999926, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0999999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0999999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1249999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1249999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1499999999999924, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1499999999999924, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1749999999999923, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1749999999999923, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1999999999999922, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1999999999999922, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2249999999999921, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2249999999999921, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2749999999999919, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2749999999999919, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2999999999999918, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2999999999999918, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3249999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3249999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3499999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3499999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3749999999999916, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3749999999999916, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3999999999999915, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3999999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4249999999999914, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4249999999999914, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4499999999999913, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4499999999999913, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4749999999999912, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4749999999999912, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4999999999999911, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.524999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.524999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5499999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5499999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5749999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5749999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5999999999999908, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5999999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6249999999999907, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6249999999999907, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6499999999999906, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6499999999999906, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6749999999999905, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6749999999999905, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6999999999999904, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6999999999999904, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7249999999999903, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.7249999999999903, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7499999999999902, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.7499999999999902, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.77499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.77499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.79999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.79999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.82499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.82499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.84999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.84999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.87499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.87499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8999999999999897, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.8999999999999897, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9249999999999896, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9249999999999896, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9499999999999895, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9499999999999895, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9749999999999894, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9749999999999894, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9999999999999893, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9999999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.02499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0249999999999897, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.04999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.04999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.07499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0749999999999904, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0999999999999908, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0999999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1249999999999911, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1249999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1499999999999915, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1499999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1749999999999918, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1749999999999918, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1999999999999922, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1999999999999922, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2249999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2249999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2499999999999929, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2499999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2749999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2749999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2999999999999936, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.324999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.324999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3499999999999943, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.3499999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3749999999999947, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.3749999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.399999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4249999999999954, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4249999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4499999999999957, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4499999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4749999999999961, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4749999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4999999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4999999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5249999999999959, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5249999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5499999999999963, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5499999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.5749999999999966, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5749999999999966, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.10920160933177625, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.599999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.599999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.1108664282504217, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6249999999999973, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6249999999999973, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 0.1108664282504217, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6499999999999977, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6499999999999977, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.674999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.674999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.6999999999999984, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6999999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7249999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7249999999999988, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7499999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7499999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.7749999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7749999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.8, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.825, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.825, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.8500000000000005, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.8500000000000005, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 6.5704775019876669, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 154, - "startsAtGearSwitch": false, - "endsAtGearSwitch": false - } - ] - }, - { - "id": "forward-reverse-switch", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:86e1436370027ad30a7a205a78da291f81345e6acff2c0bb4f2256803ad9d962", - "map": { - "xMinMm": 0.0, - "xMaxMm": 6000.0, - "yMinMm": 0.0, - "yMaxMm": 4000.0, - "resolutionMm": 50.0, - "obstacles": [] - }, - "vehicle": { - "lengthMeters": 0.8, - "widthMeters": 0.6, - "safetyMarginMeters": 0.05, - "maximumCurvaturePerMeter": 0.83333333333333337 - }, - "planningConfiguration": { - "primitiveLengthMeters": 0.5, - "integrationStepMeters": 0.05, - "maximumCollisionCheckStepMeters": 0.025, - "headingResolutionRadians": 0.087266462599716474, - "curvatureLevelCount": 5, - "goalPositionToleranceMeters": 0.15, - "goalHeadingToleranceRadians": 0.087266462599716474, - "maximumExpandedNodes": 200000, - "searchTimeoutSeconds": 120.0, - "heuristicWeight": 1.0, - "reverseCostMultiplier": 1.5, - "gearSwitchPenaltyMeters": 1.0, - "curvatureMagnitudeWeight": 0.1, - "curvatureChangePenaltyMetersPerLevel": 0.05, - "clearanceCostWeight": 0.2, - "clearanceCostDistanceMeters": 0.5, - "allowReverse": true - }, - "path": [ - { - "xMeters": 1.0, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.0, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 0 - }, - { - "xMeters": 1.025, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.024999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0499999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.049999999999999822, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0749999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.074999999999999734, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.0999999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.099999999999999645, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1249999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.12499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1499999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.14999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1749999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.17499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.1999999999999993, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.19999999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.2249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2499999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.24999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.274999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.274999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.2999999999999989, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.29999999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3249999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.32499999999999885, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3499999999999988, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.34999999999999876, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3749999999999987, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.37499999999999867, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.3999999999999986, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.39999999999999858, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4249999999999985, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.42499999999999849, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4499999999999984, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.4499999999999984, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4749999999999983, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.47499999999999831, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.4999999999999982, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.49999999999999822, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5249999999999981, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.52499999999999813, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.549999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.549999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.574999999999998, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.574999999999998, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.5999999999999979, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.59999999999999787, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6249999999999978, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.62499999999999778, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6499999999999977, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.64999999999999769, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6749999999999976, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.6749999999999976, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.6999999999999975, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.69999999999999751, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7249999999999974, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.72499999999999742, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7499999999999973, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.74999999999999734, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7749999999999972, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.77499999999999725, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.7999999999999972, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.79999999999999716, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8249999999999971, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.82499999999999707, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.849999999999997, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.849999999999997, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8749999999999969, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.87499999999999689, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.8999999999999968, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.8999999999999968, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9249999999999967, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.92499999999999671, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9499999999999966, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.94999999999999662, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9749999999999965, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.97499999999999654, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 1.9999999999999964, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 0.99999999999999645, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0249999999999964, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0249999999999964, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0499999999999963, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0499999999999963, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0749999999999962, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0749999999999962, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.0999999999999961, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.0999999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.124999999999996, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.124999999999996, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1499999999999959, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1499999999999959, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1749999999999958, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1749999999999958, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.1999999999999957, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.1999999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2249999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2249999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2499999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2499999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2749999999999955, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2749999999999955, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.2999999999999954, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.2999999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3249999999999953, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3249999999999953, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3499999999999952, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3499999999999952, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.3749999999999951, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.3749999999999951, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.399999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4249999999999949, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4249999999999949, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4499999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4499999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4749999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4749999999999948, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.4999999999999947, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.4999999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5249999999999946, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5249999999999946, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5499999999999945, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5499999999999945, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5749999999999944, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5749999999999944, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.5999999999999943, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.5999999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6249999999999942, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.6249999999999942, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.6499999999999941, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.6499999999999941, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.674999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.674999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.699999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.699999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7249999999999939, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7249999999999939, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7499999999999938, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7499999999999938, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7749999999999937, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7749999999999937, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.7999999999999936, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.7999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8249999999999935, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8249999999999935, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8499999999999934, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8499999999999934, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8749999999999933, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8749999999999933, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.8999999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.8999999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9249999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9249999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9499999999999931, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9499999999999931, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.974999999999993, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.974999999999993, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 2.9999999999999929, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 1.9999999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0249999999999928, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0249999999999928, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0499999999999927, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0499999999999927, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0749999999999926, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0749999999999926, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.0999999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.0999999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1249999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1249999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1499999999999924, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1499999999999924, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1749999999999923, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1749999999999923, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.1999999999999922, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.1999999999999922, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2249999999999921, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2249999999999921, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.249999999999992, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2749999999999919, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2749999999999919, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.2999999999999918, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.2999999999999918, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3249999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3249999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3499999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3499999999999917, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3749999999999916, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3749999999999916, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.3999999999999915, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.3999999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4249999999999914, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4249999999999914, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4499999999999913, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4499999999999913, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4749999999999912, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4749999999999912, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.4999999999999911, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.4999999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.524999999999991, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.524999999999991, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5499999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5499999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5749999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5749999999999909, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.5999999999999908, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.5999999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6249999999999907, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6249999999999907, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6499999999999906, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6499999999999906, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6749999999999905, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6749999999999905, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.6999999999999904, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.6999999999999904, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7249999999999903, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.7249999999999903, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.7499999999999902, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.7499999999999902, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.77499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.77499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.79999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.79999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.82499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.82499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.84999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.84999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.87499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.87499999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.8999999999999897, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.8999999999999897, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9249999999999896, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9249999999999896, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9499999999999895, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9499999999999895, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9749999999999894, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9749999999999894, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 3.9999999999999893, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 2.9999999999999893, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.02499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0249999999999897, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.04999999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.04999999999999, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.07499999999999, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0749999999999904, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.0999999999999908, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.0999999999999908, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1249999999999911, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1249999999999911, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1499999999999915, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1499999999999915, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1749999999999918, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1749999999999918, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1999999999999922, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.1999999999999922, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2249999999999925, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2249999999999925, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2499999999999929, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2499999999999929, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2749999999999932, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2749999999999932, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2999999999999936, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.2999999999999936, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.324999999999994, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.324999999999994, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3499999999999943, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.3499999999999943, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3749999999999947, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.3749999999999947, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.399999999999995, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.399999999999995, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4249999999999954, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4249999999999954, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4499999999999957, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4499999999999957, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4749999999999961, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4749999999999961, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4999999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4999999999999956, - "direction": 0, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4999999999999956, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.4999999999999956, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": true, - "source": 1 - }, - { - "xMeters": 4.4749999999999952, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5249999999999959, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4499999999999948, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5499999999999963, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.4249999999999945, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.5749999999999966, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3999999999999941, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.599999999999997, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3749999999999938, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6249999999999973, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3499999999999934, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6499999999999977, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.3249999999999931, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.674999999999998, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2999999999999927, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.6999999999999984, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2749999999999924, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7249999999999988, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.249999999999992, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7499999999999991, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.2249999999999917, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.7749999999999995, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1999999999999913, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.8, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1749999999999909, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.825, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 1 - }, - { - "xMeters": 4.1499999999999906, - "yMeters": 2.0, - "headingRadians": 0.0, - "unwrappedHeadingRadians": 0.0, - "arcLengthMeters": 3.8500000000000005, - "direction": 1, - "vehicleCurvaturePerMeter": 0.0, - "bodyClearanceMeters": 7.2111025509279782, - "isGearSwitchPoint": false, - "source": 2 - } - ], - "segments": [ - { - "segmentIndex": 0, - "direction": 0, - "startIndex": 0, - "endIndex": 140, - "startsAtGearSwitch": false, - "endsAtGearSwitch": true - }, - { - "segmentIndex": 1, - "direction": 1, - "startIndex": 141, - "endIndex": 155, - "startsAtGearSwitch": true, - "endsAtGearSwitch": false - } - ] - } - ] -} \ No newline at end of file diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs deleted file mode 100644 index 2d564be..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Test; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; - -/// 开发人员离线导出八个快速夹具和四个 Hybrid A* 端到端场景的比较报告。 -public sealed class PathSmoothingComparisonDemo -{ - private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); - private readonly PathSmoothingComparisonService _comparisonService = new PathSmoothingComparisonService(); - private readonly SmoothingFigureModelBuilder _figureBuilder = new SmoothingFigureModelBuilder(); - private readonly SmoothingReportExporter _reportExporter = new SmoothingReportExporter(); - - /// 导出八个已验证夹具;不运行 Hybrid A*。 - public IReadOnlyList ExportFixtureReports( - string fixturePath, - string outputDirectory, - CancellationToken cancellationToken = default) - { - IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); - IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); - var results = new List(requests.Count); - for (int index = 0; index < requests.Count; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - results.Add(Export(fixtures[index].Id, requests[index], outputDirectory, cancellationToken)); - } - return results.AsReadOnly(); - } - - /// 运行四个固定 Hybrid A* 场景,并仅为成功粗路径导出平滑比较报告。 - public IReadOnlyList ExportEndToEndReports( - string outputDirectory, - CancellationToken cancellationToken = default) - { - CoarsePathTestScenario[] scenarios = - { - CoarsePathTestScenario.ExplicitEmpty, - CoarsePathTestScenario.RectangleDetour, - CoarsePathTestScenario.ManualAndTwoLeg, - CoarsePathTestScenario.ReverseGearSwitch, - }; - var planner = new CoarsePathPlanningService(); - var results = new List(scenarios.Length); - for (int index = 0; index < scenarios.Length; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenarios[index]); - CoarsePathPlanningJobResult coarse = planner.Plan(job, cancellationToken); - if (coarse.PlanningResult.Status != PlanningStatus.Success) - { - results.Add(PathSmoothingComparisonScenarioResult.CoarsePathFailure( - scenarios[index].ToString(), coarse.PlanningResult.Status, coarse.PlanningResult.Diagnostics.TerminationReason)); - continue; - } - results.Add(Export( - scenarios[index].ToString(), - SmoothingScenarioFactory.CreateEndToEndRequest(job, coarse), - outputDirectory, - cancellationToken)); - } - return results.AsReadOnly(); - } - - /// 供业务调用示例使用:仅在已成功的粗路径上调用平滑服务。 - public PathSmoothingResult SmoothSuccessfulCoarsePath( - CoarsePathPlanningJob job, - CoarsePathPlanningJobResult coarseResult, - PathSmoothingConfiguration configuration, - CancellationToken cancellationToken = default) - { - if (job == null || coarseResult == null || coarseResult.PlanningResult.Status != PlanningStatus.Success) - throw new ArgumentException("只有成功的粗路径能够进入平滑流程。", nameof(coarseResult)); - return _smoothingService.Smooth(new PathSmoothingRequest( - coarseResult.PlanningResult.Path, - coarseResult.PlanningResult.Segments, - coarseResult.MapResult.Map, - job.Vehicle, - configuration), cancellationToken); - } - - /// 业务调用方只可消费成功平滑或显式回退的路径。 - public static bool IsConsumable(PathSmoothingStatus status) - { - return status == PathSmoothingStatus.Success || status == PathSmoothingStatus.FallbackToCoarsePath; - } - - private PathSmoothingComparisonScenarioResult Export( - string scenarioId, - PathSmoothingComparisonRequest request, - string outputDirectory, - CancellationToken cancellationToken) - { - if (request == null || request.SmoothingRequest == null || request.SmoothingRequest.CoarsePath.Count == 0) - throw new ArgumentException("比较请求必须包含粗路径。", nameof(request)); - PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken); - CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; - CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; - SmoothingFigureModel model = _figureBuilder.Build( - comparison, - request.SmoothingRequest.Map, - new Pose2D(first.X, first.Y, first.Heading), - new Pose2D(last.X, last.Y, last.Heading), - scenarioId, - scenarioId); - SmoothingReportExportResult report = _reportExporter.Export(new SmoothingReportExportRequest - { - Model = model, - OutputDirectory = System.IO.Path.Combine(outputDirectory, scenarioId), - FileStem = "comparison", - }); - return PathSmoothingComparisonScenarioResult.ComparisonComplete(scenarioId, comparison, report); - } -} - -/// 批处理为一个场景发布的粗路径状态、平滑比较和报告结果。 -public sealed class PathSmoothingComparisonScenarioResult -{ - private PathSmoothingComparisonScenarioResult( - string scenarioId, - PlanningStatus coarsePathStatus, - string diagnostic, - PathSmoothingComparisonResult comparison, - SmoothingReportExportResult report) - { - ScenarioId = scenarioId ?? string.Empty; - CoarsePathStatus = coarsePathStatus; - Diagnostic = diagnostic ?? string.Empty; - Comparison = comparison; - Report = report; - } - - public string ScenarioId { get; } - public PlanningStatus CoarsePathStatus { get; } - public string Diagnostic { get; } - public PathSmoothingComparisonResult Comparison { get; } - public SmoothingReportExportResult Report { get; } - - internal static PathSmoothingComparisonScenarioResult CoarsePathFailure( - string scenarioId, - PlanningStatus status, - string diagnostic) - { - return new PathSmoothingComparisonScenarioResult(scenarioId, status, diagnostic, null, null); - } - - internal static PathSmoothingComparisonScenarioResult ComparisonComplete( - string scenarioId, - PathSmoothingComparisonResult comparison, - SmoothingReportExportResult report) - { - return new PathSmoothingComparisonScenarioResult(scenarioId, PlanningStatus.Success, string.Empty, comparison, report); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs deleted file mode 100644 index c942216..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs +++ /dev/null @@ -1,260 +0,0 @@ -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; - -/// 显式开发入口:从已成功的 Hybrid A* 粗路径结果生成八个稳定的快速比较夹具。 -public static class SmoothingFixtureGenerator -{ - // 仅用于开发时固化真实规划输出;不会改变业务场景或运行时服务的预算。 - private static readonly TimeSpan FixtureGenerationSearchTimeout = TimeSpan.FromSeconds(120d); - - /// 生成夹具 JSON;目标已存在且未明确允许覆盖时拒绝写入。 - 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 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 CreatePlans() - { - return new List - { - 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 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 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 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 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 createJob) - { - Id = id; - CreateJob = createJob; - } - - public string Id { get; } - public Func CreateJob { get; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs deleted file mode 100644 index f263573..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; - -/// 将快速夹具或现有粗路径业务结果转换为独立平滑比较请求。 -public static class SmoothingScenarioFactory -{ - /// 不运行 Hybrid A*,从已验证的 JSON 夹具创建八个快速比较请求。 - public static IReadOnlyList CreateFixtureRequests(string fixturePath) - { - IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); - var requests = new List(fixtures.Count); - for (int index = 0; index < fixtures.Count; index++) - { - SmoothingScenarioFixture fixture = fixtures[index]; - requests.Add(new PathSmoothingComparisonRequest( - new PathSmoothingRequest( - fixture.Path, - fixture.Segments, - SmoothingScenarioFixtureLoader.BuildMap(fixture), - SmoothingScenarioFixtureLoader.BuildVehicle(fixture), - new PathSmoothingConfiguration()))); - } - return requests.AsReadOnly(); - } - - /// 把现有 已规划成功的结果转换为比较请求。 - public static PathSmoothingComparisonRequest CreateEndToEndRequest( - CoarsePathPlanningJob job, - CoarsePathPlanningJobResult result) - { - if (job == null || result == null || result.MapResult == null || result.MapResult.Map == null || - result.PlanningResult == null || result.PlanningResult.Status != PlanningStatus.Success) - throw new ArgumentException("只能从包含成功地图和粗路径的业务结果创建平滑比较请求。", nameof(result)); - - return new PathSmoothingComparisonRequest(new PathSmoothingRequest( - CopyWithFiniteClearance(result.PlanningResult.Path, result.MapResult.Map), - result.PlanningResult.Segments, - result.MapResult.Map, - job.Vehicle, - new PathSmoothingConfiguration())); - } - - internal static IReadOnlyList CopyWithFiniteClearance( - IReadOnlyList source, - MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap map) - { - double widthMeters = (map.Bounds.XMax - map.Bounds.XMin) / 1000d; - double heightMeters = (map.Bounds.YMax - map.Bounds.YMin) / 1000d; - double finiteEmptyMapClearance = Math.Sqrt(widthMeters * widthMeters + heightMeters * heightMeters); - var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) - { - CoarsePathPoint point = source[index]; - double clearance = double.IsPositiveInfinity(point.BodyClearance) - ? finiteEmptyMapClearance - : point.BodyClearance; - copy.Add(new CoarsePathPoint( - point.X, point.Y, point.Heading, point.UnwrappedHeading, point.ArcLength, point.Direction, - point.VehicleCurvature, clearance, point.IsGearSwitchPoint, point.Source)); - } - return copy.AsReadOnly(); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs deleted file mode 100644 index e19185d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using Newtonsoft.Json; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; - -/// 一个可复现的快速粗路径夹具及其地图、车辆和方向段快照。 -public sealed class SmoothingScenarioFixture -{ - internal SmoothingScenarioFixture(SmoothingFixtureRecord record, bool fingerprintCurrent) - { - Record = record; - Id = record.Id; - FixtureVersion = record.FixtureVersion; - ConfigurationFingerprint = record.ConfigurationFingerprint; - IsConfigurationFingerprintCurrent = fingerprintCurrent; - Path = ToPath(record.Path); - Segments = ToSegments(record.Segments); - } - - /// 稳定英文场景标识。 - public string Id { get; } - /// 夹具自身的正版本号。 - public int FixtureVersion { get; } - /// 由夹具输入生成的稳定 SHA-256 指纹。 - public string ConfigurationFingerprint { get; } - /// 存储指纹是否与当前夹具内容一致。 - public bool IsConfigurationFingerprintCurrent { get; } - /// 快速比较使用的不可变粗路径点。 - public IReadOnlyList Path { get; } - /// 覆盖粗路径的不可变方向段。 - public IReadOnlyList Segments { get; } - - internal SmoothingFixtureRecord Record { get; } - - private static IReadOnlyList ToPath(IReadOnlyList source) - { - var result = new List(source.Count); - for (int index = 0; index < source.Count; index++) - { - SmoothingFixturePathPoint point = source[index]; - result.Add(new CoarsePathPoint( - point.XMeters, point.YMeters, point.HeadingRadians, point.UnwrappedHeadingRadians, - point.ArcLengthMeters, point.Direction, point.VehicleCurvaturePerMeter, - point.BodyClearanceMeters, point.IsGearSwitchPoint, point.Source)); - } - return new ReadOnlyCollection(result); - } - - private static IReadOnlyList ToSegments(IReadOnlyList source) - { - var result = new List(source.Count); - for (int index = 0; index < source.Count; index++) - { - SmoothingFixtureSegment segment = source[index]; - result.Add(new PathSegment(segment.SegmentIndex, segment.Direction, segment.StartIndex, segment.EndIndex, - segment.StartsAtGearSwitch, segment.EndsAtGearSwitch)); - } - return new ReadOnlyCollection(result); - } -} - -internal sealed class SmoothingFixtureDocument -{ - [JsonProperty("schemaVersion")] public int SchemaVersion { get; set; } - [JsonProperty("scenarios")] public List Scenarios { get; set; } = new List(); -} - -internal sealed class SmoothingFixtureRecord -{ - [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("fixtureVersion")] public int FixtureVersion { get; set; } - [JsonProperty("configurationFingerprint")] public string ConfigurationFingerprint { get; set; } - [JsonProperty("map")] public SmoothingFixtureMap Map { get; set; } - [JsonProperty("vehicle")] public SmoothingFixtureVehicle Vehicle { get; set; } - [JsonProperty("planningConfiguration")] public SmoothingFixturePlanningConfiguration PlanningConfiguration { get; set; } - [JsonProperty("path")] public List Path { get; set; } = new List(); - [JsonProperty("segments")] public List Segments { get; set; } = new List(); -} - -internal sealed class SmoothingFixtureMap -{ - [JsonProperty("xMinMm")] public float XMinMm { get; set; } - [JsonProperty("xMaxMm")] public float XMaxMm { get; set; } - [JsonProperty("yMinMm")] public float YMinMm { get; set; } - [JsonProperty("yMaxMm")] public float YMaxMm { get; set; } - [JsonProperty("resolutionMm")] public float ResolutionMm { get; set; } - [JsonProperty("obstacles")] public List Obstacles { get; set; } = new List(); -} - -internal sealed class SmoothingFixtureObstacle -{ - [JsonProperty("kind")] public string Kind { get; set; } - [JsonProperty("xMinMm")] public float XMinMm { get; set; } - [JsonProperty("xMaxMm")] public float XMaxMm { get; set; } - [JsonProperty("yMinMm")] public float YMinMm { get; set; } - [JsonProperty("yMaxMm")] public float YMaxMm { get; set; } - [JsonProperty("centerXMm")] public float CenterXMm { get; set; } - [JsonProperty("centerYMm")] public float CenterYMm { get; set; } - [JsonProperty("radiusMm")] public float RadiusMm { get; set; } -} - -internal sealed class SmoothingFixtureVehicle -{ - [JsonProperty("lengthMeters")] public double LengthMeters { get; set; } - [JsonProperty("widthMeters")] public double WidthMeters { get; set; } - [JsonProperty("safetyMarginMeters")] public double SafetyMarginMeters { get; set; } - [JsonProperty("maximumCurvaturePerMeter")] public double MaximumCurvaturePerMeter { get; set; } -} - -/// 产生夹具粗路径时使用的 Hybrid A* 配置快照;仅用于溯源和指纹验证,不参与快速夹具加载时的规划。 -internal sealed class SmoothingFixturePlanningConfiguration -{ - [JsonProperty("primitiveLengthMeters")] public double PrimitiveLengthMeters { get; set; } - [JsonProperty("integrationStepMeters")] public double IntegrationStepMeters { get; set; } - [JsonProperty("maximumCollisionCheckStepMeters")] public double MaximumCollisionCheckStepMeters { get; set; } - [JsonProperty("headingResolutionRadians")] public double HeadingResolutionRadians { get; set; } - [JsonProperty("curvatureLevelCount")] public int CurvatureLevelCount { get; set; } - [JsonProperty("goalPositionToleranceMeters")] public double GoalPositionToleranceMeters { get; set; } - [JsonProperty("goalHeadingToleranceRadians")] public double GoalHeadingToleranceRadians { get; set; } - [JsonProperty("maximumExpandedNodes")] public int MaximumExpandedNodes { get; set; } - [JsonProperty("searchTimeoutSeconds")] public double SearchTimeoutSeconds { get; set; } - [JsonProperty("heuristicWeight")] public double HeuristicWeight { get; set; } - [JsonProperty("reverseCostMultiplier")] public double ReverseCostMultiplier { get; set; } - [JsonProperty("gearSwitchPenaltyMeters")] public double GearSwitchPenaltyMeters { get; set; } - [JsonProperty("curvatureMagnitudeWeight")] public double CurvatureMagnitudeWeight { get; set; } - [JsonProperty("curvatureChangePenaltyMetersPerLevel")] public double CurvatureChangePenaltyMetersPerLevel { get; set; } - [JsonProperty("clearanceCostWeight")] public double ClearanceCostWeight { get; set; } - [JsonProperty("clearanceCostDistanceMeters")] public double ClearanceCostDistanceMeters { get; set; } - [JsonProperty("allowReverse")] public bool AllowReverse { get; set; } -} - -internal sealed class SmoothingFixturePathPoint -{ - [JsonProperty("xMeters")] public double XMeters { get; set; } - [JsonProperty("yMeters")] public double YMeters { get; set; } - [JsonProperty("headingRadians")] public double HeadingRadians { get; set; } - [JsonProperty("unwrappedHeadingRadians")] public double UnwrappedHeadingRadians { get; set; } - [JsonProperty("arcLengthMeters")] public double ArcLengthMeters { get; set; } - [JsonProperty("direction")] public TravelDirection Direction { get; set; } - [JsonProperty("vehicleCurvaturePerMeter")] public double VehicleCurvaturePerMeter { get; set; } - [JsonProperty("bodyClearanceMeters")] public double BodyClearanceMeters { get; set; } - [JsonProperty("isGearSwitchPoint")] public bool IsGearSwitchPoint { get; set; } - [JsonProperty("source")] public CoarsePathPointSource Source { get; set; } -} - -internal sealed class SmoothingFixtureSegment -{ - [JsonProperty("segmentIndex")] public int SegmentIndex { get; set; } - [JsonProperty("direction")] public TravelDirection Direction { get; set; } - [JsonProperty("startIndex")] public int StartIndex { get; set; } - [JsonProperty("endIndex")] public int EndIndex { get; set; } - [JsonProperty("startsAtGearSwitch")] public bool StartsAtGearSwitch { get; set; } - [JsonProperty("endsAtGearSwitch")] public bool EndsAtGearSwitch { get; set; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs deleted file mode 100644 index e66b4e9..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using Newtonsoft.Json; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; - -/// 加载并验证版本化 JSON 快速夹具;此类不运行 Hybrid A*。 -public static class SmoothingScenarioFixtureLoader -{ - /// 读取、校验并冻结指定的快速夹具文件。 - public static IReadOnlyList LoadAndVerify(string fixturePath) - { - if (string.IsNullOrWhiteSpace(fixturePath)) throw new ArgumentException("夹具路径不能为空。", nameof(fixturePath)); - if (!File.Exists(fixturePath)) throw new FileNotFoundException("找不到路径平滑夹具文件。", fixturePath); - - SmoothingFixtureDocument document = JsonConvert.DeserializeObject( - File.ReadAllText(fixturePath, Encoding.UTF8)); - if (document == null || document.SchemaVersion != 1 || document.Scenarios == null) - throw new InvalidDataException("路径平滑夹具架构版本无效。"); - - var fixtures = new List(document.Scenarios.Count); - var ids = new HashSet(StringComparer.Ordinal); - for (int index = 0; index < document.Scenarios.Count; index++) - { - SmoothingFixtureRecord record = document.Scenarios[index]; - Validate(record, ids); - string fingerprint = ComputeFingerprint(record); - if (!string.Equals(record.ConfigurationFingerprint, fingerprint, StringComparison.Ordinal)) - throw new InvalidDataException("路径平滑夹具指纹已过期:" + record.Id + "。"); - fixtures.Add(new SmoothingScenarioFixture(record, true)); - } - return fixtures.AsReadOnly(); - } - - internal static string ComputeFingerprint(SmoothingFixtureRecord record) - { - string material = BuildFingerprintMaterial(record); - using (SHA256 sha256 = SHA256.Create()) - { - byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(material)); - var builder = new StringBuilder(hash.Length * 2 + 7); - builder.Append("sha256:"); - for (int index = 0; index < hash.Length; index++) builder.Append(hash[index].ToString("x2", CultureInfo.InvariantCulture)); - return builder.ToString(); - } - } - - internal static PlanningGridMap BuildMap(SmoothingScenarioFixture fixture) - { - SmoothingFixtureRecord record = fixture.Record; - var obstacles = new List(record.Map.Obstacles.Count); - for (int index = 0; index < record.Map.Obstacles.Count; index++) - { - SmoothingFixtureObstacle obstacle = record.Map.Obstacles[index]; - if (string.Equals(obstacle.Kind, "circle", StringComparison.Ordinal)) - obstacles.Add(new CircleObstacle(obstacle.CenterXMm, obstacle.CenterYMm, obstacle.RadiusMm)); - else if (string.Equals(obstacle.Kind, "axis-aligned-rectangle", StringComparison.Ordinal)) - obstacles.Add(new AxisAlignedRectangleObstacle(obstacle.XMinMm, obstacle.XMaxMm, obstacle.YMinMm, obstacle.YMaxMm)); - else - throw new InvalidDataException("夹具包含未知障碍物类型。"); - } - - var mapRequest = new PlanningMapRequest - { - Bounds = new MapBoundsMm(record.Map.XMinMm, record.Map.XMaxMm, record.Map.YMinMm, record.Map.YMaxMm), - ResolutionMm = record.Map.ResolutionMm, - AllowExplicitEmptyMap = obstacles.Count == 0, - ObstacleSources = obstacles.Count == 0 - ? Array.Empty() - : new IMapObstacleSource[] { new ManualObstacleSource("fixture-" + record.Id, record.FixtureVersion, true, obstacles) }, - }; - PlanningMapBuildResult build = new PlanningMapFactory().Create(mapRequest); - if (!build.Succeeded || build.Map == null) throw new InvalidDataException("夹具地图无法重建:" + record.Id + "。"); - return build.Map; - } - - internal static VehicleParameters BuildVehicle(SmoothingScenarioFixture fixture) - { - SmoothingFixtureVehicle vehicle = fixture.Record.Vehicle; - return new VehicleParameters - { - LengthMeters = vehicle.LengthMeters, - WidthMeters = vehicle.WidthMeters, - SafetyMarginMeters = vehicle.SafetyMarginMeters, - MaximumCurvaturePerMeter = vehicle.MaximumCurvaturePerMeter, - }; - } - - private static void Validate(SmoothingFixtureRecord record, ISet ids) - { - if (record == null || string.IsNullOrWhiteSpace(record.Id) || record.FixtureVersion <= 0 || - record.Map == null || record.Vehicle == null || record.PlanningConfiguration == null || record.Path == null || record.Path.Count < 2 || - record.Segments == null || record.Segments.Count == 0 || !ids.Add(record.Id)) - throw new InvalidDataException("路径平滑夹具缺少必需字段、版本或唯一 ID。"); - - ValidateMap(record); - ValidateVehicle(record); - ValidatePlanningConfiguration(record); - ValidatePathContract(record); - } - - private static void ValidateMap(SmoothingFixtureRecord record) - { - SmoothingFixtureMap map = record.Map; - if (!IsFinite(map.XMinMm) || !IsFinite(map.XMaxMm) || !IsFinite(map.YMinMm) || !IsFinite(map.YMaxMm) || - !IsFinite(map.ResolutionMm) || map.XMaxMm <= map.XMinMm || map.YMaxMm <= map.YMinMm || map.ResolutionMm <= 0f || - map.Obstacles == null) - { - throw new InvalidDataException("Fixture map contract is invalid: " + record.Id + "."); - } - - for (int index = 0; index < map.Obstacles.Count; index++) - { - SmoothingFixtureObstacle obstacle = map.Obstacles[index]; - bool circle = obstacle != null && string.Equals(obstacle.Kind, "circle", StringComparison.Ordinal) && - IsFinite(obstacle.CenterXMm) && IsFinite(obstacle.CenterYMm) && IsFinite(obstacle.RadiusMm) && obstacle.RadiusMm >= 0f; - bool rectangle = obstacle != null && string.Equals(obstacle.Kind, "axis-aligned-rectangle", StringComparison.Ordinal) && - IsFinite(obstacle.XMinMm) && IsFinite(obstacle.XMaxMm) && IsFinite(obstacle.YMinMm) && IsFinite(obstacle.YMaxMm) && - obstacle.XMaxMm >= obstacle.XMinMm && obstacle.YMaxMm >= obstacle.YMinMm; - if (!circle && !rectangle) - throw new InvalidDataException("Fixture map contract is invalid: " + record.Id + "."); - } - } - - private static void ValidateVehicle(SmoothingFixtureRecord record) - { - SmoothingFixtureVehicle vehicle = record.Vehicle; - if (!IsFinite(vehicle.LengthMeters) || !IsFinite(vehicle.WidthMeters) || !IsFinite(vehicle.SafetyMarginMeters) || - !IsFinite(vehicle.MaximumCurvaturePerMeter) || vehicle.LengthMeters <= 0d || vehicle.WidthMeters <= 0d || - vehicle.SafetyMarginMeters < 0d || vehicle.MaximumCurvaturePerMeter <= 0d) - { - throw new InvalidDataException("Fixture vehicle contract is invalid: " + record.Id + "."); - } - } - - private static void ValidatePlanningConfiguration(SmoothingFixtureRecord record) - { - SmoothingFixturePlanningConfiguration configuration = record.PlanningConfiguration; - if (!IsPositiveFinite(configuration.PrimitiveLengthMeters) || !IsPositiveFinite(configuration.IntegrationStepMeters) || - !IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || !IsPositiveFinite(configuration.HeadingResolutionRadians) || - configuration.CurvatureLevelCount < 2 || !IsPositiveFinite(configuration.GoalPositionToleranceMeters) || - !IsPositiveFinite(configuration.GoalHeadingToleranceRadians) || configuration.MaximumExpandedNodes <= 0 || - !IsPositiveFinite(configuration.SearchTimeoutSeconds) || !IsFinite(configuration.HeuristicWeight) || - !IsFinite(configuration.ReverseCostMultiplier) || !IsFinite(configuration.GearSwitchPenaltyMeters) || - !IsFinite(configuration.CurvatureMagnitudeWeight) || !IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) || - !IsFinite(configuration.ClearanceCostWeight) || !IsPositiveFinite(configuration.ClearanceCostDistanceMeters)) - { - throw new InvalidDataException("Fixture planning configuration contract is invalid: " + record.Id + "."); - } - } - - private static void ValidatePathContract(SmoothingFixtureRecord record) - { - var fixture = new SmoothingScenarioFixture(record, true); - try - { - var request = new PathSmoothingRequest( - fixture.Path, - fixture.Segments, - BuildMap(fixture), - BuildVehicle(fixture), - new PathSmoothingConfiguration()); - if (!new PathSmoothingPreprocessor().TryPrepare(request, out _, out string reason)) - throw new InvalidDataException("Fixture path contract is invalid: " + record.Id + "; " + reason); - } - catch (InvalidDataException) - { - throw; - } - catch (Exception exception) - { - throw new InvalidDataException("Fixture path contract is invalid: " + record.Id + "; " + exception.Message, exception); - } - } - - private static bool IsFinite(float value) - { - return !float.IsNaN(value) && !float.IsInfinity(value); - } - - private static bool IsFinite(double value) - { - return !double.IsNaN(value) && !double.IsInfinity(value); - } - - private static bool IsPositiveFinite(double value) - { - return IsFinite(value) && value > 0d; - } - - private static string BuildFingerprintMaterial(SmoothingFixtureRecord record) - { - var builder = new StringBuilder(); - Append(builder, record.Id); Append(builder, record.FixtureVersion); - Append(builder, record.Map.XMinMm); Append(builder, record.Map.XMaxMm); Append(builder, record.Map.YMinMm); - Append(builder, record.Map.YMaxMm); Append(builder, record.Map.ResolutionMm); - for (int index = 0; index < record.Map.Obstacles.Count; index++) - { - SmoothingFixtureObstacle obstacle = record.Map.Obstacles[index]; - Append(builder, obstacle.Kind); Append(builder, obstacle.XMinMm); Append(builder, obstacle.XMaxMm); - Append(builder, obstacle.YMinMm); Append(builder, obstacle.YMaxMm); Append(builder, obstacle.CenterXMm); - Append(builder, obstacle.CenterYMm); Append(builder, obstacle.RadiusMm); - } - Append(builder, record.Vehicle.LengthMeters); Append(builder, record.Vehicle.WidthMeters); - Append(builder, record.Vehicle.SafetyMarginMeters); Append(builder, record.Vehicle.MaximumCurvaturePerMeter); - SmoothingFixturePlanningConfiguration configuration = record.PlanningConfiguration; - Append(builder, configuration.PrimitiveLengthMeters); Append(builder, configuration.IntegrationStepMeters); - Append(builder, configuration.MaximumCollisionCheckStepMeters); Append(builder, configuration.HeadingResolutionRadians); - Append(builder, configuration.CurvatureLevelCount); Append(builder, configuration.GoalPositionToleranceMeters); - Append(builder, configuration.GoalHeadingToleranceRadians); Append(builder, configuration.MaximumExpandedNodes); - Append(builder, configuration.SearchTimeoutSeconds); Append(builder, configuration.HeuristicWeight); - Append(builder, configuration.ReverseCostMultiplier); Append(builder, configuration.GearSwitchPenaltyMeters); - Append(builder, configuration.CurvatureMagnitudeWeight); Append(builder, configuration.CurvatureChangePenaltyMetersPerLevel); - Append(builder, configuration.ClearanceCostWeight); Append(builder, configuration.ClearanceCostDistanceMeters); - Append(builder, configuration.AllowReverse ? 1 : 0); - for (int index = 0; index < record.Path.Count; index++) - { - SmoothingFixturePathPoint point = record.Path[index]; - Append(builder, point.XMeters); Append(builder, point.YMeters); Append(builder, point.HeadingRadians); - Append(builder, point.UnwrappedHeadingRadians); Append(builder, point.ArcLengthMeters); Append(builder, (int)point.Direction); - Append(builder, point.VehicleCurvaturePerMeter); Append(builder, point.BodyClearanceMeters); - Append(builder, point.IsGearSwitchPoint ? 1 : 0); Append(builder, (int)point.Source); - } - for (int index = 0; index < record.Segments.Count; index++) - { - SmoothingFixtureSegment segment = record.Segments[index]; - Append(builder, segment.SegmentIndex); Append(builder, (int)segment.Direction); Append(builder, segment.StartIndex); - Append(builder, segment.EndIndex); Append(builder, segment.StartsAtGearSwitch ? 1 : 0); Append(builder, segment.EndsAtGearSwitch ? 1 : 0); - } - return builder.ToString(); - } - - private static void Append(StringBuilder builder, string value) { builder.Append(value ?? string.Empty).Append('|'); } - private static void Append(StringBuilder builder, int value) { builder.Append(value.ToString(CultureInfo.InvariantCulture)).Append('|'); } - private static void Append(StringBuilder builder, float value) - { - AppendBits(builder, BitConverter.GetBytes(value)); - } - - private static void Append(StringBuilder builder, double value) - { - AppendBits(builder, BitConverter.GetBytes(value)); - } - - private static void AppendBits(StringBuilder builder, byte[] bytes) - { - for (int index = bytes.Length - 1; index >= 0; index--) - builder.Append(bytes[index].ToString("x2", CultureInfo.InvariantCulture)); - builder.Append('|'); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs deleted file mode 100644 index 31fb88d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; - -/// 独立复核平滑候选的端点拓扑、车辆曲率和连续车体安全性。 -public sealed class SmoothedPathValidator -{ - private const double Tolerance = 1e-6d; - private readonly FootprintCollisionChecker _collisionChecker; - - /// 创建使用默认连续车体碰撞检查器的平滑路径验证器。 - public SmoothedPathValidator() - : this(new FootprintCollisionChecker()) - { - } - - /// 创建使用指定连续车体碰撞检查器的平滑路径验证器。 - public SmoothedPathValidator(FootprintCollisionChecker collisionChecker) - { - _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); - } - - /// - /// 复核候选平滑路径。每个候选方向段必须保持原始段的端点和换向拓扑; - /// 输出中的净空均由本次实际车体检查重新计算,绝不沿用候选声明值。 - /// - public bool TryValidate( - IReadOnlyList candidatePath, - IReadOnlyList candidateSegments, - PreparedPath originalPath, - PlanningGridMap map, - VehicleParameters vehicle, - double maximumCollisionCheckStepMeters, - out IReadOnlyList pathWithClearance, - out double minimumClearanceMeters, - out string reason) - { - pathWithClearance = EmptyPath(); - minimumClearanceMeters = 0d; - reason = string.Empty; - if (candidatePath == null || candidateSegments == null || originalPath == null || map == null || vehicle == null || - candidatePath.Count == 0 || candidateSegments.Count == 0 || !NumericGuard.IsPositiveFinite(maximumCollisionCheckStepMeters)) - { - reason = "平滑候选、原始路径、地图、车辆或碰撞步长无效。"; - return false; - } - - if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvatureMeters)) - { - reason = "车辆曲率约束无效。"; - return false; - } - - if (!TryValidateSegmentTopology(candidatePath, candidateSegments, originalPath, out reason)) return false; - if (Math.Abs(candidatePath[0].ArcLength) > Tolerance) - { - reason = "平滑路径首点的全局弧长必须为零。"; - return false; - } - - var checkedClearances = new double[candidatePath.Count]; - double minimumClearance = double.PositiveInfinity; - for (int index = 0; index < candidatePath.Count; index++) - { - SmoothedPathPoint current = candidatePath[index]; - if (!IsValidPoint(current) || Math.Abs(current.VehicleCurvature) > maximumCurvatureMeters + Tolerance) - { - reason = "平滑路径包含非法数值或超限车辆曲率。"; - return false; - } - - var currentPose = new Pose2D(current.X, current.Y, current.Heading); - if (!_collisionChecker.IsPoseCollisionFree(currentPose, map, vehicle, 0d, out double poseClearance)) - { - reason = "平滑路径点未通过完整车体碰撞或边界复核。"; - return false; - } - - checkedClearances[index] = poseClearance; - minimumClearance = Math.Min(minimumClearance, poseClearance); - if (index == 0) continue; - - SmoothedPathPoint previous = candidatePath[index - 1]; - if (!IsUnwrappedHeadingContinuous(previous, current)) - { - reason = "平滑路径展开航向不连续。"; - return false; - } - - if (IsDuplicatePoseAndArcLength(previous, current)) - { - if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) - { - reason = "相邻重复点不是合法换向对。"; - return false; - } - continue; - } - - if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + Tolerance) - { - reason = "非换向点必须保持正弧长增量,换向点必须保留重复位姿。"; - return false; - } - - var previousPose = new Pose2D(previous.X, previous.Y, previous.Heading); - if (!_collisionChecker.IsSweptMotionCollisionFree(previousPose, currentPose, map, vehicle, - maximumCollisionCheckStepMeters, out double sweptClearance)) - { - reason = "平滑路径相邻点之间的完整车体扫掠碰撞复核失败。"; - return false; - } - - checkedClearances[index - 1] = Math.Min(checkedClearances[index - 1], sweptClearance); - checkedClearances[index] = Math.Min(checkedClearances[index], sweptClearance); - minimumClearance = Math.Min(minimumClearance, sweptClearance); - } - - var output = new List(candidatePath.Count); - for (int index = 0; index < candidatePath.Count; index++) - { - SmoothedPathPoint point = candidatePath[index]; - output.Add(new SmoothedPathPoint( - point.X, point.Y, point.Heading, point.UnwrappedHeading, point.ArcLength, point.Direction, - point.GeometricCurvature, point.VehicleCurvature, point.VehicleCurvatureDerivative, - checkedClearances[index], point.IsGearSwitchPoint, point.Source)); - } - - pathWithClearance = new ReadOnlyCollection(output); - minimumClearanceMeters = minimumClearance; - return true; - } - - private static bool TryValidateSegmentTopology( - IReadOnlyList candidatePath, - IReadOnlyList candidateSegments, - PreparedPath originalPath, - out string reason) - { - reason = string.Empty; - if (originalPath.Segments == null || originalPath.Segments.Count != candidateSegments.Count) - { - reason = "平滑路径方向段数量必须保持原始换向拓扑。"; - return false; - } - - int expectedStartIndex = 0; - for (int segmentIndex = 0; segmentIndex < candidateSegments.Count; segmentIndex++) - { - SmoothedPathSegment candidateSegment = candidateSegments[segmentIndex]; - PreparedDirectionSegment originalSegment = originalPath.Segments[segmentIndex]; - if (candidateSegment == null || originalSegment == null || candidateSegment.SegmentIndex != segmentIndex || - candidateSegment.StartIndex != expectedStartIndex || candidateSegment.StartIndex < 0 || - candidateSegment.EndIndex < candidateSegment.StartIndex || candidateSegment.EndIndex >= candidatePath.Count || - candidateSegment.Direction != originalSegment.Direction || - candidateSegment.StartsAtGearSwitch != originalSegment.StartsAtGearSwitch || - candidateSegment.EndsAtGearSwitch != originalSegment.EndsAtGearSwitch || - originalSegment.Points == null || originalSegment.Points.Count == 0) - { - reason = "平滑路径方向段索引、方向或换向拓扑无效。"; - return false; - } - - SmoothedPathPoint candidateStart = candidatePath[candidateSegment.StartIndex]; - SmoothedPathPoint candidateEnd = candidatePath[candidateSegment.EndIndex]; - SmoothingPoint2D originalStart = originalSegment.Points[0]; - SmoothingPoint2D originalEnd = originalSegment.Points[originalSegment.Points.Count - 1]; - if (!SamePose(candidateStart, originalStart) || !SamePose(candidateEnd, originalEnd) || - candidateStart.Direction != originalSegment.Direction || candidateEnd.Direction != originalSegment.Direction || - candidateStart.IsGearSwitchPoint != originalStart.IsGearSwitchPoint || - candidateEnd.IsGearSwitchPoint != originalEnd.IsGearSwitchPoint) - { - reason = "平滑路径改变了原始方向段端点或换向点。"; - return false; - } - - for (int pointIndex = candidateSegment.StartIndex; pointIndex <= candidateSegment.EndIndex; pointIndex++) - { - if (candidatePath[pointIndex] == null || candidatePath[pointIndex].Direction != originalSegment.Direction) - { - reason = "平滑路径方向段包含与段方向不一致的点。"; - return false; - } - } - - expectedStartIndex = candidateSegment.EndIndex + 1; - } - - if (expectedStartIndex != candidatePath.Count) - { - reason = "平滑路径方向段未完整覆盖候选点。"; - return false; - } - return true; - } - - private static bool IsValidPoint(SmoothedPathPoint point) - { - return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && - NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && - NumericGuard.IsFinite(point.ArcLength) && point.ArcLength >= 0d && - NumericGuard.IsFinite(point.GeometricCurvature) && NumericGuard.IsFinite(point.VehicleCurvature) && - NumericGuard.IsFinite(point.VehicleCurvatureDerivative) && - IsDirection(point.Direction) && Enum.IsDefined(typeof(SmoothedPathPointSource), point.Source) && - Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= Tolerance; - } - - private static bool SamePose(SmoothedPathPoint candidate, SmoothingPoint2D original) - { - return candidate != null && original != null && Math.Abs(candidate.X - original.X) <= Tolerance && - Math.Abs(candidate.Y - original.Y) <= Tolerance && - Math.Abs(AngleMath.ShortestSignedDifference(candidate.Heading, original.Heading)) <= Tolerance; - } - - private static bool IsUnwrappedHeadingContinuous(SmoothedPathPoint previous, SmoothedPathPoint current) - { - double expectedDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); - return NumericGuard.IsFinite(expectedDelta) && - Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedDelta) <= Tolerance; - } - - private static bool IsDuplicatePoseAndArcLength(SmoothedPathPoint previous, SmoothedPathPoint current) - { - return Math.Abs(previous.X - current.X) <= Tolerance && Math.Abs(previous.Y - current.Y) <= Tolerance && - Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= Tolerance && - Math.Abs(previous.ArcLength - current.ArcLength) <= Tolerance; - } - - private static bool IsDirection(TravelDirection direction) - { - return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; - } - - private static IReadOnlyList EmptyPath() - { - return new ReadOnlyCollection(new List()); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs deleted file mode 100644 index 01271f2..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 路径平滑对比报告使用的固定 IEEE 风格物理尺寸和色卡。 -public static class IeeeFigureStyle -{ - /// 双栏宽度,单位 pt。 - public const double FigureWidthPoints = 7.16d * 72d; - - /// 图形高度,单位 pt。 - public const double FigureHeightPoints = 5.20d * 72d; - - /// 原始粗路径颜色。 - public const string RawColor = "#4D4D4D"; - - /// 三次 B 样条颜色。 - public const string BSplineColor = "#0072B2"; - - /// 局部三次 Bézier 颜色。 - public const string BezierColor = "#D55E00"; - - /// 分段五次路径颜色。 - public const string QuinticColor = "#009E73"; - - /// 曲率限制颜色。 - public const string LimitColor = "#CC79A7"; -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs deleted file mode 100644 index c324a35..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 把共享图形模型的指标表写为带 BOM 的 UTF-8 CSV。 -public sealed class SmoothingCsvWriter -{ - private const string Header = "ScenarioId,Method,Status,PathLengthMeters,MaximumAbsoluteVehicleCurvaturePerMeter,RootMeanSquareVehicleCurvaturePerMeter,TotalAbsoluteCurvatureVariationPerMeter,CurvatureVariationEnergy,MinimumBodyClearanceMeters,MedianElapsedMilliseconds,TimingSampleCount,IsDeterministic,RetryCount,AcceptedStrength"; - - public byte[] Write(SmoothingFigureModel model) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - var text = new StringBuilder(Header).Append("\r\n"); - for (int index = 0; index < model.MetricRows.Count; index++) - { - SmoothingFigureMetricRow row = model.MetricRows[index]; - PathQualityMetrics metrics = row.Metrics; - text.Append(Field(model.ScenarioId)).Append(',').Append(Field(row.Method)).Append(',').Append(Field(row.Status.ToString())).Append(',') - .Append(Number(metrics.PathLengthMeters)).Append(',').Append(Number(metrics.MaximumAbsoluteVehicleCurvaturePerMeter)).Append(',') - .Append(Number(metrics.RootMeanSquareVehicleCurvaturePerMeter)).Append(',').Append(Number(metrics.TotalAbsoluteCurvatureVariationPerMeter)).Append(',') - .Append(Number(metrics.CurvatureVariationEnergy)).Append(',').Append(Number(metrics.MinimumBodyClearanceMeters)).Append(',') - .Append(Number(row.Timing == null ? 0d : row.Timing.MedianElapsedMilliseconds)).Append(',') - .Append(row.Timing == null ? 0 : row.Timing.MeasuredElapsedMilliseconds.Count).Append(',') - .Append(row.Timing != null && row.Timing.IsDeterministic ? "true" : "false").Append(',').Append(row.RetryCount).Append(',') - .Append(Number(row.AcceptedStrength)).Append("\r\n"); - } - byte[] body = new UTF8Encoding(false).GetBytes(text.ToString()); - byte[] preamble = new UTF8Encoding(true).GetPreamble(); - var output = new byte[preamble.Length + body.Length]; - Buffer.BlockCopy(preamble, 0, output, 0, preamble.Length); - Buffer.BlockCopy(body, 0, output, preamble.Length, body.Length); - return output; - } - - private static string Number(double value) { return value.ToString("0.#################", CultureInfo.InvariantCulture); } - private static string Field(string value) - { - string text = value ?? string.Empty; - return text.IndexOfAny(new[] { ',', '\"', '\r', '\n' }) < 0 ? text : "\"" + text.Replace("\"", "\"\"") + "\""; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs deleted file mode 100644 index 61dfb11..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 单张 SVG/PNG 共享的不可变绘图视图。 -public sealed class SmoothingFigureDefinition -{ - internal SmoothingFigureDefinition( - SmoothingFigureKind kind, - string fileStem, - string title, - SmoothingFigureModel model, - bool showsMapContext, - IReadOnlyList series, - double worldXMinMeters, - double worldXMaxMeters, - double worldYMinMeters, - double worldYMaxMeters, - IReadOnlyList xTicks, - IReadOnlyList yTicks, - double curvatureArcLengthMaximumMeters, - double curvatureMinimumPerMeter, - double curvatureMaximumPerMeter, - IReadOnlyList curvatureArcLengthTicks, - IReadOnlyList curvatureTicks) - { - Kind = kind; - FileStem = fileStem ?? string.Empty; - Title = title ?? string.Empty; - Model = model ?? throw new ArgumentNullException(nameof(model)); - ShowsMapContext = showsMapContext; - Series = Copy(series); - WorldXMinMeters = worldXMinMeters; - WorldXMaxMeters = worldXMaxMeters; - WorldYMinMeters = worldYMinMeters; - WorldYMaxMeters = worldYMaxMeters; - XTicks = Copy(xTicks); - YTicks = Copy(yTicks); - CurvatureArcLengthMaximumMeters = curvatureArcLengthMaximumMeters; - CurvatureMinimumPerMeter = curvatureMinimumPerMeter; - CurvatureMaximumPerMeter = curvatureMaximumPerMeter; - CurvatureArcLengthTicks = Copy(curvatureArcLengthTicks); - CurvatureTicks = Copy(curvatureTicks); - } - - public SmoothingFigureKind Kind { get; } - public string FileStem { get; } - public string Title { get; } - public SmoothingFigureModel Model { get; } - public bool ShowsMapContext { get; } - public bool IsCurvatureFigure => Kind == SmoothingFigureKind.CurvatureComparison; - public double FigureWidthPoints => Model.FigureWidthPoints; - public double FigureHeightPoints => Model.FigureHeightPoints; - public double PlotXPoints => 68d; - public double PlotYPoints => 44d; - public double PlotWidthPoints => 400d; - public double PlotHeightPoints => 245d; - public double LegendYPoints => 340d; - public IReadOnlyList Series { get; } - public IReadOnlyList LegendEntries => BuildLegend(Series); - public double WorldXMinMeters { get; } - public double WorldXMaxMeters { get; } - public double WorldYMinMeters { get; } - public double WorldYMaxMeters { get; } - public double WorldScalePointsPerMeter => Math.Min(PlotWidthPoints / (WorldXMaxMeters - WorldXMinMeters), PlotHeightPoints / (WorldYMaxMeters - WorldYMinMeters)); - public IReadOnlyList XTicks { get; } - public IReadOnlyList YTicks { get; } - public double CurvatureArcLengthMaximumMeters { get; } - public double CurvatureMinimumPerMeter { get; } - public double CurvatureMaximumPerMeter { get; } - public IReadOnlyList CurvatureArcLengthTicks { get; } - public IReadOnlyList CurvatureTicks { get; } - - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } - - private static IReadOnlyList BuildLegend(IReadOnlyList views) - { - var entries = new List(views == null ? 0 : views.Count); - if (views != null) - { - for (int index = 0; index < views.Count; index++) - { - SmoothingFigureSeries series = views[index].Series; - entries.Add(new SmoothingFigureLegendEntry(series.Label + " (" + series.Status + ")", series.Color, string.Empty)); - } - } - return new ReadOnlyCollection(entries); - } -} - -/// 某条路径在特定图中的显示透明度。 -public sealed class SmoothingFigureSeriesView -{ - internal SmoothingFigureSeriesView(SmoothingFigureSeries series, double opacity) - { - Series = series ?? throw new ArgumentNullException(nameof(series)); - Opacity = opacity < 0d ? 0d : (opacity > 1d ? 1d : opacity); - } - - public SmoothingFigureSeries Series { get; } - public double Opacity { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs deleted file mode 100644 index e55203b..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 每个场景稳定发布的六种报告图。 -public enum SmoothingFigureKind -{ - CoarsePathOverview, - AllPathsComparison, - CubicBSplineOverview, - LocalCubicBezierOverview, - PiecewiseQuinticOverview, - CurvatureComparison, -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs deleted file mode 100644 index 22ebd8d..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// SVG 和 PNG 共享的不可变路径平滑报告图形模型。 -public sealed class SmoothingFigureModel -{ - internal SmoothingFigureModel( - string scenarioId, - string scenarioLabel, - double worldXMinMeters, - double worldXMaxMeters, - double worldYMinMeters, - double worldYMaxMeters, - double pathPanelX, - double pathPanelY, - double pathPanelWidth, - double pathPanelHeight, - double curvaturePanelX, - double curvaturePanelY, - double curvaturePanelWidth, - double curvaturePanelHeight, - double metricsPanelX, - double metricsPanelY, - double metricsPanelWidth, - double metricsPanelHeight, - IReadOnlyList obstacles, - IReadOnlyList series, - IReadOnlyList metricRows, - SmoothingFigurePoint start, - SmoothingFigurePoint goal) - { - ScenarioId = scenarioId ?? string.Empty; - ScenarioLabel = scenarioLabel ?? string.Empty; - WorldXMinMeters = worldXMinMeters; - WorldXMaxMeters = worldXMaxMeters; - WorldYMinMeters = worldYMinMeters; - WorldYMaxMeters = worldYMaxMeters; - PathPanelX = pathPanelX; - PathPanelY = pathPanelY; - PathPanelWidth = pathPanelWidth; - PathPanelHeight = pathPanelHeight; - CurvaturePanelX = curvaturePanelX; - CurvaturePanelY = curvaturePanelY; - CurvaturePanelWidth = curvaturePanelWidth; - CurvaturePanelHeight = curvaturePanelHeight; - MetricsPanelX = metricsPanelX; - MetricsPanelY = metricsPanelY; - MetricsPanelWidth = metricsPanelWidth; - MetricsPanelHeight = metricsPanelHeight; - Obstacles = Copy(obstacles); - Series = Copy(series); - MetricRows = Copy(metricRows); - Start = start ?? throw new ArgumentNullException(nameof(start)); - Goal = goal ?? throw new ArgumentNullException(nameof(goal)); - } - - public string ScenarioId { get; } - public string ScenarioLabel { get; } - public double FigureWidthPoints => IeeeFigureStyle.FigureWidthPoints; - public double FigureHeightPoints => IeeeFigureStyle.FigureHeightPoints; - public string PathPanelLabel => "(a)"; - public string CurvaturePanelLabel => "(b)"; - public string MetricsPanelLabel => "(c)"; - public double WorldXMinMeters { get; } - public double WorldXMaxMeters { get; } - public double WorldYMinMeters { get; } - public double WorldYMaxMeters { get; } - public double PathPanelX { get; } - public double PathPanelY { get; } - public double PathPanelWidth { get; } - public double PathPanelHeight { get; } - public double CurvaturePanelX { get; } - public double CurvaturePanelY { get; } - public double CurvaturePanelWidth { get; } - public double CurvaturePanelHeight { get; } - public double MetricsPanelX { get; } - public double MetricsPanelY { get; } - public double MetricsPanelWidth { get; } - public double MetricsPanelHeight { get; } - public double PathScaleX { get; internal set; } - public double PathScaleY { get; internal set; } - public IReadOnlyList Obstacles { get; } - public IReadOnlyList Series { get; } - public IReadOnlyList LegendEntries => BuildLegend(Series); - public IReadOnlyList MetricRows { get; } - public SmoothingFigurePoint Start { get; } - public SmoothingFigurePoint Goal { get; } - - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } - - private static IReadOnlyList BuildLegend(IReadOnlyList series) - { - var legend = new List(series == null ? 0 : series.Count); - if (series != null) - { - for (int index = 0; index < series.Count; index++) - legend.Add(new SmoothingFigureLegendEntry(series[index].Label, series[index].Color, series[index].DashArray)); - } - return new ReadOnlyCollection(legend); - } -} - -public sealed class SmoothingFigurePoint -{ - public SmoothingFigurePoint(double xMeters, double yMeters, double arcLengthMeters, double vehicleCurvaturePerMeter) - { - X = xMeters; Y = yMeters; ArcLength = arcLengthMeters; VehicleCurvature = vehicleCurvaturePerMeter; - } - public double X { get; } - public double Y { get; } - public double ArcLength { get; } - public double VehicleCurvature { get; } -} - -public sealed class SmoothingFigureObstacle -{ - public SmoothingFigureObstacle(double xMeters, double yMeters, double widthMeters, double heightMeters) - { - X = xMeters; Y = yMeters; Width = widthMeters; Height = heightMeters; - } - public double X { get; } - public double Y { get; } - public double Width { get; } - public double Height { get; } -} - -public sealed class SmoothingFigureSeries -{ - internal SmoothingFigureSeries(SmoothingMethod? method, string key, string label, PathSmoothingStatus status, string color, string dashArray, - bool isRawPathBaseline, IReadOnlyList points, IReadOnlyList violationMarkers) - { - Method = method; Key = key ?? string.Empty; Label = label ?? string.Empty; Status = status; Color = color ?? string.Empty; - DashArray = dashArray ?? string.Empty; IsRawPathBaseline = isRawPathBaseline; Points = Copy(points); ViolationMarkers = Copy(violationMarkers); - } - public SmoothingMethod? Method { get; } - public string Key { get; } - public string Label { get; } - public PathSmoothingStatus Status { get; } - public string Color { get; } - public string DashArray { get; } - public bool IsRawPathBaseline { get; } - public bool IsCurveVisible => Points.Count > 0; - public IReadOnlyList Points { get; } - public IReadOnlyList ViolationMarkers { get; } - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); - return new ReadOnlyCollection(copy); - } -} - -public sealed class SmoothingFigureLegendEntry -{ - internal SmoothingFigureLegendEntry(string label, string color, string dashArray) { Label = label ?? string.Empty; Color = color ?? string.Empty; DashArray = dashArray ?? string.Empty; } - public string Label { get; } - public string Color { get; } - public string DashArray { get; } -} - -public sealed class SmoothingFigureMetricRow -{ - internal SmoothingFigureMetricRow(string method, string label, PathSmoothingStatus status, PathQualityMetrics metrics, - SmoothingTimingSummary timing, int retryCount, double acceptedStrength) - { - Method = method ?? string.Empty; Label = label ?? string.Empty; Status = status; Metrics = metrics ?? new PathQualityMetrics(); - Timing = timing; RetryCount = retryCount < 0 ? 0 : retryCount; AcceptedStrength = acceptedStrength; - } - public string Method { get; } - public string Label { get; } - public PathSmoothingStatus Status { get; } - public PathQualityMetrics Metrics { get; } - public SmoothingTimingSummary Timing { get; } - public int RetryCount { get; } - public double AcceptedStrength { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs deleted file mode 100644 index cbd48b3..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 将不可变的比较结果、地图和端点转换为共享报告图形模型。 -public sealed class SmoothingFigureModelBuilder -{ - private const double MarginPoints = 18d; - private const double PanelGapPoints = 12d; - - public SmoothingFigureModel Build( - PathSmoothingComparisonResult comparison, - PlanningGridMap map, - Pose2D start, - Pose2D goal, - string scenarioId, - string scenarioLabel) - { - if (comparison == null) throw new ArgumentNullException(nameof(comparison)); - if (map == null) throw new ArgumentNullException(nameof(map)); - if (start == null) throw new ArgumentNullException(nameof(start)); - if (goal == null) throw new ArgumentNullException(nameof(goal)); - - double worldXMin = map.Bounds.XMin / 1000d; - double worldXMax = map.Bounds.XMax / 1000d; - double worldYMin = map.Bounds.YMin / 1000d; - double worldYMax = map.Bounds.YMax / 1000d; - double innerWidth = IeeeFigureStyle.FigureWidthPoints - 2d * MarginPoints; - double innerHeight = IeeeFigureStyle.FigureHeightPoints - 2d * MarginPoints; - double pathWidth = innerWidth * 0.60d; - double rightWidth = innerWidth - pathWidth - PanelGapPoints; - double rightHeight = (innerHeight - PanelGapPoints) / 2d; - - var model = new SmoothingFigureModel( - scenarioId, scenarioLabel, worldXMin, worldXMax, worldYMin, worldYMax, - MarginPoints, MarginPoints, pathWidth, innerHeight, - MarginPoints + pathWidth + PanelGapPoints, MarginPoints, rightWidth, rightHeight, - MarginPoints + pathWidth + PanelGapPoints, MarginPoints + rightHeight + PanelGapPoints, rightWidth, rightHeight, - BuildObstacles(map), BuildSeries(comparison, map), BuildMetricRows(comparison), - new SmoothingFigurePoint(start.X, start.Y, 0d, 0d), new SmoothingFigurePoint(goal.X, goal.Y, 0d, 0d)); - - double worldWidth = worldXMax - worldXMin; - double worldHeight = worldYMax - worldYMin; - if (worldWidth <= 0d || worldHeight <= 0d) throw new ArgumentOutOfRangeException(nameof(map), "地图边界必须为非退化有限范围。"); - double scale = Math.Min(pathWidth / worldWidth, innerHeight / worldHeight); - model.PathScaleX = scale; - model.PathScaleY = scale; - return model; - } - - private static IReadOnlyList BuildObstacles(PlanningGridMap map) - { - var runs = new List(); - double resolution = map.ResolutionMeters; - double xMin = map.Bounds.XMin / 1000d; - double yMin = map.Bounds.YMin / 1000d; - for (int row = 0; row < map.Rows; row++) - { - int runStart = -1; - for (int column = 0; column <= map.Cols; column++) - { - bool occupied = column < map.Cols && map.IsOccupied(row, column); - if (occupied && runStart < 0) { runStart = column; continue; } - if (!occupied && runStart >= 0) - { - AddOrExtendRun(runs, xMin + runStart * resolution, yMin + row * resolution, - (column - runStart) * resolution, resolution); - runStart = -1; - } - } - } - var obstacles = new List(runs.Count); - for (int index = 0; index < runs.Count; index++) - { - ObstacleRun run = runs[index]; - obstacles.Add(new SmoothingFigureObstacle(run.X, run.Y, run.Width, run.Height)); - } - return obstacles; - } - - private static void AddOrExtendRun(IList runs, double x, double y, double width, double height) - { - for (int index = runs.Count - 1; index >= 0; index--) - { - ObstacleRun candidate = runs[index]; - if (NearlyEqual(candidate.X, x) && NearlyEqual(candidate.Width, width) && NearlyEqual(candidate.Y + candidate.Height, y)) - { - candidate.Height += height; - return; - } - } - runs.Add(new ObstacleRun(x, y, width, height)); - } - - private static bool NearlyEqual(double first, double second) { return Math.Abs(first - second) < 1e-9d; } - - private static IReadOnlyList BuildSeries(PathSmoothingComparisonResult comparison, PlanningGridMap map) - { - var series = new List(4) - { - CreateSeries(comparison.RawPathBaseline, null, "raw", "原始粗路径", IeeeFigureStyle.RawColor, string.Empty, true, map) - }; - series.Add(CreateSeries(Find(comparison, SmoothingMethod.CubicBSpline), SmoothingMethod.CubicBSpline, "bspline", "三次 B 样条", IeeeFigureStyle.BSplineColor, string.Empty, false, map)); - series.Add(CreateSeries(Find(comparison, SmoothingMethod.LocalCubicBezier), SmoothingMethod.LocalCubicBezier, "bezier", "局部三次 Bézier", IeeeFigureStyle.BezierColor, string.Empty, false, map)); - series.Add(CreateSeries(Find(comparison, SmoothingMethod.PiecewiseQuintic), SmoothingMethod.PiecewiseQuintic, "quintic", "分段五次", IeeeFigureStyle.QuinticColor, string.Empty, false, map)); - return series; - } - - private static IReadOnlyList BuildMetricRows(PathSmoothingComparisonResult comparison) - { - return new List - { - CreateRow(comparison.RawPathBaseline, "RawPath", "原始粗路径"), - CreateRow(Find(comparison, SmoothingMethod.CubicBSpline), "CubicBSpline", "三次 B 样条"), - CreateRow(Find(comparison, SmoothingMethod.LocalCubicBezier), "LocalCubicBezier", "局部三次 Bézier"), - CreateRow(Find(comparison, SmoothingMethod.PiecewiseQuintic), "PiecewiseQuintic", "分段五次") - }; - } - - private static PathSmoothingComparisonEntry Find(PathSmoothingComparisonResult comparison, SmoothingMethod method) - { - for (int index = 0; index < comparison.Entries.Count; index++) - { - PathSmoothingComparisonEntry entry = comparison.Entries[index]; - if (entry.Method == method) return entry; - } - return null; - } - - private static SmoothingFigureMetricRow CreateRow(PathSmoothingComparisonEntry entry, string method, string label) - { - return entry == null - ? new SmoothingFigureMetricRow(method, label, PathSmoothingStatus.Failed, new PathQualityMetrics(), null, 0, 0d) - : new SmoothingFigureMetricRow(method, label, entry.Status, entry.Metrics, entry.Timing, entry.RetryCount, entry.AcceptedStrength); - } - - private static SmoothingFigureSeries CreateSeries(PathSmoothingComparisonEntry entry, SmoothingMethod? method, string key, string label, - string color, string dashArray, bool isRawPathBaseline, PlanningGridMap map) - { - var points = new List(); - var violations = new List(); - PathSmoothingStatus status = entry == null ? PathSmoothingStatus.Failed : entry.Status; - if (entry != null) - { - for (int index = 0; index < entry.Path.Count; index++) - { - SmoothedPathPoint point = entry.Path[index]; - var figurePoint = new SmoothingFigurePoint(point.X, point.Y, point.ArcLength, point.VehicleCurvature); - points.Add(figurePoint); - if (status == PathSmoothingStatus.Infeasible && map.IsOccupiedWorld(point.X, point.Y)) violations.Add(figurePoint); - } - } - if (status == PathSmoothingStatus.Infeasible && points.Count > 0 && violations.Count == 0) - violations.Add(points[points.Count / 2]); - return new SmoothingFigureSeries(method, key, label, status, color, dashArray, isRawPathBaseline, points, violations); - } - - private sealed class ObstacleRun - { - public ObstacleRun(double x, double y, double width, double height) { X = x; Y = y; Width = width; Height = height; } - public double X { get; } - public double Y { get; } - public double Width { get; } - public double Height { get; set; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs deleted file mode 100644 index 00d3e82..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 从一份比较模型构建六张含义固定、相机聚焦的报告图。 -public sealed class SmoothingFigureSetBuilder -{ - private const double MinimumExtentMeters = 0.25d; - private const double PaddingFraction = 0.10d; - - public SmoothingFigureSet Build(SmoothingFigureModel model) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - SmoothingFigureSeries raw = Find(model, "raw"); - SmoothingFigureSeries bSpline = Find(model, "bspline"); - SmoothingFigureSeries bezier = Find(model, "bezier"); - SmoothingFigureSeries quintic = Find(model, "quintic"); - var figures = new List - { - BuildOverhead(SmoothingFigureKind.CoarsePathOverview, "01-coarse-path-overview", "Hybrid A* 原始粗路径", model, true, View(raw, 1d)), - BuildOverhead(SmoothingFigureKind.AllPathsComparison, "02-all-paths-comparison", "路径平滑结果对比", model, false, View(raw, 1d), View(bSpline, 1d), View(bezier, 1d), View(quintic, 1d)), - BuildOverhead(SmoothingFigureKind.CubicBSplineOverview, "03-cubic-bspline-overview", "三次 B 样条平滑", model, true, View(raw, 0.28d), View(bSpline, 1d)), - BuildOverhead(SmoothingFigureKind.LocalCubicBezierOverview, "04-local-cubic-bezier-overview", "局部三次 Bézier 平滑", model, true, View(raw, 0.28d), View(bezier, 1d)), - BuildOverhead(SmoothingFigureKind.PiecewiseQuinticOverview, "05-piecewise-quintic-overview", "分段五次平滑", model, true, View(raw, 0.28d), View(quintic, 1d)), - BuildCurvature(model, View(raw, 1d), View(bSpline, 1d), View(bezier, 1d), View(quintic, 1d)), - }; - return new SmoothingFigureSet(figures); - } - - private static SmoothingFigureDefinition BuildOverhead(SmoothingFigureKind kind, string stem, string title, SmoothingFigureModel model, bool mapContext, params SmoothingFigureSeriesView[] series) - { - Bounds bounds = CalculateWorldBounds(model, mapContext, series); - return new SmoothingFigureDefinition(kind, stem, title, model, mapContext, series, - bounds.XMin, bounds.XMax, bounds.YMin, bounds.YMax, - BuildTicks(bounds.XMin, bounds.XMax), BuildTicks(bounds.YMin, bounds.YMax), - 1d, -1d, 1d, Array.Empty(), Array.Empty()); - } - - private static SmoothingFigureDefinition BuildCurvature(SmoothingFigureModel model, params SmoothingFigureSeriesView[] series) - { - double arcMaximum = 0d; - double curvatureMinimum = 0d; - double curvatureMaximum = 0d; - for (int viewIndex = 0; viewIndex < series.Length; viewIndex++) - { - IReadOnlyList points = series[viewIndex].Series.Points; - for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) - { - SmoothingFigurePoint point = points[pointIndex]; - if (point.ArcLength > arcMaximum) arcMaximum = point.ArcLength; - if (point.VehicleCurvature < curvatureMinimum) curvatureMinimum = point.VehicleCurvature; - if (point.VehicleCurvature > curvatureMaximum) curvatureMaximum = point.VehicleCurvature; - } - } - arcMaximum = ExpandMaximum(arcMaximum, MinimumExtentMeters); - ExpandRange(ref curvatureMinimum, ref curvatureMaximum, MinimumExtentMeters); - return new SmoothingFigureDefinition(SmoothingFigureKind.CurvatureComparison, "06-curvature-comparison", "车辆曲率对比", model, false, series, - 0d, 1d, 0d, 1d, Array.Empty(), Array.Empty(), - arcMaximum, curvatureMinimum, curvatureMaximum, BuildTicks(0d, arcMaximum), BuildTicks(curvatureMinimum, curvatureMaximum)); - } - - private static Bounds CalculateWorldBounds(SmoothingFigureModel model, bool includesEndpoints, IReadOnlyList series) - { - bool hasPoint = false; - double xMin = 0d, xMax = 0d, yMin = 0d, yMax = 0d; - for (int viewIndex = 0; viewIndex < series.Count; viewIndex++) - { - IReadOnlyList points = series[viewIndex].Series.Points; - for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) Include(points[pointIndex].X, points[pointIndex].Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); - } - if (includesEndpoints) - { - Include(model.Start.X, model.Start.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); - Include(model.Goal.X, model.Goal.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); - } - if (!hasPoint) - { - xMin = model.WorldXMinMeters; xMax = model.WorldXMaxMeters; - yMin = model.WorldYMinMeters; yMax = model.WorldYMaxMeters; - } - ExpandRange(ref xMin, ref xMax, MinimumExtentMeters); - ExpandRange(ref yMin, ref yMax, MinimumExtentMeters); - double xPadding = (xMax - xMin) * PaddingFraction; - double yPadding = (yMax - yMin) * PaddingFraction; - xMin -= xPadding; xMax += xPadding; yMin -= yPadding; yMax += yPadding; - double desiredAspect = 400d / 245d; - double width = xMax - xMin; - double height = yMax - yMin; - if (width / height < desiredAspect) - { - double halfWidth = height * desiredAspect / 2d; - double center = (xMin + xMax) / 2d; - xMin = center - halfWidth; xMax = center + halfWidth; - } - else - { - double halfHeight = width / desiredAspect / 2d; - double center = (yMin + yMax) / 2d; - yMin = center - halfHeight; yMax = center + halfHeight; - } - return new Bounds(xMin, xMax, yMin, yMax); - } - - private static void Include(double x, double y, ref bool hasPoint, ref double xMin, ref double xMax, ref double yMin, ref double yMax) - { - if (!hasPoint) { hasPoint = true; xMin = xMax = x; yMin = yMax = y; return; } - if (x < xMin) xMin = x; - if (x > xMax) xMax = x; - if (y < yMin) yMin = y; - if (y > yMax) yMax = y; - } - - private static void ExpandRange(ref double minimum, ref double maximum, double minimumExtent) - { - double extent = maximum - minimum; - if (extent >= minimumExtent) return; - double center = (minimum + maximum) / 2d; - minimum = center - minimumExtent / 2d; - maximum = center + minimumExtent / 2d; - } - - private static double ExpandMaximum(double value, double minimum) { return value < minimum ? minimum : value * (1d + PaddingFraction); } - - private static IReadOnlyList BuildTicks(double minimum, double maximum) - { - double span = maximum - minimum; - if (span <= 0d) return new[] { minimum, maximum }; - double roughStep = span / 5d; - double magnitude = Math.Pow(10d, Math.Floor(Math.Log10(roughStep))); - double normalized = roughStep / magnitude; - double nice = normalized <= 1d ? 1d : (normalized <= 2d ? 2d : (normalized <= 5d ? 5d : 10d)); - double step = nice * magnitude; - var ticks = new List(); - double first = Math.Ceiling(minimum / step) * step; - for (double value = first; value <= maximum + step * 0.001d; value += step) ticks.Add(value); - if (ticks.Count < 2) { ticks.Clear(); ticks.Add(minimum); ticks.Add(maximum); } - return new ReadOnlyCollection(ticks); - } - - private static SmoothingFigureSeries Find(SmoothingFigureModel model, string key) - { - for (int index = 0; index < model.Series.Count; index++) if (model.Series[index].Key == key) return model.Series[index]; - throw new InvalidOperationException("比较图形模型缺少路径序列: " + key); - } - - private static SmoothingFigureSeriesView View(SmoothingFigureSeries series, double opacity) { return new SmoothingFigureSeriesView(series, opacity); } - - private readonly struct Bounds - { - public Bounds(double xMin, double xMax, double yMin, double yMax) { XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; } - public double XMin { get; } - public double XMax { get; } - public double YMin { get; } - public double YMax { get; } - } -} - -/// 六张图的稳定顺序。 -public sealed class SmoothingFigureSet -{ - internal SmoothingFigureSet(IReadOnlyList figures) - { - var copy = new List(figures == null ? 0 : figures.Count); - if (figures != null) for (int index = 0; index < figures.Count; index++) copy.Add(figures[index]); - Figures = new ReadOnlyCollection(copy); - } - - public IReadOnlyList Figures { get; } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs deleted file mode 100644 index 61299d7..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Text; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 按准确族名解析报告所需字体,并提供混合中英文文本的共同基线度量。 -public sealed class SmoothingFontResolver -{ - public bool TryResolve( - string chineseFamilyName, - string latinFamilyName, - out SmoothingFontResolution resolution, - out string reason) - { - resolution = null; - reason = string.Empty; - if (string.IsNullOrWhiteSpace(chineseFamilyName) || string.IsNullOrWhiteSpace(latinFamilyName)) - { - reason = "中文和拉丁字体族名均不能为空。"; - return false; - } - - var collection = new InstalledFontCollection(); - FontFamily chinese = FindExact(collection.Families, chineseFamilyName); - FontFamily latin = FindExact(collection.Families, latinFamilyName); - if (chinese == null || latin == null) - { - collection.Dispose(); - reason = "未安装报告所需的精确字体族:" + (chinese == null ? chineseFamilyName : latinFamilyName) + "。"; - return false; - } - - resolution = new SmoothingFontResolution(collection, chinese, latin, chineseFamilyName, latinFamilyName); - return true; - } - - public RectangleF MeasureMixedText(SmoothingFontResolution resolution, string text, float points) - { - if (resolution == null) throw new ArgumentNullException(nameof(resolution)); - if (string.IsNullOrEmpty(text) || points <= 0f) return RectangleF.Empty; - using (var bitmap = new Bitmap(1, 1)) - using (Graphics graphics = Graphics.FromImage(bitmap)) - using (var format = (StringFormat)StringFormat.GenericTypographic.Clone()) - { - format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; - float width = 0f, height = 0f; - foreach (TextRun run in SplitRuns(text)) - { - using (Font font = resolution.CreateFont(run.IsChinese ? resolution.ChineseFamily : resolution.LatinFamily, points)) - { - SizeF size = graphics.MeasureString(run.Text, font, PointF.Empty, format); - width += size.Width; - height = Math.Max(height, size.Height); - } - } - return new RectangleF(0f, 0f, width, height); - } - } - - internal static IReadOnlyList SplitRuns(string text) - { - var runs = new List(); - if (string.IsNullOrEmpty(text)) return runs; - int start = 0; - bool isChinese = IsChinese(text[0]); - for (int index = 1; index < text.Length; index++) - { - bool currentIsChinese = IsChinese(text[index]); - if (currentIsChinese == isChinese) continue; - runs.Add(new TextRun(text.Substring(start, index - start), isChinese)); - start = index; - isChinese = currentIsChinese; - } - runs.Add(new TextRun(text.Substring(start), isChinese)); - return runs; - } - - private static FontFamily FindExact(IReadOnlyList families, string name) - { - for (int index = 0; index < families.Count; index++) - { - FontFamily family = families[index]; - if (string.Equals(family.Name, name, StringComparison.Ordinal) || - string.Equals(family.GetName(1033), name, StringComparison.Ordinal)) return family; - } - return null; - } - - private static bool IsChinese(char value) - { - return (value >= 0x3400 && value <= 0x4dbf) || (value >= 0x4e00 && value <= 0x9fff) || - (value >= 0xf900 && value <= 0xfaff) || value == 0x3002 || value == 0xff0c || value == 0xff1a; - } - - internal sealed class TextRun - { - public TextRun(string text, bool isChinese) { Text = text; IsChinese = isChinese; } - public string Text { get; } - public bool IsChinese { get; } - } -} - -/// 一次报告导出所使用的精确字体族;释放时同时释放字体集合。 -public sealed class SmoothingFontResolution : IDisposable -{ - private readonly InstalledFontCollection _collection; - private readonly string _chineseFamilyName; - private readonly string _latinFamilyName; - - internal SmoothingFontResolution( - InstalledFontCollection collection, - FontFamily chineseFamily, - FontFamily latinFamily, - string chineseFamilyName, - string latinFamilyName) - { - _collection = collection ?? throw new ArgumentNullException(nameof(collection)); - ChineseFamily = chineseFamily ?? throw new ArgumentNullException(nameof(chineseFamily)); - LatinFamily = latinFamily ?? throw new ArgumentNullException(nameof(latinFamily)); - _chineseFamilyName = chineseFamilyName ?? throw new ArgumentNullException(nameof(chineseFamilyName)); - _latinFamilyName = latinFamilyName ?? throw new ArgumentNullException(nameof(latinFamilyName)); - } - - public string ChineseFamilyName => _chineseFamilyName; - public string LatinFamilyName => _latinFamilyName; - internal FontFamily ChineseFamily { get; } - internal FontFamily LatinFamily { get; } - - internal Font CreateFont(FontFamily family, float points) - { - return new Font(family, points, FontStyle.Regular, GraphicsUnit.Point); - } - - public void Dispose() - { - _collection.Dispose(); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs deleted file mode 100644 index 24ac0b8..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Globalization; -using System.IO; -using System.Runtime.InteropServices; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 仅 Windows 运行时使用 GDI+ 将每张共享图形定义渲染为 600 dpi PNG;轨迹仅绘制离散点。 -public sealed class SmoothingPngRenderer -{ - public const int WidthPixels = 4296; - public const int HeightPixels = 3120; - public const uint PixelsPerMeter = 23622u; - private const float PixelsPerPoint = 600f / 72f; - - public byte[] Render(SmoothingFigureModel model, SmoothingFontResolution fonts) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1], fonts); - } - - public byte[] Render(SmoothingFigureDefinition figure, SmoothingFontResolution fonts) - { - if (figure == null) throw new ArgumentNullException(nameof(figure)); - if (fonts == null) throw new ArgumentNullException(nameof(fonts)); - using (var bitmap = new Bitmap(WidthPixels, HeightPixels, PixelFormat.Format32bppArgb)) - { - bitmap.SetResolution(600f, 600f); - using (Graphics graphics = Graphics.FromImage(bitmap)) - { - graphics.Clear(Color.White); - graphics.SmoothingMode = SmoothingMode.AntiAlias; - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - DrawFigure(graphics, figure, fonts); - } - return Encode(bitmap); - } - } - - private static void DrawFigure(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) - { - DrawMixedText(graphics, fonts, figure.Title + " — " + figure.Model.ScenarioLabel, PointX(figure.PlotXPoints), PointY(12d), 12f, Color.Black); - if (figure.IsCurvatureFigure) DrawCurvatureAxes(graphics, figure, fonts); else DrawOverheadAxes(graphics, figure, fonts); - GraphicsState state = graphics.Save(); - graphics.SetClip(new RectangleF(PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints))); - if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawObstacles(graphics, figure); - if (figure.IsCurvatureFigure) DrawCurvaturePoints(graphics, figure); else DrawPathPoints(graphics, figure); - if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawStartGoal(graphics, figure); - graphics.Restore(state); - DrawLegend(graphics, figure, fonts); - } - - private static void DrawOverheadAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) - { - DrawPlotFrame(graphics, figure); - using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint)) - { - for (int index = 0; index < figure.XTicks.Count; index++) - { - float x = WorldX(figure, figure.XTicks[index]); - graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints)); - DrawMixedText(graphics, fonts, Number(figure.XTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black); - } - for (int index = 0; index < figure.YTicks.Count; index++) - { - float y = WorldY(figure, figure.YTicks[index]); - graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); - DrawMixedText(graphics, fonts, Number(figure.YTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black); - } - } - DrawMixedText(graphics, fonts, "X (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 11d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black); - DrawVerticalText(graphics, fonts, "Y (m)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 17d), 10f, Color.Black); - } - - private static void DrawCurvatureAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) - { - DrawPlotFrame(graphics, figure); - using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint)) - { - for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) - { - float x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); - graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints)); - DrawMixedText(graphics, fonts, Number(figure.CurvatureArcLengthTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black); - } - for (int index = 0; index < figure.CurvatureTicks.Count; index++) - { - float y = CurvatureY(figure, figure.CurvatureTicks[index]); - graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); - DrawMixedText(graphics, fonts, Number(figure.CurvatureTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black); - } - } - if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) - { - float y = CurvatureY(figure, 0d); - using (var zero = new Pen(Color.FromArgb(77, 77, 77), 0.65f * PixelsPerPoint)) - graphics.DrawLine(zero, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); - } - DrawMixedText(graphics, fonts, "s (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 10d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black); - DrawVerticalText(graphics, fonts, "κ (m⁻¹)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 22d), 10f, Color.Black); - } - - private static void DrawPlotFrame(Graphics graphics, SmoothingFigureDefinition figure) - { - using (var border = new Pen(Color.Black, 0.75f * PixelsPerPoint)) - graphics.DrawRectangle(border, PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints)); - } - - private static void DrawObstacles(Graphics graphics, SmoothingFigureDefinition figure) - { - using (var fill = new SolidBrush(Color.FromArgb(217, 217, 217))) - using (var outline = new Pen(Color.FromArgb(128, 128, 128), 0.35f * PixelsPerPoint)) - { - for (int index = 0; index < figure.Model.Obstacles.Count; index++) - { - SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; - float x = WorldX(figure, obstacle.X); - float y = WorldY(figure, obstacle.Y + obstacle.Height); - float width = (float)(obstacle.Width * figure.WorldScalePointsPerMeter * PixelsPerPoint); - float height = (float)(obstacle.Height * figure.WorldScalePointsPerMeter * PixelsPerPoint); - graphics.FillRectangle(fill, x, y, width, height); - graphics.DrawRectangle(outline, x, y, width, height); - } - } - } - - private static void DrawPathPoints(Graphics graphics, SmoothingFigureDefinition figure) - { - for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) - { - SmoothingFigureSeriesView view = figure.Series[viewIndex]; - DrawPoints(graphics, view, point => new PointF(WorldX(figure, point.X), WorldY(figure, point.Y))); - DrawViolations(graphics, figure, view); - } - } - - private static void DrawCurvaturePoints(Graphics graphics, SmoothingFigureDefinition figure) - { - for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) - { - SmoothingFigureSeriesView view = figure.Series[viewIndex]; - DrawPoints(graphics, view, point => new PointF(CurvatureX(figure, point.ArcLength), CurvatureY(figure, point.VehicleCurvature))); - } - } - - private static void DrawPoints(Graphics graphics, SmoothingFigureSeriesView view, Func transform) - { - if (!view.Series.IsCurveVisible) return; - using (var fill = new SolidBrush(WithOpacity(ColorFromHex(view.Series.Color), view.Opacity))) - { - float radius = 1.35f * PixelsPerPoint; - for (int index = 0; index < view.Series.Points.Count; index++) - { - PointF point = transform(view.Series.Points[index]); - graphics.FillEllipse(fill, point.X - radius, point.Y - radius, radius * 2f, radius * 2f); - } - } - } - - private static void DrawViolations(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFigureSeriesView view) - { - if (view.Series.ViolationMarkers.Count == 0) return; - using (var marker = new Pen(ColorFromHex(view.Series.Color), 1.1f * PixelsPerPoint)) - { - float radius = 3f * PixelsPerPoint; - for (int index = 0; index < view.Series.ViolationMarkers.Count; index++) - { - SmoothingFigurePoint point = view.Series.ViolationMarkers[index]; - float x = WorldX(figure, point.X), y = WorldY(figure, point.Y); - graphics.DrawLine(marker, x - radius, y - radius, x + radius, y + radius); - graphics.DrawLine(marker, x - radius, y + radius, x + radius, y - radius); - } - } - } - - private static void DrawStartGoal(Graphics graphics, SmoothingFigureDefinition figure) - { - float startX = WorldX(figure, figure.Model.Start.X), startY = WorldY(figure, figure.Model.Start.Y), radius = 3.2f * PixelsPerPoint; - using (var startBrush = new SolidBrush(Color.FromArgb(240, 228, 66))) - using (var border = new Pen(Color.Black, 0.8f * PixelsPerPoint)) - using (var goalBrush = new SolidBrush(ColorFromHex(IeeeFigureStyle.LimitColor))) - { - graphics.FillEllipse(startBrush, startX - radius, startY - radius, radius * 2f, radius * 2f); - graphics.DrawEllipse(border, startX - radius, startY - radius, radius * 2f, radius * 2f); - float goalX = WorldX(figure, figure.Model.Goal.X), goalY = WorldY(figure, figure.Model.Goal.Y), diamond = 4f * PixelsPerPoint; - PointF[] points = { new PointF(goalX, goalY - diamond), new PointF(goalX + diamond, goalY), new PointF(goalX, goalY + diamond), new PointF(goalX - diamond, goalY) }; - graphics.FillPolygon(goalBrush, points); - graphics.DrawPolygon(border, points); - } - } - - private static void DrawLegend(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) - { - const double columnWidth = 198d; - for (int index = 0; index < figure.LegendEntries.Count; index++) - { - SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; - int column = index % 2, row = index / 2; - float x = PointX(figure.PlotXPoints + column * columnWidth); - float y = PointY(figure.LegendYPoints + row * 15d - 3d); - using (var fill = new SolidBrush(ColorFromHex(entry.Color))) graphics.FillEllipse(fill, x, y - 2.2f * PixelsPerPoint, 4.4f * PixelsPerPoint, 4.4f * PixelsPerPoint); - DrawMixedText(graphics, fonts, entry.Label, x + 10f * PixelsPerPoint, y - 5f * PixelsPerPoint, 8.5f, Color.Black); - } - } - - private static void DrawVerticalText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float y, float points, Color color) - { - GraphicsState state = graphics.Save(); - graphics.TranslateTransform(x, y); - graphics.RotateTransform(-90f); - DrawMixedText(graphics, fonts, text, 0f, 0f, points, color); - graphics.Restore(state); - } - - private static void DrawMixedText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float top, float points, Color color) - { - var runs = SmoothingFontResolver.SplitRuns(text); - float emPixels = points * PixelsPerPoint, maxAscent = 0f; - for (int index = 0; index < runs.Count; index++) - { - FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily; - maxAscent = Math.Max(maxAscent, family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular)); - } - float baseline = top + maxAscent; - using (var brush = new SolidBrush(color)) - using (var format = (StringFormat)StringFormat.GenericTypographic.Clone()) - { - format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; - for (int index = 0; index < runs.Count; index++) - { - FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily; - using (Font font = fonts.CreateFont(family, points)) - { - float runTop = baseline - family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular); - graphics.DrawString(runs[index].Text, font, brush, x, runTop, format); - x += graphics.MeasureString(runs[index].Text, font, PointF.Empty, format).Width; - } - } - } - } - - private static byte[] Encode(Bitmap bitmap) - { - Rectangle rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); - BitmapData data = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); - try - { - int sourceLength = checked(data.Stride * bitmap.Height); - var source = new byte[sourceLength]; - Marshal.Copy(data.Scan0, source, 0, source.Length); - var rgba = new byte[checked(bitmap.Width * bitmap.Height * 4)]; - for (int y = 0; y < bitmap.Height; y++) - { - int sourceRow = y * data.Stride, outputRow = y * bitmap.Width * 4; - for (int x = 0; x < bitmap.Width; x++) - { - int sourceOffset = sourceRow + x * 4, outputOffset = outputRow + x * 4; - rgba[outputOffset] = source[sourceOffset + 2]; - rgba[outputOffset + 1] = source[sourceOffset + 1]; - rgba[outputOffset + 2] = source[sourceOffset]; - rgba[outputOffset + 3] = source[sourceOffset + 3]; - } - } - using (var output = new MemoryStream()) - { - ValidatedPngWriter.Write(rgba, bitmap.Width, bitmap.Height, output, PixelsPerMeter); - return output.ToArray(); - } - } - finally { bitmap.UnlockBits(data); } - } - - private static Color ColorFromHex(string value) { return ColorTranslator.FromHtml(value); } - private static Color WithOpacity(Color color, double opacity) { return Color.FromArgb((int)Math.Round(255d * opacity), color.R, color.G, color.B); } - private static float PointX(double points) { return (float)(points * PixelsPerPoint); } - private static float PointY(double points) { return (float)(points * PixelsPerPoint); } - private static float WorldX(SmoothingFigureDefinition figure, double x) { return PointX(figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter); } - private static float WorldY(SmoothingFigureDefinition figure, double y) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter); } - private static float CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return PointX(figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints); } - private static float CurvatureY(SmoothingFigureDefinition figure, double curvature) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints); } - private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs deleted file mode 100644 index 31aa234..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 一次报告导出的共享图形模型、目标目录与精确字体要求。 -public sealed class SmoothingReportExportRequest -{ - public SmoothingFigureModel Model { get; set; } - public string OutputDirectory { get; set; } - public string FileStem { get; set; } - public string ChineseFontFamilyName { get; set; } = "SimSun"; - public string LatinFontFamilyName { get; set; } = "Times New Roman"; -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs deleted file mode 100644 index c4dda77..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 报告导出结果的稳定状态。 -public enum SmoothingReportExportStatus -{ - Success, - InvalidInput, - FontUnavailable, - Failed, -} - -/// 六张 SVG、六张 PNG 与一个 CSV 的发布结果;失败时不返回部分输出路径。 -public sealed class SmoothingReportExportResult -{ - internal SmoothingReportExportResult(SmoothingReportExportStatus status, string reason, IReadOnlyList svgPaths, IReadOnlyList pngPaths, string csvPath) - { - Status = status; - Reason = reason ?? string.Empty; - SvgPaths = Copy(svgPaths); - PngPaths = Copy(pngPaths); - CsvPath = csvPath ?? string.Empty; - } - - public SmoothingReportExportStatus Status { get; } - public string Reason { get; } - public IReadOnlyList SvgPaths { get; } - public IReadOnlyList PngPaths { get; } - public string CsvPath { get; } - - internal static SmoothingReportExportResult Success(IReadOnlyList svgPaths, IReadOnlyList pngPaths, string csvPath) - { - return new SmoothingReportExportResult(SmoothingReportExportStatus.Success, string.Empty, svgPaths, pngPaths, csvPath); - } - - internal static SmoothingReportExportResult Failure(SmoothingReportExportStatus status, string reason) - { - return new SmoothingReportExportResult(status, reason, new string[0], new string[0], string.Empty); - } - - private static IReadOnlyList Copy(IReadOnlyList source) - { - var copy = new List(source == null ? 0 : source.Count); - if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index] ?? string.Empty); - return new ReadOnlyCollection(copy); - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs deleted file mode 100644 index 55d6de0..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 以同级临时文件生成六张 SVG、六张 PNG 和 CSV 后成组发布。 -public sealed class SmoothingReportExporter -{ - private readonly SmoothingFontResolver _fontResolver = new SmoothingFontResolver(); - private readonly SmoothingFigureSetBuilder _figureSetBuilder = new SmoothingFigureSetBuilder(); - private readonly SmoothingSvgRenderer _svgRenderer = new SmoothingSvgRenderer(); - private readonly SmoothingPngRenderer _pngRenderer = new SmoothingPngRenderer(); - private readonly SmoothingCsvWriter _csvWriter = new SmoothingCsvWriter(); - - public SmoothingReportExportResult Export(SmoothingReportExportRequest request) - { - if (request == null || request.Model == null || string.IsNullOrWhiteSpace(request.OutputDirectory) || !IsFileStem(request.FileStem)) - return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.InvalidInput, "报告模型、输出目录或文件名无效。"); - if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution availableFonts, out string fontReason)) - return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.FontUnavailable, fontReason); - availableFonts.Dispose(); - - var pending = new List(); - var svgPaths = new List(); - var pngPaths = new List(); - string csvPath = Path.Combine(request.OutputDirectory, request.FileStem + ".csv"); - try - { - SmoothingFigureSet figures = _figureSetBuilder.Build(request.Model); - for (int index = 0; index < figures.Figures.Count; index++) - { - SmoothingFigureDefinition figure = figures.Figures[index]; - string svgPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".svg"); - string pngPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".png"); - svgPaths.Add(svgPath); - pngPaths.Add(pngPath); - pending.Add(new PendingFile(svgPath, Encoding.UTF8.GetBytes(_svgRenderer.Render(figure)))); - if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution renderFonts, out fontReason)) - throw new InvalidOperationException(fontReason); - byte[] png; - using (renderFonts) png = _pngRenderer.Render(figure, renderFonts); - pending.Add(new PendingFile(pngPath, png)); - } - pending.Add(new PendingFile(csvPath, _csvWriter.Write(request.Model))); - Directory.CreateDirectory(request.OutputDirectory); - for (int index = 0; index < pending.Count; index++) File.WriteAllBytes(pending[index].TemporaryPath, pending[index].Content); - PublishAll(pending); - DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.svg")); - DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.png")); - return SmoothingReportExportResult.Success(svgPaths, pngPaths, csvPath); - } - catch (Exception exception) - { - RestorePublishedFiles(pending); - return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.Failed, exception.Message); - } - finally - { - for (int index = 0; index < pending.Count; index++) - { - DeleteIfExists(pending[index].TemporaryPath); - DeleteIfExists(pending[index].BackupPath); - } - } - } - - private static void PublishAll(IReadOnlyList files) - { - string transaction = Guid.NewGuid().ToString("N"); - for (int index = 0; index < files.Count; index++) - { - PendingFile file = files[index]; - file.ExistedBeforePublish = File.Exists(file.FinalPath); - file.BackupPath = file.ExistedBeforePublish ? file.FinalPath + ".backup-" + transaction : string.Empty; - if (file.ExistedBeforePublish) File.Replace(file.TemporaryPath, file.FinalPath, file.BackupPath); - else File.Move(file.TemporaryPath, file.FinalPath); - file.Published = true; - } - for (int index = 0; index < files.Count; index++) DeleteIfExists(files[index].BackupPath); - } - - private static void RestorePublishedFiles(IReadOnlyList files) - { - for (int index = files.Count - 1; index >= 0; index--) - { - PendingFile file = files[index]; - if (!file.Published) continue; - try - { - if (file.ExistedBeforePublish && File.Exists(file.BackupPath)) - { - if (File.Exists(file.FinalPath)) File.Replace(file.BackupPath, file.FinalPath, null); - else File.Move(file.BackupPath, file.FinalPath); - } - else if (!file.ExistedBeforePublish) DeleteIfExists(file.FinalPath); - } - catch { } - } - } - - private static bool IsFileStem(string value) - { - return !string.IsNullOrWhiteSpace(value) && value.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 && value.IndexOf(Path.DirectorySeparatorChar) < 0 && value.IndexOf(Path.AltDirectorySeparatorChar) < 0; - } - - private static void DeleteIfExists(string path) - { - if (!string.IsNullOrEmpty(path) && File.Exists(path)) File.Delete(path); - } - - private sealed class PendingFile - { - public PendingFile(string finalPath, byte[] content) - { - FinalPath = finalPath; - Content = content; - TemporaryPath = finalPath + ".tmp"; - BackupPath = string.Empty; - } - - public string FinalPath { get; } - public byte[] Content { get; } - public string TemporaryPath { get; } - public string BackupPath { get; set; } - public bool ExistedBeforePublish { get; set; } - public bool Published { get; set; } - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs deleted file mode 100644 index 5cbb46e..0000000 --- a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -/// 把单张共享图形定义渲染为 UTF-8 XML 可编辑 SVG;轨迹仅由离散点组成。 -public sealed class SmoothingSvgRenderer -{ - public string Render(SmoothingFigureModel model) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1]); - } - - public string Render(SmoothingFigureDefinition figure) - { - if (figure == null) throw new ArgumentNullException(nameof(figure)); - var svg = new StringBuilder(); - svg.Append("\n\n") - .Append("\n") - .Append("\n"); - AppendTitle(svg, figure); - if (figure.IsCurvatureFigure) AppendCurvatureAxes(svg, figure); else AppendOverheadAxes(svg, figure); - svg.Append("\n"); - if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendObstacles(svg, figure); - if (figure.IsCurvatureFigure) AppendCurvaturePoints(svg, figure); else AppendPathPoints(svg, figure); - if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendStartGoal(svg, figure); - svg.Append("\n"); - AppendLegend(svg, figure); - svg.Append(""); - return svg.ToString(); - } - - private static void AppendTitle(StringBuilder svg, SmoothingFigureDefinition figure) - { - svg.Append("") - .Append(Escape(figure.Title)).Append(" — ").Append(Escape(figure.Model.ScenarioLabel)).Append("\n"); - } - - private static void AppendOverheadAxes(StringBuilder svg, SmoothingFigureDefinition figure) - { - AppendPlotFrame(svg, figure); - for (int index = 0; index < figure.XTicks.Count; index++) - { - double x = WorldX(figure, figure.XTicks[index]); - AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); - svg.Append("").Append(Number(figure.XTicks[index])).Append("\n"); - } - for (int index = 0; index < figure.YTicks.Count; index++) - { - double y = WorldY(figure, figure.YTicks[index]); - AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); - svg.Append("").Append(Number(figure.YTicks[index])).Append("\n"); - } - svg.Append("X (m)\n") - .Append("Y (m)\n"); - } - - private static void AppendCurvatureAxes(StringBuilder svg, SmoothingFigureDefinition figure) - { - AppendPlotFrame(svg, figure); - for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) - { - double x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); - AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); - svg.Append("").Append(Number(figure.CurvatureArcLengthTicks[index])).Append("\n"); - } - for (int index = 0; index < figure.CurvatureTicks.Count; index++) - { - double y = CurvatureY(figure, figure.CurvatureTicks[index]); - AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); - svg.Append("").Append(Number(figure.CurvatureTicks[index])).Append("\n"); - } - if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) - { - double zero = CurvatureY(figure, 0d); - svg.Append("\n"); - } - svg.Append("s (m)\n") - .Append("κ (m⁻¹)\n"); - } - - private static void AppendPlotFrame(StringBuilder svg, SmoothingFigureDefinition figure) - { - svg.Append("\n"); - } - - private static void AppendGridLine(StringBuilder svg, double x1, double y1, double x2, double y2) - { - svg.Append("\n"); - } - - private static void AppendObstacles(StringBuilder svg, SmoothingFigureDefinition figure) - { - for (int index = 0; index < figure.Model.Obstacles.Count; index++) - { - SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; - svg.Append("\n"); - } - } - - private static void AppendPathPoints(StringBuilder svg, SmoothingFigureDefinition figure) - { - for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) - { - SmoothingFigureSeriesView view = figure.Series[viewIndex]; - AppendPoints(svg, view, point => WorldX(figure, point.X), point => WorldY(figure, point.Y)); - for (int markerIndex = 0; markerIndex < view.Series.ViolationMarkers.Count; markerIndex++) AppendCross(svg, figure, view.Series.ViolationMarkers[markerIndex], view.Series.Color); - } - } - - private static void AppendCurvaturePoints(StringBuilder svg, SmoothingFigureDefinition figure) - { - for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) - { - SmoothingFigureSeriesView view = figure.Series[viewIndex]; - AppendPoints(svg, view, point => CurvatureX(figure, point.ArcLength), point => CurvatureY(figure, point.VehicleCurvature)); - } - } - - private static void AppendPoints(StringBuilder svg, SmoothingFigureSeriesView view, Func x, Func y) - { - if (!view.Series.IsCurveVisible) return; - svg.Append("\n"); - for (int index = 0; index < view.Series.Points.Count; index++) - { - SmoothingFigurePoint point = view.Series.Points[index]; - svg.Append("\n"); - } - svg.Append("\n"); - } - - private static void AppendCross(StringBuilder svg, SmoothingFigureDefinition figure, SmoothingFigurePoint point, string color) - { - double x = WorldX(figure, point.X), y = WorldY(figure, point.Y), radius = 3d; - svg.Append("\n"); - } - - private static void AppendStartGoal(StringBuilder svg, SmoothingFigureDefinition figure) - { - svg.Append("\n"); - double x = WorldX(figure, figure.Model.Goal.X), y = WorldY(figure, figure.Model.Goal.Y), radius = 4d; - svg.Append("\n"); - } - - private static void AppendLegend(StringBuilder svg, SmoothingFigureDefinition figure) - { - double columnWidth = 198d; - for (int index = 0; index < figure.LegendEntries.Count; index++) - { - SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; - int column = index % 2; - int row = index / 2; - double x = figure.PlotXPoints + column * columnWidth; - double y = figure.LegendYPoints + row * 15d; - svg.Append("\n") - .Append(Escape(entry.Label)).Append("\n"); - } - } - - private static double WorldX(SmoothingFigureDefinition figure, double x) { return figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter; } - private static double WorldY(SmoothingFigureDefinition figure, double y) { return figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter; } - private static double CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints; } - private static double CurvatureY(SmoothingFigureDefinition figure, double curvature) { return figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints; } - private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } - private static string Escape(string value) { return (value ?? string.Empty).Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs b/.task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs deleted file mode 100644 index 5490ddc..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Utils; - -/// Deterministic angle helpers used by planning code. -public static class AngleMath -{ - public static double NormalizeRadians(double radians) - { - if (!NumericGuard.IsFinite(radians)) - return double.NaN; - - double normalized = radians % (2d * Math.PI); - if (normalized >= Math.PI) normalized -= 2d * Math.PI; - if (normalized < -Math.PI) normalized += 2d * Math.PI; - return normalized; - } - - public static double ShortestSignedDifference(double from, double to) - { - return NormalizeRadians(to - from); - } - - public static int ToHeadingIndex(double heading, double resolution, int binCount) - { - if (!NumericGuard.IsFinite(heading) || !NumericGuard.IsPositiveFinite(resolution) || binCount <= 0) - throw new ArgumentOutOfRangeException(); - - double normalized = NormalizeRadians(heading); - int index = (int)Math.Floor((normalized + Math.PI) / resolution); - index %= binCount; - return index < 0 ? index + binCount : index; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs b/.task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs deleted file mode 100644 index 44d4cea..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Utils; - -/// Two-dimensional rigid-body coordinate transforms. Units are preserved. -public static class CoordinateTransform -{ - public static void LocalToWorld( - double originX, double originY, double headingRadians, - double localX, double localY, - out double worldX, out double worldY) - { - double cosine = Math.Cos(headingRadians); - double sine = Math.Sin(headingRadians); - worldX = originX + cosine * localX - sine * localY; - worldY = originY + sine * localX + cosine * localY; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs b/.task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs deleted file mode 100644 index 5683184..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Utils; - -public readonly struct GridIndex : IEquatable -{ - public GridIndex(int row, int col) { Row = row; Col = col; } - public int Row { get; } - public int Col { get; } - public bool Equals(GridIndex other) { return Row == other.Row && Col == other.Col; } - public override bool Equals(object obj) { return obj is GridIndex && Equals((GridIndex)obj); } - public override int GetHashCode() { unchecked { return Row * 397 ^ Col; } } - public static bool operator ==(GridIndex left, GridIndex right) { return left.Equals(right); } - public static bool operator !=(GridIndex left, GridIndex right) { return !left.Equals(right); } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs b/.task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs deleted file mode 100644 index 0714ad4..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Utils; - -public static class NumericGuard -{ - public static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } - public static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value); } - public static bool IsPositiveFinite(double value) { return IsFinite(value) && value > 0d; } - public static bool IsInRange(float value, float minimum, float maximum) - { - return IsFinite(value) && value >= minimum && value <= maximum; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs b/.task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs deleted file mode 100644 index 2217a45..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; - -namespace MultiWheelC.TrajectoryPlanning.Utils; - -/// 一次规划操作的中立停止原因;Map 与 CoarsePath 分别映射为自己的结果状态。 -internal enum PlanningOperationStopReason -{ - /// 预算仍可继续执行。 - None, - /// 调用方取消了本次操作。 - Cancelled, - /// 从预算创建起已经达到总超时。 - TimedOut, -} - -/// -/// 以单调时钟统一控制一次规划操作的取消和总超时。 -/// 本类不依赖 Map 或 CoarsePath,供两层在长循环检查同一个停止条件。 -/// -internal sealed class PlanningOperationBudget -{ - private const int CheckIntervalWorkItems = 256; - - private readonly CancellationToken _cancellationToken; - private readonly Stopwatch _stopwatch; - private readonly TimeSpan _timeout; - private readonly bool _hasTimeout; - - /// - /// 创建具有总超时的操作预算。 - /// 参数:cancellationToken 为调用方停止请求;timeout 必须非负,且从构造时开始计时。 - /// - internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout) - { - if (timeout < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(timeout)); - _cancellationToken = cancellationToken; - _timeout = timeout; - _hasTimeout = true; - _stopwatch = Stopwatch.StartNew(); - } - - private PlanningOperationBudget(CancellationToken cancellationToken) - { - _cancellationToken = cancellationToken; - _timeout = TimeSpan.Zero; - _hasTimeout = false; - _stopwatch = Stopwatch.StartNew(); - } - - /// 创建只响应取消、不限制总耗时的兼容预算。 - internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken) - { - return new PlanningOperationBudget(cancellationToken); - } - - /// 从预算创建到当前的单调经过时间。 - internal TimeSpan Elapsed { get { return _stopwatch.Elapsed; } } - - /// 立即查询当前停止原因;取消优先于超时。 - internal PlanningOperationStopReason GetStopReason() - { - if (_cancellationToken.IsCancellationRequested) return PlanningOperationStopReason.Cancelled; - return _hasTimeout && _stopwatch.Elapsed >= _timeout - ? PlanningOperationStopReason.TimedOut - : PlanningOperationStopReason.None; - } - - /// - /// 在循环工作单元中按固定上限检查预算。 - /// 参数:workItemCount 为当前循环共享的已处理单元计数;首次及每 256 个单元检查一次。 - /// - internal PlanningOperationStopReason CheckEvery(ref int workItemCount) - { - workItemCount++; - return workItemCount == 1 || workItemCount % CheckIntervalWorkItems == 0 - ? GetStopReason() - : PlanningOperationStopReason.None; - } -} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs b/.task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs deleted file mode 100644 index 7c41b97..0000000 --- a/.task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace MultiWheelC.TrajectoryPlanning.Utils; - -public static class UnitConverter -{ - public static double MillimetersToMeters(double value) { return value / 1000d; } - public static double MetersToMillimeters(double value) { return value * 1000d; } - public static double DegreesToRadians(double value) { return value * Math.PI / 180d; } - public static double RadiansToDegrees(double value) { return value * 180d / Math.PI; } -} diff --git a/.task8-sweep/PilotConfig.cs b/.task8-sweep/PilotConfig.cs deleted file mode 100644 index 57dab71..0000000 --- a/.task8-sweep/PilotConfig.cs +++ /dev/null @@ -1,334 +0,0 @@ -using ClumsyCore; -using MDCSToolBox.Clumsy.Pilot.MultiWheel; -using Newtonsoft.Json; - -namespace MultiWheelC; - -public class PilotConfig : MultiWheelPilotConfig -{ - [FieldMember(desc = "[sync] steering angle acceleration(deg/s^2)")] public float SyncThAccPerSec = 30f; - [FieldMember(desc = "[sync] fleet member distance(mm)")] public float TestCarSyncDistance = 2400f; - [FieldMember(desc = "[sync] fleet layout bias angle(deg)")] public float TestCarSyncTh = 0f; - // Fleet manual remote IO values are normalized joystick ratios. Keep all speed/angle scaling here. - [FieldMember(desc = "[sync] fleet manual max linear speed(m/s)")] public float FleetManualMaxSpeed = 0.3f; - [FieldMember(desc = "[sync] fleet manual normal-mode full-stick steering angle(deg)")] public float FleetManualMaxSteerAngleDeg = 45f; - [FieldMember(desc = "[sync] fleet manual crab-mode full-stick steering angle(deg)")] public float FleetManualMaxCrabAngleDeg = 60f; - [FieldMember(desc = "[sync] fleet manual rotate-mode full-stick angular speed(deg/s)")] public float FleetManualMaxRotateOmegaDegPerSec = 45f; - [FieldMember(desc = "[sync] 蟹行舵角上限(deg,应与Medulla舵轮角度限制匹配,默认120)")] public float MultiVehicleCrabSteerLimitDeg = 120f; - [FieldMember(desc = "[sync] 检测中心偏移(mm)")] public float DeltaDetectCenter = 350f; - // 仅控制"车队内姿态纠正"(POS 补偿)是否使用 Detour 的 SLAM 位姿,不影响"整个车队姿态的计算"。 - // 默认 false:定位不参与车队内姿态纠正(各车按编队几何/互识别保持队形,不做 SLAM 逐车纠偏)。 - // 为 true:额外用 getCartLocation() 反推每台车相对编队中心的偏差并做 POS 补偿。 - // 注意:无论该开关如何,自动模式下整队姿态(反推/广播车队中心、SLAM 间距、自动安全门)始终依赖 Detour 全局定位; - // 主车自动模式必调用 getCartLocation(),若无有效全局定位该调用会阻塞 → 联动线程阻塞不下发速度(安全停车)。 - [FieldMember(desc = "[sync] 定位是否参与车队内姿态纠正(不影响整队姿态计算)")] public bool MultiVehicleSyncUseDetour = false; - // 手动外部遥控联动默认只走 2 腿检测/几何同步,避免 Detour getCartLocation 阻塞导致遥控和检测可视化变慢。 - [FieldMember(desc = "[sync] 手动联动是否启用定位姿态纠正(默认关闭)")] public bool MultiVehicleManualUseDetourCorrection = false; - - [FieldMember(desc = "多车联动:总车数")] public int MultiVehicleFleetNum = 2; - [FieldMember(desc = "联动线程周期(ms)")] public int MultiVehicleSyncInterval = 50; - [FieldMember(desc = "多车联动:主车端点 ip:port,/ 表示本车为主车")] public string MultiVehicleMasterEndpoint = "/"; - [FieldMember(desc = "多车联动:本车同步 IP")] public string SimpleIp = "127.0.0.1"; - - [FieldMember(desc = "多车联动:本车回连端点 ip:port,供主车 notify 回连,空=127.0.0.1:本车port")] public string MultiVehicleSelfEndpoint = ""; - - [JsonProperty("MultiVehicleMasterIp")] - private string LegacyMasterIpSetter - { - set - { - if (string.IsNullOrEmpty(value) || value == "/") return; - if (MultiVehicleMasterEndpoint == "/") - MultiVehicleMasterEndpoint = value.Contains(":") ? value : $"{value}:8008"; - } - } - - [FieldMember(desc = "多车联动:启用互识别纠正")] public bool MultiVehicleUseDetect = false; - - // B: 自动速度命令新鲜度(ms)。主车超过此时长未从路径控制器收到新速度命令(路径结束/早退/卡顿), - // 即视为失效并清零下发速度,避免车队按末速度滑行。0 表示自动取 max(200, interval*4)。 - [FieldMember(desc = "多车联动:自动速度命令超时(ms,0=auto)")] public int MultiVehicleAutoCmdTimeoutMs = 0; - // C: fleet 成员存活 TTL(ms)。主车剔除超过此时长未 register/刷新的从车;编队就绪要求所有成员新鲜。 - // 0 表示自动取 max(500, interval*6)。 - [FieldMember(desc = "多车联动:成员存活TTL(ms,0=auto)")] public int MultiVehicleMemberTtlMs = 0; - // D: 自动模式下用主车路径控制器的理想车队中心(idealPos/idealAngle)作为各车 layout 目标, - // 弧线路径上做 per-car 前馈而非仅共用 frontTh/rearTh 事后纠偏。 - [FieldMember(desc = "多车联动:自动模式按理想中心前馈(弧线)")] public bool MultiVehicleAutoUseIdealCenter = true; - // H: 自动模式必须有有效车队中心(SLAM 可反推),全程定位丢失时停车,避免纯 SLAM 下盲跑。 - [FieldMember(desc = "多车联动:自动模式要求有效车队中心")] public bool MultiVehicleAutoRequireFleetCenter = true; - [FieldMember(desc = "多车联动:SLAM X补偿系数")] public float MultiVehiclePosBiasXFac = 0.5f; - [FieldMember(desc = "多车联动:SLAM Y补偿系数")] public float MultiVehiclePosBiasYFac = 0.5f; - [FieldMember(desc = "多车联动:SLAM Th补偿系数")] public float MultiVehiclePosBiasThFac = 0.5f; - [FieldMember(desc = "多车联动:X补偿阈值(mm)")] public float MultiVehiclePosBiasXThreshold = 50f; - [FieldMember(desc = "多车联动:Y补偿阈值(mm)")] public float MultiVehiclePosBiasYThreshold = 50f; - [FieldMember(desc = "多车联动:Th补偿阈值(deg)")] public float MultiVehiclePosBiasThThreshold = 5f; - - [FieldMember(desc = "多车联动:互识别 X补偿系数")] public float MultiVehicleDetectBiasXFac = 0.5f; - [FieldMember(desc = "多车联动:互识别 Y补偿系数")] public float MultiVehicleDetectBiasYFac = 0.5f; - [FieldMember(desc = "多车联动:互识别 Th补偿系数")] public float MultiVehicleDetectBiasThFac = 0.5f; - [FieldMember(desc = "多车联动:互识别 X补偿阈值(mm)")] public float MultiVehicleDetectBiasXThreshold = 50f; - [FieldMember(desc = "多车联动:互识别 Y补偿阈值(mm)")] public float MultiVehicleDetectBiasYThreshold = 50f; - [FieldMember(desc = "多车联动:互识别 Th补偿阈值(deg)")] public float MultiVehicleDetectBiasThThreshold = 5f; - - // 原地旋转(mode2)闭环纠偏(PI):把"本车应移动到的位置(dx,dy,mm)/应转角(dth,deg)"作为误差, - // 用 PI 控制器换算成车体系修正速度叠加到绕队心旋转上。纯 P 对抗恒定横向滑移扰动有稳态残差, - // 加积分项把稳态误差拉到 0;积分带限幅(抗 windup),总输出限幅在 Max 内防过冲/振荡。 - // Fac=比例增益(mm/s per mm、deg/s per deg);IFac=积分增益(mm/s per mm·s、deg/s per deg·s);Max=总输出上限。 - [FieldMember(desc = "原地旋转纠偏:平移比例增益P(mm/s per mm)")] public float MultiVehicleRotateCompXyFac = 1.2f; - [FieldMember(desc = "原地旋转纠偏:平移积分增益I(mm/s per mm·s)")] public float MultiVehicleRotateCompXyIFac = 0.8f; - [FieldMember(desc = "原地旋转纠偏:平移速度上限(mm/s)")] public float MultiVehicleRotateCompXyMax = 150f; - [FieldMember(desc = "原地旋转纠偏:转向比例增益P(deg/s per deg)")] public float MultiVehicleRotateCompThFac = 0.8f; - [FieldMember(desc = "原地旋转纠偏:转向积分增益I(deg/s per deg·s)")] public float MultiVehicleRotateCompThIFac = 0.8f; - [FieldMember(desc = "原地旋转纠偏:转向速度上限(deg/s)")] public float MultiVehicleRotateCompThMax = 15f; - // 仅当车队实际被指令旋转(|fleetOmega|超过此阈值)时才运行纠偏 PI;否则清零并复位积分, - // 避免松开摇杆后积分残留持续驱动车辆"自行旋转停不下来"。 - [FieldMember(desc = "原地旋转纠偏:生效的最小角速度阈值(deg/s)")] public float MultiVehicleRotateActiveOmega = 0.5f; - // 安全网:每轮纠偏速度幅值 <= 该比例 * 本轮旋转切向速度,限制合速度相对纯切向的最大偏角。 - // 旧配置若仍为 <0,运行时按安全默认 0.10 处理;确需放宽时可在主车显式调大并同步给从车。 - [FieldMember(desc = "原地旋转纠偏:纠偏/旋转切向比例硬上限,<0使用安全默认0.10")] public float MultiVehicleRotateCompTangentFrac = 0.10f; - - [FieldMember(desc = "单车同步 xy 精度(mm)")] public float SingleCarSyncPrecisionXy = 10f; - [FieldMember(desc = "单车同步 th 精度(deg)")] public float SingleCarSyncPrecisionTh = 0.2f; - - [FieldMember(desc = "Playground WebAPI 基地址")] - public string PlaygroundWebApiUrl = "http://localhost:18090"; - - [FieldMember(desc = "MultiVehicle rotate pose WebAPI diagnostics (simulation only)")] - public bool MultiVehicleRotatePoseWebApiDiagEnabled = false; - - [FieldMember(desc = "Playground 小车名称(场景 robots[].name)")] - public string PlaygroundRobotName = "agv_multi_1"; - - [FieldMember(desc = "Playground 邻车名称(仅主车用于原地旋转位姿诊断)")] - public string PlaygroundNeighborRobotName = "agv_multi_2"; - - [FieldMember(desc = "WebAPI 平移测试:平移距离(mm)")] - public float WebApiTranslateMm = 100f; - - [FieldMember(desc = "WebAPI 旋转测试:旋转角度(deg)")] - public float WebApiRotateDeg = 5f; - - [FieldMember(desc = "原地旋转:目标朝向(世界坐标系, deg)")] - public float InPlaceRotateTargetWorldDeg = 90f; - - [FieldMember(desc = "原地旋转:旋转角速度(deg/s)")] - public float InPlaceRotateSpeed = 30f; - - [FieldMember(desc = "原地旋转:到位角度精度(deg)")] - public float InPlaceRotateArriveDeg = 1f; - - [FieldMember(desc = "原地旋转:起转前舵轮对齐精度(deg)")] - public float InPlaceRotateWheelAlignDeg = 2f; - - [FieldMember(desc = "原地旋转:旋转过程中舵轮偏差重对齐阈值(deg)")] - public float InPlaceRotateActiveWheelAlignDeg = 10f; - - // ===== 车队联动-原地旋转动作(FleetRotateInPlace / 对应 FleetRemote 原地旋转模式)===== - // 通过 Clumsy 内部脚本字段驱动 TickMultiVehicle 的 mode2 旋转(绕车队中心 + PI 纠偏),需主车运行。 - [FieldMember(desc = "车队原地旋转:角速度大小(deg/s,方向由目标角符号决定)")] - public float FleetRotateOmega = 15f; - - [FieldMember(desc = "车队原地旋转:目标相对转角(deg,+逆时针)")] - public float FleetRotateTargetDeltaDeg = 90f; - - [FieldMember(desc = "车队原地旋转:到位角度精度(deg)")] - public float FleetRotateArriveDeg = 1.5f; - - [FieldMember(desc = "车队原地旋转:减速区宽度(deg),抑制收尾惯性超调")] - public float FleetRotateSlowDeg = 25f; - - [FieldMember(desc = "车队原地旋转:减速区末段最小角速度(deg/s)")] - public float FleetRotateMinOmega = 3f; - - [FieldMember(desc = "车队原地旋转:起步缓启动角加速度(deg/s²,<=0关闭)")] - public float FleetRotateAccel = 20f; - - [FieldMember(desc = "车队原地旋转:到位后安定时长(s)")] - public float FleetRotateSettleSec = 0.5f; - - // 与 MultiVehicleSyncUseDetour 解耦:转到指定角度需航向反馈,默认 true 读主车 SLAM 航向闭环判停。 - // false 时退化为按估算时长开环停止(实际转速≠指令时不精确,易出现"没转到目标就停")。 - [FieldMember(desc = "车队原地旋转:用Detour主车航向闭环判停(默认true,false=按时长开环)")] - public bool FleetRotateUseDetourHeading = true; - - // ===== 车队联动-自动蟹行(FleetCrabWalk)===== - // 以当前车队中心为起点,构造一条直线路径;MovementTest 中车身保持启动朝向追踪该路径。 - // 动作侧参考几何控制器的路径跟踪思路,直接写入 MultiVehicleAuto... 字段,不再复用脚本手动链路。 - [FieldMember(desc = "车队蟹行:路径方向相对启动时车队朝向夹角(deg,逆时针为正;路径在车右侧x度时填-x)")] - public float FleetCrabAngleDeg = 45f; - - [FieldMember(desc = "车队蟹行:AGV入口使用的车队世界系目标朝向(deg)")] - public float FleetCrabBodyWorldHeadingDeg = 0f; - - [FieldMember(desc = "车队蟹行:路径长度(mm)")] - public float FleetCrabLengthMm = 2000f; - - [FieldMember(desc = "车队蟹行:行驶速度(m/s)")] - public float FleetCrabSpeed = 0.2f; - - [FieldMember(desc = "车队蟹行:速度命令加速度限制(m/s^2,<=0表示不限制)")] - public float FleetCrabAccel = 0.2f; - - [FieldMember(desc = "车队蟹行:预对齐后正式下发速度前5秒加速度(m/s^2,<=0表示不限制)")] - public float FleetCrabStartAccel = 0.01f; - - [FieldMember(desc = "车队蟹行:末端开始减速距离(mm)")] - public float FleetCrabSlowDistance = 2000f; - - [FieldMember(desc = "车队蟹行:完成距离(mm),低于该剩余距离结束动作")] - public float FleetCrabFinishDistance = 20f; - - [FieldMember(desc = "车队蟹行:末端最低速度(m/s)")] - public float FleetCrabFinishSpeed = 0.02f; - - [FieldMember(desc = "车队蟹行:末端减速曲线指数")] - public float FleetCrabSlowingPow = 0.8f; - - [FieldMember(desc = "车队蟹行:GCP舵角修正上限(deg)")] - public float FleetCrabGcpThetaThreshold = 95f; - - [FieldMember(desc = "车队蟹行:headingErr角度纠偏比例系数")] - public float FleetCrabDthLinearFac = 1f; - - [FieldMember(desc = "车队蟹行:headingErr角度纠偏舵角限幅(deg)")] - public float FleetCrabDthLinearThreshold = 10f; - - [FieldMember(desc = "FleetCrab startup sync timeout(s)")] - public float FleetCrabStartSyncTimeoutSec = 8f; - - [FieldMember(desc = "FleetCrab startup wheel alignment tolerance(deg)")] - public float FleetCrabStartWheelAlignDeg = 2f; - - // ===== Fleet linked Bezier curve walk ===== - [FieldMember(desc = "FleetCurve MovementTest Bezier control point count")] - public int FleetCurveTestControlPointCount = 4; - - [FieldMember(desc = "FleetCurve speed(m/s)")] - public float FleetCurveSpeed = 0.2f; - - [FieldMember(desc = "FleetCurve slow distance(mm)")] - public float FleetCurveSlowDistance = 2000f; - - [FieldMember(desc = "FleetCurve finish distance(mm)")] - public float FleetCurveFinishDistance = 20f; - - [FieldMember(desc = "FleetCurve finish speed(m/s)")] - public float FleetCurveFinishSpeed = 0.02f; - - [FieldMember(desc = "FleetCurve slowing curve exponent")] - public float FleetCurveSlowingPow = 0.8f; - - // ===== 2腿检测(单线雷达识别两腿托盘 / 轮胎)===== - [FieldMember(desc = "2腿检测:雷达名(逗号分隔可多个)")] - public string TwoLegLidarName = "rear_left_lidar_1,rear_right_lidar_1"; - - [FieldMember(desc = "2腿检测:初始猜测X(mm, 车体坐标系)")] - public float TwoLegGuessX = 2000f; - - [FieldMember(desc = "2腿检测:两腿间距(mm)")] - public float TwoLegWidth = 800f; - - [FieldMember(desc = "2腿检测:两腿间距允许误差(mm)")] - public float TwoLegWidthErr = 100f; - - [FieldMember(desc = "2腿检测:聚类点间距(mm)")] - public float TwoLegBlobDist = 100f; - - [FieldMember(desc = "2腿检测:聚类尺寸(mm)")] - public float TwoLegBlobSize = 200f; - - [FieldMember(desc = "2腿检测:聚类最小点数")] - public int TwoLegBlobPtCount = 5; - - [FieldMember(desc = "2腿检测:聚类 padding")] - public int TwoLegPadding = 5; - - [FieldMember(desc = "2腿检测:腿柱搜索范围")] - public int TwoLegPillarFindingScope = 20; - - [FieldMember(desc = "2腿检测:方向符号(±1)")] - public int TwoLegSgnDir = 1; - - [FieldMember(desc = "2腿检测:中心X偏移(mm)")] - public float TwoLegCenterChangeX = 0f; - - [FieldMember(desc = "2腿检测:输出X补偿(mm)")] - public float TwoLegOutputBiasX = 0f; - - [FieldMember(desc = "2腿检测:输出Y补偿(mm)")] - public float TwoLegOutputBiasY = 0f; - - [FieldMember(desc = "2腿检测:ROI滤波框长(mm)")] - public float TwoLegFilterLength = 1800f; - - [FieldMember(desc = "2腿检测:ROI滤波框宽(mm)")] - public float TwoLegFilterWidth = 600f; - - #region - [FieldMember(desc = "轮胎识别:识别框长")] public float TireFilterLength = 1800f; - [FieldMember(desc = "轮胎识别:识别框宽")] public float TireFilterWidth = 600f; - [FieldMember(desc = "轮胎识别:轮胎间距")] public float TireTwoLegWidth = 800f; - [FieldMember(desc = "轮胎识别:轮胎识别允许误差")] public float TireTwoLegWidthErr = 100f; - [FieldMember(desc = "轮胎识别:轮胎聚类最小点云数")] public int TireTwoLegBlobPtCount = 15; - - [FieldMember(desc = "轮胎识别:前雷达参数")] public float TireFrontTwoLegBlobDist = 100f; - [FieldMember(desc = "轮胎识别:前雷达参数")] public float TireFrontTwoLegBlobSize = 200f; - [FieldMember(desc = "轮胎识别:前雷达参数")] public int TireFrontPadding = 5; - [FieldMember(desc = "轮胎识别:前雷达参数")] public int TireFrontTwoLegPillarFindingScope = 20; - [FieldMember(desc = "轮胎识别:前雷达参数")] public int TireFrontTwoLegSgnDir = 1; - [FieldMember(desc = "轮胎识别:前雷达参数")] public float TireFrontTwoLegCenterChangeX = 0; - [FieldMember(desc = "轮胎识别:后雷达参数")] public float TireBackTwoLegBlobDist = 100f; - [FieldMember(desc = "轮胎识别:后雷达参数")] public float TireBackTwoLegBlobSize = 200f; - [FieldMember(desc = "轮胎识别:后雷达参数")] public int TireBackPadding = 5; - [FieldMember(desc = "轮胎识别:后雷达参数")] public int TireBackTwoLegPillarFindingScope = 20; - [FieldMember(desc = "轮胎识别:后雷达参数")] public int TireBackTwoLegSgnDir = 1; - [FieldMember(desc = "轮胎识别:后雷达参数")] public float TireBackTwoLegCenterChangeX = 0; - - [FieldMember(desc = "抱夹控制pid:Kp")] public float ClampControlKp = 0.1f; - [FieldMember(desc = "抱夹控制pid:Ki")] public float ClampControlKi = 0f; - [FieldMember(desc = "抱夹控制pid:Kd")] public float ClampControlKd = 0f; - [FieldMember(desc = "抱夹控制pid:MaxI")] public float ClampControlMaxI = 0f; - [FieldMember(desc = "抱夹控制pid:Acc")] public float ClampControlSpeedAcc = 1f; - [FieldMember(desc = "抱夹控制pid:Thresh")] public float ClampControlThresh = 0.2f; - [FieldMember(desc = "抱夹控制pid:DeadZone")] public float ClampControlDeadZone = 5f; - [FieldMember(desc = "抱夹最大速度")] public float MaxClampSpeed = 1.5f; - - [FieldMember(desc = "直线行走距离")] public float LineTrackDistance = 1000f; - [FieldMember(desc = "直线行走最大速度")] public float LineTrackMaxSpeed = 0.3f; - [FieldMember(desc = "直线行走Kp")] public float LineTrackKp = 0.2f; - [FieldMember(desc = "直线行走Ki")] public float LineTrackKi = 0f; - [FieldMember(desc = "直线行走Kd")] public float LineTrackKd = 0f; - [FieldMember(desc = "直线行走DeadZone")] public float LineTrackDeadZone = 50f; - - [FieldMember(desc = "轮胎跟踪:切换至盲走距离")] public float TireFollowingWalkBlindSwitchingDistance = 1200f; - [FieldMember(desc = "轮胎跟踪:识别第一对轮胎的初始距离")] public float TireFollowingStage1GuessX = 2000f; - [FieldMember(desc = "轮胎跟踪:识别第二对轮胎的初始距离")] public float TireFollowingStage2GuessX = 2475f; - [FieldMember(desc = "轮胎跟踪:盲走停止距离")] public float TireFollowingWalkBlindFinishDistance = 10f; - [FieldMember(desc = "轮胎跟踪:减速距离")] public float TireFollowingSlowDistance = 200f; - [FieldMember(desc = "轮胎跟踪:最大速度")] public float TireFollowingMaxSpeed = 0.2f; - [FieldMember(desc = "轮胎跟踪:前雷达识别路径偏移X")] public float TireFollowingFrontLidarPathTransformationX = 253f; - [FieldMember(desc = "轮胎跟踪:前雷达识别路径偏移Y")] public float TireFollowingFrontLidarPathTransformationY = 13f; - [FieldMember(desc = "轮胎跟踪:前雷达识别路径偏移Th")] public float TireFollowingFrontLidarWalkBlindTh = -1f; - [FieldMember(desc = "轮胎跟踪:后雷达识别路径偏移X")] public float TireFollowingBackLidarPathTransformationX = 148f; - [FieldMember(desc = "轮胎跟踪:后雷达识别路径偏移Y")] public float TireFollowingBackLidarPathTransformationY = 2f; - [FieldMember(desc = "轮胎跟踪:后雷达识别路径偏移Th")] public float TireFollowingBackLidarWalkBlindTh = 0f; - [FieldMember(desc = "轮胎跟踪:离车时后雷达识别路径偏移X")] public float TireFollowingLeaveCarBackLidarPathTransformationX = 1500f; - [FieldMember(desc = "轮胎跟踪:离车时切换至盲走距离")] public float TireFollowingLeaveCarWalkBlindSwitchingDistance = 1200f; - - [FieldMember(desc = "轮胎跟踪:测试钻轮胎数量")] public int TireFollowingTireNum = 1; - [FieldMember(desc = "轮胎跟踪:过近距离")] public float TireFollowingCloseDistance = 1400; - [FieldMember(desc = "轮胎跟踪:距离过近角度忽略阈值")] public float TireFollowingAngleIgnoreThr = 0.2f; - [FieldMember(desc = "轮胎跟踪:Y最大平均数")] public int TireFollowingYAverageFrameCount = 5; - - [FieldMember(desc = "终点跟踪:速度")] public float DstTrackerMaxSpeed = 0.3f; - [FieldMember(desc = "轮胎跟踪:释放锁点距离")] public float TireFollowingReleaseDistance = 1600; - #endregion - - [FieldMember(desc = "轮胎跟踪:角度调整kp")] public float TireFollowingThkp = 0.05f; - [FieldMember(desc = "轮胎跟踪:角度调整ki")] public float TireFollowingThki = 0.01f; - [FieldMember(desc = "轮胎跟踪:角度调整kd")] public float TireFollowingThkd = 0f; - [FieldMember(desc = "轮胎跟踪:角度调整SpeedAcc")] public float TireFollowingThSpeedAccPerSec = 1f; - [FieldMember(desc = "轮胎跟踪:角度调整Thresh")] public float TireFollowingThThresh = 0.1f; - [FieldMember(desc = "轮胎跟踪:角度调整DeadZone")] public float TireFollowingThDeadZone = 5f; - [FieldMember(desc = "轮胎跟踪:角度调整MaxI")] public float TireFollowingThMaxI = 0.01f; -} diff --git a/.task8-sweep/PilotDefinition.cs b/.task8-sweep/PilotDefinition.cs deleted file mode 100644 index 3352b6e..0000000 --- a/.task8-sweep/PilotDefinition.cs +++ /dev/null @@ -1,2128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Net.Http; -using System.Numerics; -using System.Threading; -using System.Threading.Tasks; -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using ClumsyCore.Utilities; -using CommonUsage; -using CommonUsage.Chassis; -using CommonUsage.Mathematics; -using FundamentalLib; -using FundamentalLib.Utilities; -using MDCSToolBox.Clumsy.Pilot.MultiWheel; -using Vector = ClumsyCore.Utilities.Vector; - -namespace MultiWheelC; - -public class PilotDefinition : MultiWheelPilotDefinition -{ - public new float CarLength = 1472f; - public new float CarWidth = 948f; - - public override void StandardInit() - { - InitMultiVehicleCoordination(); - } - - #region MultiVehicleCoordination (从 MDCSToolBox 内联回 Tutorial) - - [AsLowerIO(desc = "启用(手动)多车联动")] public bool MultiVehicleManualEnabled; - [AsLowerIO(desc = "(手动)多车联动模式")] public int MultiVehicleManualMode; - [FieldMember(desc = "多车联动已同步")] public bool MultiVehicleAligned; - - [AsLowerIO(desc = "多车联动:遥控器Vx比例")] public float MultiVehicleManualVx; - [AsLowerIO(desc = "多车联动:遥控器Vy比例")] public float MultiVehicleManualVy; - [AsLowerIO(desc = "多车联动:遥控器Vth比例")] public float MultiVehicleManualVth; - [AsLowerIO(desc = "多车联动:遥控暂停")] public bool MultiVehicleHold; - - // ===== Clumsy 侧脚本/动作驱动的手动等价输入(不走 Medulla IO,不会被 IO 同步覆盖)===== - // 手动 IO 字段是 [AsLowerIO](Medulla→Clumsy,Medulla 每周期回写),Clumsy 侧 MovementTest 写它们会被覆盖。 - // 因此提供这组内部字段,让 Clumsy 侧动作(如 FleetRotateInPlace)能像 FleetRemote 一样驱动车队联动。 - // ScriptEnabled=使能;Mode 0=常规 1=蟹行 2=原地旋转。 - // 注意:Medulla 遥控 IO 是归一化摇杆比例;脚本字段保持物理量,避免动作参数再被 FleetManual* 二次缩放。 - // Mode0: Vx=m/s, Vth=目标舵角deg;Mode1: Vx=m/s, Vy=蟹行舵角deg;Mode2: Vth=角速度deg/s。 - [FieldMember(desc = "多车联动:脚本驱动使能")] public bool MultiVehicleScriptEnabled; - [FieldMember(desc = "多车联动:脚本驱动模式")] public int MultiVehicleScriptMode; - [FieldMember(desc = "多车联动:脚本驱动Vx")] public float MultiVehicleScriptVx; - [FieldMember(desc = "多车联动:脚本驱动Vy")] public float MultiVehicleScriptVy; - [FieldMember(desc = "多车联动:脚本驱动Vth")] public float MultiVehicleScriptVth; - - [AsUpperIO(desc = "多车联动:自动驾驶", timeOutReset = true)] public bool MultiVehicleAutoEnabled; - [FieldMember(desc = "多车联动:自动驾驶Vx")] public float MultiVehicleAutoVx; - [FieldMember(desc = "多车联动:自动驾驶FrontTh")] public float MultiVehicleAutoFrontTh; - [FieldMember(desc = "多车联动:自动驾驶RearTh")] public float MultiVehicleAutoRearTh; - - public Dictionary MultiVehicleFleet = new(); - public VehicleSyncNotification MultiVehicleNotification; - - // A: 互斥锁固定为独立 readonly 对象,绝不随 MultiVehicleFleet 字段被整体替换而失效。 - // 所有对 MultiVehicleFleet / _multiVehicleFleetSeen 的读写都必须 lock(FleetLock)。 - public readonly object FleetLock = new(); - - // C: 各 fleet 成员最近一次被 register/notify 刷新的本地时刻(用本地时钟,避免远端时钟偏差)。 - // 主车 Tick 据此剔除掉线成员;编队就绪要求所有成员新鲜。 - private readonly Dictionary _multiVehicleFleetSeen = new(); - - [FieldMember(desc = "多车联动:车队姿态x")] public float CenterX; - [FieldMember(desc = "多车联动:车队姿态y")] public float CenterY; - [FieldMember(desc = "多车联动:车队姿态th")] public float CenterTh; - - // G: 车队中心原子快照。三个 float 无法整体原子写,改为整体新建对象后引用赋值(引用赋值原子), - // 路径控制器线程只读最近一次完整快照,避免读到 x 新 / y,th 旧的撕裂组合。 - public sealed class FleetCenterSnapshot - { - public float X, Y, Th; - public long Tick; - } - private volatile FleetCenterSnapshot _fleetCenter = new(); - public FleetCenterSnapshot GetFleetCenterSnapshot() => _fleetCenter; - private void PublishFleetCenter(float x, float y, float th) - { - CenterX = x; - CenterY = y; - CenterTh = th; - _fleetCenter = new FleetCenterSnapshot { X = x, Y = y, Th = th, Tick = DateTime.Now.Ticks }; - } - - // B: 路径控制器最近一次写入自动速度命令的时刻;超时即视为失效(路径结束/早退/卡顿),清零下发。 - public DateTime MultiVehicleAutoCmdTime = DateTime.MinValue; - // D: 路径控制器透传的理想车队中心位姿(世界系),由 idealPos/idealAngle 写入。 - public bool MultiVehicleAutoHasIdeal; - public float MultiVehicleAutoIdealX, MultiVehicleAutoIdealY, MultiVehicleAutoIdealTh; - - // F: notify 序列号(主车单调递增)与从车已应用的最大序列号(丢弃乱序旧包)。 - private long _multiVehicleNotifySeq; - private long _multiVehicleAppliedSeq = -1; - - [AsLowerIO(desc = "车号")] public int CarNum = 1; - [FieldMember(desc = "多车联动:原地旋转本车舵轮已对齐")] - public bool MultiVehicleRotateWheelsReady = true; - [FieldMember(desc = "多车联动:原地旋转整队舵轮已对齐")] - public bool MultiVehicleRotateFleetReady = true; - - #region 夹臂变量 - [AsUpperIO(desc = "左夹臂下发速度")] public float SpeedLeftArm; - - [AsUpperIO(desc = "右夹臂下发速度")] public float SpeedRightArm; - - [AsLowerIO(desc = "左夹臂实际位置")] public float ActualPosLeftArm; - - [AsLowerIO(desc = "右夹臂实际位置")] public float ActualPosRightArm; - - [AsUpperIO(desc = "夹臂不同步报警")] public bool ClampOutOfSync = false; - #endregion - - [AsLowerIO(desc = "左前左轮实际位置")] public float LFLActualPos; - [AsLowerIO(desc = "左前右轮实际位置")] public float LFRActualPos; - [AsLowerIO(desc = "右前左轮实际位置")] public float RFLActualPos; - [AsLowerIO(desc = "右前右轮实际位置")] public float RFRActualPos; - [AsLowerIO(desc = "左后左轮实际位置")] public float LRLActualPos; - [AsLowerIO(desc = "左后右轮实际位置")] public float LRRActualPos; - [AsLowerIO(desc = "右后左轮实际位置")] public float RRLActualPos; - [AsLowerIO(desc = "右后右轮实际位置")] public float RRRActualPos; - - [AsLowerIO(desc = "左夹臂低限位")] public float LeftArmLowerPos; - [AsLowerIO(desc = "左夹臂高限位")] public float LeftArmUpperPos; - [AsLowerIO(desc = "右夹臂低限位")] public float RightArmLowerPos; - [AsLowerIO(desc = "右夹臂高限位")] public float RightArmUpperPos; - - [AsUpperIO(desc = "从C往驱动器下使能")] public bool DisableFromC = false; - [AsUpperIO(desc = "从C上复位")] public bool ResetFromC = false; - [AsLowerIO(desc = "驱动轮使能状态")] public bool WheelAbleState = true; - - private float _multiVehicleAccumulateTh; - private DateTime _multiVehicleLastThTime = DateTime.Now; - private readonly object _multiVehicleNotificationLock = new(); - private DateTime _multiVehicleLastNotifyTime = DateTime.MinValue; - private bool _multiVehicleSyncInitialized; - private bool _multiVehicleWasActive; - private bool _multiVehicleMotionFeasible = true; - private string _multiVehicleMotionInfeasibleReason = ""; - private DateTime _multiVehicleStopLastLog = DateTime.MinValue; - private bool _multiVehicleRotateModeActive; - private float _multiVehicleRotateDirectionHint = 1f; - private string _multiVehicleRotateAlignDetail = ""; - private DateTime _multiVehicleRotateAlignLastLog = DateTime.MinValue; - private const float DefaultRotateCompTangentFrac = 0.10f; - - private struct RotateControlParams - { - public float ActiveOmega; - public float CompXyFac; - public float CompXyIFac; - public float CompXyMax; - public float CompThFac; - public float CompThIFac; - public float CompThMax; - public float CompTangentFrac; - public float StartWheelAlignDeg; - public float ActiveWheelAlignDeg; - } - - // 诊断日志:节流计时 + 最近一次检测几何(中心/朝向/距离),用于定位剧烈运动来源 - private DateTime _mvDbgLastLog = DateTime.MinValue; - private float _mvLastDetCenterX, _mvLastDetCenterY, _mvLastDetDir, _mvLastDetDist; - // 原地旋转(mode2)实际叠加的车体系纠偏旋量(mm/s, mm/s, deg/s),仅用于诊断日志。 - private float _mvRotCompVx, _mvRotCompVy, _mvRotCompOmega; - private DateTime _mvRotateCompLimitLastLog = DateTime.MinValue; - private bool _mvRotateCenterDriftActive; - private float _mvRotateCenterStartX, _mvRotateCenterStartY, _mvRotateCenterStartTh, _mvRotateCenterMaxDrift; - private DateTime _mvRotateCenterLastLog = DateTime.MinValue; - // 原地旋转纠偏 PI 控制器的积分累加器(mm·s, mm·s, deg·s)与上次计算时刻。 - private float _rotIntegX, _rotIntegY, _rotIntegTh; - private float _rotOmegaPeak; // 本次旋转过程中观测到的指令角速度峰值(deg/s),用于纠偏随转速缩放 - private DateTime _rotPiLastTime = DateTime.MinValue; - - // 原地旋转“两车实际 sim 位姿”诊断采样状态(仅主车,通过 Playground WebAPI 读取)。 - private DateTime _rotPoseLastLog = DateTime.MinValue; - private DateTime _rotPosePrevTime = DateTime.MinValue; - private float _rotPosePrevYaw1, _rotPosePrevYaw2; - private bool _rotPoseEpisode; // 本次旋转片段是否已捕获参考量 - private float _rotPoseMid0X, _rotPoseMid0Y; // 起转瞬间车队几何中心(世界系),用于度量整体平移漂移 - - // 写盘诊断日志(Clumsy 侧)。改用 DLog 落盘:同一 topic 的日志会归到以 topic 命名的文件夹, - // 由 DLog 统一管理落点/滚动,无需自行维护文件句柄与 session 清空逻辑。 - private DateTime _mvDiskLastLog = DateTime.MinValue; - private void FleetDiag(string msg) - { - DLog.Log($"car{CarNum} {msg}", "FleetDiagClumsy"); - } - - // 互识别:邻车两腿检测的滑动窗口(最近 1s,最多 10 帧),用于平滑抖动 - private DateTime _mvRemoteInputLastLog = DateTime.MinValue; - private DateTime _mvRemoteDecisionLastLog = DateTime.MinValue; - private DateTime _mvNotifyApplyLastLog = DateTime.MinValue; - private DateTime _mvRotateWheelOutputLastLog = DateTime.MinValue; - - private void LogMultiVehicleRemoteInput(bool isMaster, bool scriptOn, bool manualEnabled, bool autoEnabled, - int manualMode, float manualVx, float manualVy, float manualVth) - { - var now = DateTime.Now; - if ((now - _mvRemoteInputLastLog).TotalMilliseconds < 200) return; - _mvRemoteInputLastLog = now; - - int fleetCnt; - lock (FleetLock) fleetCnt = MultiVehicleFleet.Count; - var notifAgeMs = _multiVehicleLastNotifyTime == DateTime.MinValue - ? -1 - : (now - _multiVehicleLastNotifyTime).TotalMilliseconds; - var notifFresh = notifAgeMs >= 0 && notifAgeMs < Math.Max(300, Conf.MultiVehicleSyncInterval * 5); - - DLog.Log( - $"INPUT master={isMaster} endpoint={Conf.MultiVehicleMasterEndpoint} selfEndpoint={Conf.MultiVehicleSelfEndpoint} car={CarNum} " + - $"rawEn={MultiVehicleManualEnabled} rawHold={MultiVehicleHold} rawMode={MultiVehicleManualMode} rawVx={MultiVehicleManualVx:0.000} rawVy={MultiVehicleManualVy:0.000} rawVth={MultiVehicleManualVth:0.000} " + - $"scriptOn={scriptOn} effEn={manualEnabled} effMode={manualMode} effVx={manualVx:0.000} effVy={manualVy:0.000} effVth={manualVth:0.000} " + - $"auto={autoEnabled} notifFresh={notifFresh} notifAgeMs={notifAgeMs:0} fleetCnt={fleetCnt}/{Conf.MultiVehicleFleetNum}", - "MultiVehicleRemoteDbg"); - } - - private void LogMultiVehicleRemoteDecision(string msg, bool force = false) - { - var now = DateTime.Now; - if (!force && (now - _mvRemoteDecisionLastLog).TotalMilliseconds < 200) return; - _mvRemoteDecisionLastLog = now; - DLog.Log($"car{CarNum} {msg}", "MultiVehicleRemoteDbg"); - } - - private void LogRotateWheelOutputs(MultiWheelChassis chassis, string phase, float omega, bool force = false) - { - var now = DateTime.Now; - if (!force && (now - _mvRotateWheelOutputLastLog).TotalMilliseconds < 300) return; - _mvRotateWheelOutputLastLog = now; - -#pragma warning disable CS0612, CS0618 - var wheels = chassis.GetSteerWheels(); -#pragma warning restore CS0612, CS0618 - var parts = new List(); - for (var i = 0; i < wheels.Count; i++) - { - var wheel = wheels[i]; - if (wheel is DiffSteerWheel diff) - { - parts.Add( - $"w{i}:tgt={diff.GetSendAngle():0.0} read={diff.ReadAngle():0.0} " + - $"L={diff.GetLeftSendSpeed():0.000} R={diff.GetRightSendSpeed():0.000}"); - } - else - { - parts.Add( - $"w{i}:tgt={wheel.GetSendAngle():0.0} read={wheel.ReadAngle():0.0} " + - $"v={wheel.GetSendSpeed():0.000}"); - } - } - - DLog.Log( - $"car{CarNum} phase={phase} omega={omega:0.000} " + string.Join(" | ", parts), - "MultiVehicleRotateWheelOutput"); - } - - private void SetMultiVehicleMotionFeasible(bool feasible, string reason = "") - { - _multiVehicleMotionFeasible = feasible; - _multiVehicleMotionInfeasibleReason = feasible ? "" : (reason ?? ""); - } - - private static float ClampFloat(float value, float min, float max) - { - if (value < min) return min; - if (value > max) return max; - return value; - } - - private void UpdateMultiVehicleRotateModeState(int fleetMode, bool active, float requestedOmega, - RotateControlParams rotateParams) - { - var rotateActive = active && fleetMode == 2; - if (!rotateActive) - { - _multiVehicleRotateModeActive = false; - if (!active) - { - MultiVehicleRotateWheelsReady = true; - _multiVehicleRotateAlignDetail = ""; - } - MultiVehicleRotateFleetReady = true; - _multiVehicleRotateDirectionHint = 1f; - return; - } - - if (!_multiVehicleRotateModeActive) - { - MultiVehicleRotateWheelsReady = false; - MultiVehicleRotateFleetReady = false; - _multiVehicleRotateAlignDetail = "rotate mode just entered"; - } - - _multiVehicleRotateModeActive = true; - if (Math.Abs(requestedOmega) > rotateParams.ActiveOmega) - _multiVehicleRotateDirectionHint = Math.Sign(requestedOmega); - } - - private bool PrepareMultiVehicleRotateWheels(MultiWheelChassis chassis, float requestedOmega, - RotateControlParams rotateParams, - float localCompensateX = 0f, float localCompensateY = 0f, float localCompensateTh = 0f, - bool rampStop = true) - { - var hint = Math.Abs(requestedOmega) > rotateParams.ActiveOmega - ? Math.Sign(requestedOmega) - : Math.Sign(_multiVehicleRotateDirectionHint); - if (hint == 0) hint = 1; - _multiVehicleRotateDirectionHint = hint; - - // Reuse SendRotateMotion's decomposition and angle-limit checks, but keep wheel speed at zero. - if (rampStop) - chassis.RampStop(); - var alignOmega = Math.Abs(requestedOmega) > rotateParams.ActiveOmega - ? requestedOmega - : 0.01f * hint; - var motionOk = chassis.SendRotateMotion(alignOmega, TimeSpan.Zero, - localCompensateX: localCompensateX, localCompensateY: localCompensateY, - localCompensateTh: localCompensateTh); - var wheelAligned = TryCheckRotateWheelAlignment(chassis, rotateParams.StartWheelAlignDeg, - out var alignDetail); - var aligned = motionOk && wheelAligned; - if (!motionOk) - alignDetail = string.IsNullOrWhiteSpace(chassis.LastMotionDecomposeFailureReason) - ? alignDetail - : chassis.LastMotionDecomposeFailureReason; - - MultiVehicleRotateWheelsReady = aligned; - _multiVehicleRotateAlignDetail = alignDetail; - - var now = DateTime.Now; - if ((now - _multiVehicleRotateAlignLastLog).TotalMilliseconds >= 200) - { - _multiVehicleRotateAlignLastLog = now; - DLog.Log( - $"car{CarNum} ROTATE_PREPARE omegaReq={requestedOmega:0.000} alignOmega={alignOmega:0.000} " + - $"hint={hint} comp=({localCompensateX:0.0},{localCompensateY:0.0},{localCompensateTh:0.000}) " + - $"rampStop={rampStop} startTol={rotateParams.StartWheelAlignDeg:0.0} ok={motionOk} aligned={aligned} detail={alignDetail} " + - $"chassisReason={chassis.LastMotionDecomposeFailureReason}", - "MultiVehicleRemoteDbg"); - FleetDiag( - $"ROTATE_PREPARE omegaReq={requestedOmega:0.000} alignOmega={alignOmega:0.000} hint={hint} " + - $"comp=({localCompensateX:0.0},{localCompensateY:0.0},{localCompensateTh:0.000}) " + - $"startTol={rotateParams.StartWheelAlignDeg:0.0} ok={motionOk} aligned={aligned} detail={alignDetail}"); - } - - Hedingben.ToastText( - aligned ? "车队原地旋转舵轮已对齐" : $"车队原地旋转舵轮预对齐中 {alignDetail}", - $"MultiVehicle{CarNum}-rotate-align"); - - return motionOk; - } - - private static bool TryCheckRotateWheelAlignment(MultiWheelChassis chassis, float toleranceDeg, out string detail) - { - try - { -#pragma warning disable CS0612, CS0618 - var wheels = chassis.GetSteerWheels(); -#pragma warning restore CS0612, CS0618 - var maxDth = 0f; - var parts = new List(); - for (var i = 0; i < wheels.Count; i++) - { - var sw = wheels[i]; - var dth = Math.Abs(CommonMath.ThDiff(sw.ReadAngle(), sw.GetSendAngle())); - maxDth = Math.Max(maxDth, dth); - parts.Add($"w{i}:{dth:0.0}"); - } - - detail = $"maxDth={maxDth:0.0} tol={toleranceDeg:0.0} {string.Join(",", parts)}"; - return maxDth <= toleranceDeg; - } - catch (Exception e) - { - detail = $"read alignment failed: {e.Message}"; - return chassis.LastRotateAligned; - } - } - - private void LogMultiVehicleStop(string reason, bool force = false) - { - var now = DateTime.Now; - if (!force && (now - _multiVehicleStopLastLog).TotalMilliseconds < 500) return; - _multiVehicleStopLastLog = now; - var msg = $"FLEET_STOP car={CarNum} reason={reason}"; - DLog.Log(msg, "MultiVehicleSafety"); - FleetDiag(msg); - Hedingben.ToastText($"Fleet stop: {reason}", $"MultiVehicle{CarNum}-stop"); - } - - private readonly object _neighborDetectLock = new(); - private readonly List<(DateTime Time, Vector2 Src, Vector2 Dst)> _neighborDetects = new(); - - // 互识别:上一帧检测中心,用作下一帧猜测(闭环跟踪),与「多舵轮-2腿检测」MovementTest 一致 - private Vector2 _neighborGuess; - private bool _neighborGuessValid; - private DateTime _neighborGuessTime = DateTime.MinValue; - - /// - /// 互识别钩子:用单线雷达识别邻车的两腿/两轮,换算成本车体坐标系下相对“理应对齐参考点”的偏差。 - /// 全对齐时返回 (true, 0, 0, 0);未检测到时返回 (false, ...)。 - /// detectDistance = 编队车间距 - 检测中心偏移(即邻车检测中心到本车原点的标称距离,恒为正值,方向由 TwoLegGuessX 符号决定)。 - /// - private (bool Valid, float Dx, float Dy, float Dth, float NeighborDist) DetectNeighborBias(float detectDistance) - { - // 检测猜测点与「多舵轮-2腿检测」MovementTest 完全一致:首帧 / 跟丢回落都用 Conf.TwoLegGuessX - // (方向与距离都由它一处决定),跟到后用上一帧中心闭环跟踪。 - // detectDistance 只用于计算编队对齐偏差/间距,不参与“去哪里找邻车”,避免猜测点偏离导致 ROI 框漏掉真实邻车。 - var nominal = new Vector2(Conf.TwoLegGuessX, 0f); - - var now = DateTime.Now; - // 闭环跟踪:1s 内检到过就用上一帧中心作猜测,使绿色 ROI 框收敛到真实邻车(与 MovementTest 行为一致); - // 跟丢超时则回落到编队标称位置,避免框漂走。 - var guess = (_neighborGuessValid && (now - _neighborGuessTime).TotalSeconds < 1) ? _neighborGuess : nominal; - var seg = TwoLegDetect.Detect(Conf.TwoLegLidarName, guess.X, TwoLegDetect.SetFilters(guess.X, guess.Y)); - - Vector2 avgSrc, avgDst; - lock (_neighborDetectLock) - { - if (seg != null) - { - _neighborDetects.Add((now, seg.Src, seg.Dst)); - _neighborGuess = (seg.Src + seg.Dst) / 2f; - _neighborGuessValid = true; - _neighborGuessTime = now; - } - _neighborDetects.RemoveAll(r => (now - r.Time).TotalSeconds > 1); - - var cnt = _neighborDetects.Count; - if (cnt == 0) - { - _neighborGuessValid = false; - Hedingben.ToastText($"邻车未检到 (guess x:{guess.X:F0} y:{guess.Y:F0})", $"MultiVehicle{CarNum}-detect"); - return (false, 0f, 0f, 0f, 0f); - } - if (cnt > 10) _neighborDetects.RemoveRange(0, cnt - 10); - - avgSrc = new Vector2(_neighborDetects.Average(r => r.Src.X), _neighborDetects.Average(r => r.Src.Y)); - avgDst = new Vector2(_neighborDetects.Average(r => r.Dst.X), _neighborDetects.Average(r => r.Dst.Y)); - } - - // 两腿连线中点为邻车检测中心;连线的垂线方向即邻车朝向 - var dirVec = avgDst - avgSrc; - var center = (avgSrc + avgDst) / 2f; - var dir = (float)LessMath.RoundTh(Math.Atan2(dirVec.Y, dirVec.X) / Math.PI * 180 + 90); - - // 从检测中心沿邻车朝向外推 detectDistance,得到“本车理应所在的参考点”(车体系) - var target = LessMath.Transform2D(center, dir, detectDistance, 0); - - var dx = target.X; - var dy = target.Y; - var dth = (float)LessMath.ThDiff(dir, 0); - var neighborDist = center.Length(); - - _mvLastDetCenterX = center.X; - _mvLastDetCenterY = center.Y; - _mvLastDetDir = dir; - _mvLastDetDist = neighborDist; - - var painter = UI.GetPainter("MultiVehicleDetectBias", false); - painter.Clear(); - painter.DrawLine(Color.DeepPink, center, target, width: 2); - painter.DrawText(Color.Yellow, $"dx:{dx:F0} dy:{dy:F0} dth:{dth:F2}", center.X, center.Y); - - Hedingben.ToastText( - $"[联动检测] lidar:{Conf.TwoLegLidarName} guess x:{guess.X:F0} y:{guess.Y:F0} | " + - $"中心 x:{center.X:F0} y:{center.Y:F0} dir:{dir:F1} dist:{neighborDist:F0} | 偏差 dx:{dx:F0} dy:{dy:F0} dth:{dth:F2}", - $"MultiVehicle{CarNum}-detect"); - - return (true, dx, dy, dth, neighborDist); - } - - private void InitMultiVehicleCoordination() - { - if (_multiVehicleSyncInitialized) return; - _multiVehicleSyncInitialized = true; - - var hc = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; - - PicoHttpServer.AddPostByteHandler("/multi-vehicle-register-bin", body => - { - try - { - var (carNum, info) = VehicleSyncBinaryCodec.DecodeRegister(body); - ApplyVehicleSyncRegister(carNum, info); - return "ok"; - } - catch (Exception e) - { - DLog.Log($"/multi-vehicle-register-bin error: {e.FormatEx()}", "MultiVehicle"); - throw; - } - }); - - PicoHttpServer.AddPostByteHandler("/multi-vehicle-notify-bin", body => - { - try - { - var notification = VehicleSyncBinaryCodec.DecodeNotification(body); - return ApplyVehicleSyncNotification(notification); - } - catch (Exception e) - { - DLog.Log($"/multi-vehicle-notify-bin error: {e.FormatEx()}", "MultiVehicle"); - throw; - } - }); - - DLog.Log("MultiVehicle coordination initialized, starting loop thread", "MultiVehicle"); - Console.WriteLine("[MultiVehicle] coordination initialized, starting loop thread"); - new Thread(() => MultiVehicleLoop(hc)) { Name = "MultiVehicle", IsBackground = true }.Start(); - } - - private void ApplyVehicleSyncRegister(int carNum, VehicleSyncInfo info) - { - lock (FleetLock) - { - MultiVehicleFleet[carNum] = info; - _multiVehicleFleetSeen[carNum] = DateTime.Now; - } - } - - private string ApplyVehicleSyncNotification(VehicleSyncNotification notification) - { - // Keep the same stale-packet semantics as the previous transport. - if (notification.Seq != 0 && notification.Seq <= _multiVehicleAppliedSeq && - notification.Seq > _multiVehicleAppliedSeq - 100) - return "stale"; - _multiVehicleAppliedSeq = notification.Seq; - - MultiVehicleAligned = notification.Aligned; - lock (FleetLock) - { - MultiVehicleFleet = notification.Fleet ?? new Dictionary(); - var nowSeen = DateTime.Now; - foreach (var key in MultiVehicleFleet.Keys) - _multiVehicleFleetSeen[key] = nowSeen; - } - - lock (_multiVehicleNotificationLock) - { - MultiVehicleNotification = notification; - _multiVehicleLastNotifyTime = DateTime.Now; - } - - var applyNow = DateTime.Now; - if ((applyNow - _mvNotifyApplyLastLog).TotalMilliseconds >= 200) - { - _mvNotifyApplyLastLog = applyNow; - DLog.Log( - $"car{CarNum} NOTIFY_APPLY seq={notification.Seq} mode={notification.Mode} " + - $"omega={notification.FleetOmega:0.000} reqOmega={notification.RequestedFleetOmega:0.000} " + - $"released={notification.FleetMotionReleased} stop={notification.FleetStopActive} " + - $"reason={notification.FleetStopReason} useDetourCorr={notification.UseDetourCorrection} " + - $"rotParam(valid={notification.RotateParamsValid} xyP={notification.RotateCompXyFac:0.###} " + - $"xyI={notification.RotateCompXyIFac:0.###} xyMax={notification.RotateCompXyMax:0.#} " + - $"thP={notification.RotateCompThFac:0.###} thI={notification.RotateCompThIFac:0.###} " + - $"thMax={notification.RotateCompThMax:0.#} frac={notification.RotateCompTangentFrac:0.###} " + - $"startTol={notification.RotateStartWheelAlignDeg:0.#} activeTol={notification.RotateActiveWheelAlignDeg:0.#})", - "MultiVehicleRemoteDbg"); - } - - return "ok"; - } - - private void MultiVehicleLoop(HttpClient hc) - { - var carLength = CarLength; - var carWidth = CarWidth; - var contour = new List - { - new(carLength / 2f, carWidth / 2f), - new(-carLength / 2f, carWidth / 2f), - new(-carLength / 2f, -carWidth / 2f), - new(carLength / 2f, -carWidth / 2f), - }; - var chassis = (MultiWheelChassis)Chassis; - var sendMotionPainter = UI.GetPainter("MultiWheelChassis-SendMotionVis", false); - chassis.SendMotionVisualizer = new Visualizer - { - LineAction = (color, src, dst, startArrow, endArrow, width) => - sendMotionPainter.DrawLine(color, src, dst, startArrow, endArrow, width), - TextAction = (color, str, pos) => sendMotionPainter.DrawText(color, str, pos), - Clear = () => sendMotionPainter.Clear() - }; - - DLog.Log("MultiVehicleLoop thread running", "MultiVehicle"); - Console.WriteLine("[MultiVehicle] loop thread running"); - var loopCount = 0L; - while (true) - { - try - { - TickMultiVehicle(hc, chassis, contour, sendMotionPainter); - } - catch (Exception e) - { - DLog.Log($"MultiVehicle loop error: {e.FormatEx()}", "MultiVehicle"); - } - - // 每 ~5s 打一条心跳,确认循环确实在跑(用于区分“循环没跑”与“写盘失败”) - if (loopCount++ % Math.Max(1, 5000 / Math.Max(1, Conf.MultiVehicleSyncInterval)) == 0) - DLog.Log($"MultiVehicleLoop heartbeat #{loopCount}", "MultiVehicle"); - - Thread.Sleep(Conf.MultiVehicleSyncInterval); - } - } - - private void TickMultiVehicle(HttpClient hc, MultiWheelChassis chassis, List contour, Painter sendMotionPainter) - { - var isMaster = IsMultiVehicleMaster(); - var autoEnabled = false; - // 手动等价输入 = Medulla 遥控(IO) 或 Clumsy 脚本/动作驱动(二者择一,脚本优先)。 - var scriptOn = MultiVehicleScriptEnabled; - var manualEnabled = MultiVehicleManualEnabled || scriptOn; - // 主车实际生效的手动模式与速度:脚本使能时取脚本字段,否则取 Medulla 手动 IO 字段。 - var manualMode = scriptOn ? MultiVehicleScriptMode : MultiVehicleManualMode; - var manualVx = scriptOn ? MultiVehicleScriptVx : MultiVehicleManualVx; - var manualVy = scriptOn ? MultiVehicleScriptVy : MultiVehicleManualVy; - var manualVth = scriptOn ? MultiVehicleScriptVth : MultiVehicleManualVth; - var manualHold = !scriptOn && MultiVehicleHold; - VehicleSyncNotification activeNotification = null; - - if (isMaster) - autoEnabled = MultiVehicleAutoEnabled; - else - { - // 从车:自动/手动联动均可由主车广播解锁(无需各自再拨开关)。 - // 仅在 notification 新鲜时认账,主车停发后超时即自动停车,避免用旧指令跑飞。 - lock (_multiVehicleNotificationLock) - { - var fresh = (DateTime.Now - _multiVehicleLastNotifyTime).TotalMilliseconds < - Math.Max(300, Conf.MultiVehicleSyncInterval * 5); - if (fresh && MultiVehicleNotification != null) - { - activeNotification = MultiVehicleNotification; - autoEnabled = MultiVehicleNotification.AutoEnabled; - manualEnabled = manualEnabled || MultiVehicleNotification.ManualEnabled; - } - } - } - var rotateParams = BuildRotateControlParams(isMaster ? null : activeNotification); - - // 入口诊断(节流 ~300ms):记录从 Medulla 收到的原始 IO 值与门控判定, - // 用于确认遥控指令是否真的传到了 Clumsy,以及为何提前 return。 - LogMultiVehicleRemoteInput(isMaster, scriptOn, manualEnabled, autoEnabled, - manualMode, manualVx, manualVy, manualVth); - - if ((DateTime.Now - _mvDiskLastLog).TotalMilliseconds >= 300) - { - _mvDiskLastLog = DateTime.Now; - int fleetCnt; - lock (FleetLock) fleetCnt = MultiVehicleFleet.Count; - var notifFresh = (DateTime.Now - _multiVehicleLastNotifyTime).TotalMilliseconds < - Math.Max(300, Conf.MultiVehicleSyncInterval * 5); - FleetDiag( - $"ENTRY master={isMaster} | IO: ManualEn={MultiVehicleManualEnabled} Mode={MultiVehicleManualMode} " + - $"Hold={MultiVehicleHold} Vx={MultiVehicleManualVx:0.000} Vy={MultiVehicleManualVy:0.000} Vth={MultiVehicleManualVth:0.0} AutoEn(IO)={MultiVehicleAutoEnabled} " + - $"| SCRIPT en={MultiVehicleScriptEnabled} mode={MultiVehicleScriptMode} Vx={MultiVehicleScriptVx:0.000} Vy={MultiVehicleScriptVy:0.000} Vth={MultiVehicleScriptVth:0.0} " + - $"| gate: manualEnabled={manualEnabled} autoEnabled={autoEnabled} notifFresh={notifFresh} " + - $"notifManualEn={(MultiVehicleNotification != null ? MultiVehicleNotification.ManualEnabled.ToString() : "null")} " + - $"fleetCnt={fleetCnt}/{Conf.MultiVehicleFleetNum} useDetect={Conf.MultiVehicleUseDetect} " + - $"useDetour={Conf.MultiVehicleSyncUseDetour} manualDetour={Conf.MultiVehicleManualUseDetourCorrection}"); - } - - if (!manualEnabled && !autoEnabled) - { - if (_multiVehicleWasActive) - { - chassis.RampStop(); - SetMultiVehicleMotionFeasible(true); - LogMultiVehicleStop("fleet control disabled; ramp stop previous fleet command", true); - } - _multiVehicleWasActive = false; - UpdateMultiVehicleRotateModeState(0, false, 0, rotateParams); - ResetMultiVehicleRotateCenterDrift(); - LogMultiVehicleRemoteDecision( - $"RETURN_IDLE master={isMaster} rawEn={MultiVehicleManualEnabled} scriptOn={scriptOn} auto={autoEnabled} " + - $"rawHold={MultiVehicleHold} rawMode={MultiVehicleManualMode} rawVx={MultiVehicleManualVx:0.000} rawVy={MultiVehicleManualVy:0.000} rawVth={MultiVehicleManualVth:0.000}"); - UI.GetPainter("MultiVehicleFleet-vis", false).Clear(); - sendMotionPainter.Clear(); - lock (FleetLock) - { - MultiVehicleFleet.Clear(); - _multiVehicleFleetSeen.Clear(); - } - MultiVehicleAutoEnabled = false; - MultiVehicleAutoHasIdeal = false; - _multiVehicleAppliedSeq = -1; - _multiVehicleAccumulateTh = 0f; - _multiVehicleLastThTime = DateTime.Now; - - if (!isMaster) - FireAndForgetRegister(hc, BuildSelfInfo(false, false, 0, 0, 0, 0, 0, 0, false)); - - return; - } - _multiVehicleWasActive = true; - - // 自动模式整队姿态依赖 Detour 全局定位(与 MultiVehicleSyncUseDetour 无关):要求编队至少一台车有定位。 - if (isMaster && autoEnabled && !manualEnabled && !FleetHasPosAvailable()) - { - MultiVehicleAutoEnabled = false; - chassis.RampStop(); - SetMultiVehicleMotionFeasible(true); - LogMultiVehicleStop("auto mode requires at least one Detour-positioned fleet member", true); - Hedingben.ToastText("自动多车联动需要至少一台车有 Detour 定位", "MultiVehicle-auto-gate"); - return; - } - - // 两类用途解耦(关键语义): - // - useDetourCorrection (= MultiVehicleSyncUseDetour):是否用 SLAM 位姿做"车队内姿态纠正"(POS 补偿)。 - // false 仅表示"定位不参与车队内姿态纠正",不影响下面整队姿态计算。 - // - slamRead:本车本轮是否读取 Detour 全局位姿。"整个车队姿态的计算"(主车反推/广播车队中心、 - // SLAM 间距、自动模式安全门)始终依赖全局定位 —— 故自动模式下主车必读,与开关无关; - // 手动外部遥控默认不读 Detour,避免 getCartLocation 阻塞拖慢 2 腿检测;脚本自动原地旋转 - // (FleetRotateInPlace) 已经依赖 Detour 判停,因此显式打开 POS 纠偏以保持旋转中心。 - var autoMode = autoEnabled && !manualEnabled; - var scriptRotateDetourCorrection = isMaster && scriptOn && manualMode == 2 && Conf.FleetRotateUseDetourHeading; - var notificationDetourCorrection = !isMaster && activeNotification != null && - activeNotification.UseDetourCorrection; - var manualDetourCorrection = manualEnabled && - (Conf.MultiVehicleManualUseDetourCorrection || - scriptRotateDetourCorrection || - notificationDetourCorrection); - var useDetourCorrection = Conf.MultiVehicleSyncUseDetour && (!manualEnabled || manualDetourCorrection); - var slamRead = useDetourCorrection || (isMaster && autoMode); - float selfX = 0, selfY = 0, selfTh = 0; - if (slamRead) - { - var carPos = DetourInterface.getCartLocation(); - selfX = (float)carPos.x; - selfY = (float)carPos.y; - selfTh = (float)carPos.th; - } - // fleetPosValid:整队全局姿态是否已知。主车=自身读到 SLAM;从车=主车广播标志(下方覆盖)。 - var fleetPosValid = slamRead; - - var syncTh = Conf.TestCarSyncTh; - var syncDistance = Conf.TestCarSyncDistance; - var deltaDetectCenter = Conf.DeltaDetectCenter; - float fleetVx = 0, fleetFrontTh = 0, fleetRearTh = 0, fleetOmega = 0; - var fleetMode = 0; // 0=常规 1=蟹行 2=原地旋转 - var autoCommandTimedOut = false; - var notificationStopActive = false; - var notificationStopReason = ""; - var notificationStopSourceCar = 0; - var notificationRequestedFleetOmega = 0f; - var notificationMotionReleased = true; - float crabInputVx = 0, crabInputVy = 0, crabRawAngle = 0; - float crabSteerLimit = 0; - var crabReverseEquivalent = false; - - if (isMaster) - { - if (manualEnabled) - { - syncTh = 0; - fleetMode = manualMode; - var remoteRatioInput = !scriptOn; - if (fleetMode == 2) - { - // 外部遥控: Vth 为摇杆比例;内部脚本: Vth 为实际角速度(deg/s)。 - fleetOmega = remoteRatioInput - ? ClampFloat(manualVth, -1f, 1f) * Math.Max(0f, Conf.FleetManualMaxRotateOmegaDegPerSec) - : manualVth; - fleetVx = 0; - fleetFrontTh = 0; - fleetRearTh = 0; - _multiVehicleAccumulateTh = 0f; - _multiVehicleLastThTime = DateTime.Now; - } - else if (fleetMode == 1) - { - // 外部遥控: Vx/Vy 为摇杆比例;内部脚本: Vx 为线速度(m/s), Vy 为蟹行舵角(deg)。 - var speed = remoteRatioInput - ? ClampFloat(manualVx, -1f, 1f) * Math.Max(0f, Conf.FleetManualMaxSpeed) - : manualVx; - var crabRatio = remoteRatioInput ? ClampFloat(manualVy, -1f, 1f) : 0f; - var crabAngle = remoteRatioInput - ? crabRatio * Math.Abs(Conf.FleetManualMaxCrabAngleDeg) - : manualVy; - crabInputVx = speed; - crabInputVy = remoteRatioInput ? crabRatio : crabAngle; - crabRawAngle = crabAngle; - crabSteerLimit = Math.Min(179f, Math.Max(1f, Math.Abs(Conf.MultiVehicleCrabSteerLimitDeg))); - - if (crabAngle > crabSteerLimit) { crabAngle -= 180f; speed = -speed; crabReverseEquivalent = true; } - else if (crabAngle < -crabSteerLimit) { crabAngle += 180f; speed = -speed; crabReverseEquivalent = true; } - fleetVx = speed; - fleetFrontTh = crabAngle; - fleetRearTh = crabAngle; - _multiVehicleAccumulateTh = 0f; - _multiVehicleLastThTime = DateTime.Now; - } - else - { - // 外部遥控: Vx/Vth 为摇杆比例;内部脚本: Vx 为线速度(m/s), Vth 为目标舵角(deg)。 - fleetVx = remoteRatioInput - ? ClampFloat(manualVx, -1f, 1f) * Math.Max(0f, Conf.FleetManualMaxSpeed) - : manualVx; - var targetTh = remoteRatioInput - ? ClampFloat(manualVth, -1f, 1f) * Math.Abs(Conf.FleetManualMaxSteerAngleDeg) - : manualVth; - var now = DateTime.Now; - var dt = (float)Math.Min(0.2, Math.Max(0, (now - _multiVehicleLastThTime).TotalSeconds)); - _multiVehicleLastThTime = now; - var sign = Math.Sign(targetTh - _multiVehicleAccumulateTh); - _multiVehicleAccumulateTh += sign * Math.Min(Math.Abs(targetTh - _multiVehicleAccumulateTh), - Conf.SyncThAccPerSec * dt); - fleetFrontTh = _multiVehicleAccumulateTh; - fleetRearTh = -fleetFrontTh; - } - - if (manualHold) - { - fleetMode = 0; - fleetVx = 0; - fleetFrontTh = 0; - fleetRearTh = 0; - fleetOmega = 0; - _multiVehicleAccumulateTh = 0f; - _multiVehicleLastThTime = DateTime.Now; - } - } - else - { - // B: 自动模式速度命令新鲜度门控。路径结束/回调早退/卡顿后,控制器不再刷新 AutoCmdTime, - // 超时即视为失效:清零下发速度与 idealPos,关闭 AutoEnabled,避免车队按末速度滑行。 - var autoTimeoutMs = Conf.MultiVehicleAutoCmdTimeoutMs > 0 - ? Conf.MultiVehicleAutoCmdTimeoutMs - : Math.Max(200, Conf.MultiVehicleSyncInterval * 4); - var autoFresh = (DateTime.Now - MultiVehicleAutoCmdTime).TotalMilliseconds < autoTimeoutMs; - if (autoFresh) - { - fleetVx = MultiVehicleAutoVx; - fleetFrontTh = MultiVehicleAutoFrontTh; - fleetRearTh = MultiVehicleAutoRearTh; - } - else - { - autoCommandTimedOut = true; - fleetVx = fleetFrontTh = fleetRearTh = 0; - MultiVehicleAutoVx = MultiVehicleAutoFrontTh = MultiVehicleAutoRearTh = 0; - MultiVehicleAutoHasIdeal = false; - MultiVehicleAutoEnabled = false; - Hedingben.ToastText("自动速度命令超时,已停车(路径结束/控制器停发)", "MultiVehicle-auto-timeout"); - } - } - } - else if (activeNotification != null) - { - var notification = activeNotification; - - syncTh = notification.SyncTh; - syncDistance = notification.SyncDistance; - deltaDetectCenter = notification.DeltaDetectCenter; - PublishFleetCenter(notification.CenterX, notification.CenterY, notification.CenterTh); - // 从车整队姿态来自主车广播:以广播标志作为 fleetPosValid(自身 slamRead 仅决定是否做本车纠偏)。 - fleetPosValid = notification.PosAvailable; - MultiVehicleAutoEnabled = notification.AutoEnabled; - fleetVx = notification.FleetVx; - fleetFrontTh = notification.FleetFrontTh; - fleetRearTh = notification.FleetRearTh; - fleetMode = notification.Mode; - fleetOmega = notification.FleetOmega; - notificationRequestedFleetOmega = notification.RequestedFleetOmega; - notificationMotionReleased = notification.FleetMotionReleased; - notificationStopActive = notification.FleetStopActive; - notificationStopReason = notification.FleetStopReason ?? ""; - notificationStopSourceCar = notification.FleetStopSourceCar; - // D: 从车采用主车广播的理想车队中心(弧线时由 idealPos/idealAngle 而来)做前馈目标。 - MultiVehicleAutoHasIdeal = notification.HasIdeal; - MultiVehicleAutoIdealX = notification.IdealX; - MultiVehicleAutoIdealY = notification.IdealY; - MultiVehicleAutoIdealTh = notification.IdealTh; - } - - var requestedFleetOmega = !isMaster && fleetMode == 2 - ? ((Math.Abs(notificationRequestedFleetOmega) > 1e-6f || !notificationMotionReleased) - ? notificationRequestedFleetOmega - : fleetOmega) - : fleetOmega; - UpdateMultiVehicleRotateModeState(fleetMode, manualEnabled || autoEnabled, requestedFleetOmega, rotateParams); - - var (layoutX, layoutY, layoutTh) = GetLayoutPose(syncTh, syncDistance); - var inferredFleetCenterValid = false; - float inferredFleetCenterX = 0, inferredFleetCenterY = 0, inferredFleetCenterTh = 0; - Tuple supposedPosForDiag = null; - - if (isMaster) - { - lock (FleetLock) - { - MultiVehicleFleet[CarNum] = BuildSelfInfo(true, slamRead, selfX, selfY, selfTh, - layoutX, layoutY, layoutTh, true); - _multiVehicleFleetSeen[CarNum] = DateTime.Now; // 主车自身恒新鲜 - PruneStaleFleetMembers(); // C: 剔除掉线从车 - } - - if (TryInferFleetCenter(out var cx, out var cy, out var cth)) - { - inferredFleetCenterValid = true; - inferredFleetCenterX = cx; - inferredFleetCenterY = cy; - inferredFleetCenterTh = cth; - PublishFleetCenter(cx, cy, cth); - } - - // D: 自动模式(非手动)下,若控制器给出理想车队中心,则以理想位姿作为各车 layout 目标, - // 使弧线路径上从车按各自相对曲率中心位置前馈,而非仅靠事后检测/SLAM 纠偏。 - if (!manualEnabled && MultiVehicleAutoEnabled && - Conf.MultiVehicleAutoUseIdealCenter && MultiVehicleAutoHasIdeal) - PublishFleetCenter(MultiVehicleAutoIdealX, MultiVehicleAutoIdealY, MultiVehicleAutoIdealTh); - } - - var selfAligned = !Conf.MultiVehicleUseDetect; - var detectValid = false; - float detectDx = 0, detectDy = 0, detectDth = 0; - var currentSpacing = float.NaN; - if (Conf.MultiVehicleUseDetect) - { - var (valid, dx, dy, dth, neighborDist) = DetectNeighborBias(syncDistance - deltaDetectCenter); - detectValid = valid; - if (valid) - { - detectDx = dx; - detectDy = dy; - detectDth = dth; - // 检测中心到本车原点距离 + 检测中心偏移 = 两车参考点当前间距 - currentSpacing = neighborDist + deltaDetectCenter; - selfAligned = Math.Abs(dx) < Conf.SingleCarSyncPrecisionXy && - Math.Abs(dy) < Conf.SingleCarSyncPrecisionXy && - Math.Abs(dth) < Conf.SingleCarSyncPrecisionTh; - } - else selfAligned = false; - } - - // 检测不可用(关闭/跟丢)时回落到 SLAM 世界坐标计算间距(需本车已读到全局定位) - if (float.IsNaN(currentSpacing) && slamRead) - currentSpacing = TryGetSlamSpacing(selfX, selfY); - - if (float.IsNaN(currentSpacing)) - Hedingben.ToastText($"间距 当前:N/A 目标:{syncDistance:F0}mm", $"MultiVehicle{CarNum}-spacing"); - else - Hedingben.ToastText( - $"间距 当前:{currentSpacing:F0}mm 目标:{syncDistance:F0}mm 差:{currentSpacing - syncDistance:F0}mm", - $"MultiVehicle{CarNum}-spacing"); - - lock (FleetLock) - { - MultiVehicleAligned = MultiVehicleFleet.Count == Conf.MultiVehicleFleetNum && - MultiVehicleFleet.Values.All(v => v.Aligned); - } - - var fleetReady = false; - var fleetCount = 0; - lock (FleetLock) - { - fleetCount = MultiVehicleFleet.Count; - fleetReady = fleetCount == Conf.MultiVehicleFleetNum; - } - - // 安全门:开启互识别时,本车或任一其它车检测不到邻车则整队停车(速度置零)。 - // ownDetectOk 是本轮新鲜值;其它车的 DetectOk 来自其上报/主车下发(滑动窗口已给 1s 去抖)。 - var ownDetectOk = !Conf.MultiVehicleUseDetect || detectValid; - bool othersDetectOk; - string otherDetectLostCars; - List> motionInfeasibleMembers; - List> rotateUnalignedMembers; - lock (FleetLock) - { - othersDetectOk = MultiVehicleFleet.Where(kv => kv.Key != CarNum).All(kv => kv.Value.DetectOk); - otherDetectLostCars = string.Join(",", MultiVehicleFleet - .Where(kv => kv.Key != CarNum && !kv.Value.DetectOk) - .Select(kv => kv.Key.ToString())); - motionInfeasibleMembers = MultiVehicleFleet - .Where(kv => kv.Key != CarNum && !kv.Value.MotionFeasible) - .ToList(); - rotateUnalignedMembers = MultiVehicleFleet - .Where(kv => !kv.Value.RotateWheelsAligned) - .ToList(); - } - if (!MultiVehicleRotateWheelsReady && rotateUnalignedMembers.All(kv => kv.Key != CarNum)) - { - rotateUnalignedMembers.Add(new KeyValuePair(CarNum, new VehicleSyncInfo - { - RotateWheelsAligned = false, - RotateWheelAlignDetail = _multiVehicleRotateAlignDetail - })); - } - var rotateUnalignedCars = string.Join(",", rotateUnalignedMembers.Select(kv => kv.Key.ToString())); - var rotateFleetWheelsAligned = fleetMode != 2 || (fleetReady && rotateUnalignedMembers.Count == 0); - MultiVehicleRotateFleetReady = rotateFleetWheelsAligned; - - var fleetStopActive = false; - var fleetStopReason = ""; - var fleetStopSourceCar = 0; - void AddFleetStop(string reason, int sourceCar = 0) - { - if (string.IsNullOrWhiteSpace(reason)) return; - if (!fleetStopActive) - { - fleetStopReason = reason; - fleetStopSourceCar = sourceCar; - } - else - { - fleetStopReason += " | " + reason; - if (fleetStopSourceCar == 0) fleetStopSourceCar = sourceCar; - } - fleetStopActive = true; - } - - if (!fleetReady) - AddFleetStop($"member not ready/stale fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum}"); - if (autoCommandTimedOut) - AddFleetStop("auto command timeout"); - if (Conf.MultiVehicleUseDetect && !ownDetectOk) - AddFleetStop("own two-leg detection lost", CarNum); - if (Conf.MultiVehicleUseDetect && !othersDetectOk) - AddFleetStop($"other two-leg detection lost cars=[{otherDetectLostCars}]"); - if (notificationStopActive) - AddFleetStop($"master stop from car{notificationStopSourceCar}: {notificationStopReason}", - notificationStopSourceCar); - if (!_multiVehicleMotionFeasible) - AddFleetStop($"car{CarNum} motion infeasible: {_multiVehicleMotionInfeasibleReason}", CarNum); - foreach (var kv in motionInfeasibleMembers) - AddFleetStop($"car{kv.Key} motion infeasible: {kv.Value.MotionInfeasibleReason}", kv.Key); - - var canMove = !fleetStopActive; - - // H: 自动模式(非手动)必须有有效车队中心——主车由 SLAM 反推、从车依赖主车广播 fleetPosValid。 - // 自动模式整队姿态始终依赖 Detour(与 MultiVehicleSyncUseDetour 无关):定位全程丢失时强制停车。 - // (用 Detour 时若定位丢失,getCartLocation() 已先行阻塞,此处再兜底要求有效车队中心。) - // 手动模式不受限(允许仅靠互识别/遥控行驶)。 - if (canMove && autoMode && Conf.MultiVehicleAutoRequireFleetCenter) - { - var fleetCenterValid = fleetPosValid && (!isMaster || TryInferFleetCenter(out _, out _, out _)); - if (!fleetCenterValid) - { - AddFleetStop("auto fleet center invalid/localization lost", CarNum); - canMove = false; - Hedingben.ToastText("自动模式无有效车队中心(定位丢失),已停车", $"MultiVehicle{CarNum}-autostop"); - } - } - - if (!canMove) - { - fleetVx = 0; - fleetFrontTh = 0; - fleetRearTh = 0; - fleetOmega = 0; - Hedingben.ToastText( - $"车队停车:{(!ownDetectOk ? "本车" : "其它车")}2腿检测丢失(速度已置零)", - $"MultiVehicle{CarNum}-stop"); - } - else - { - Hedingben.ToastText("车队检测正常", $"MultiVehicle{CarNum}-stop"); - } - - var rotateHoldForAlignment = false; - if (fleetMode == 2 && fleetReady && !fleetStopActive && !notificationMotionReleased) - { - rotateHoldForAlignment = true; - fleetOmega = 0; - Hedingben.ToastText("车队原地旋转等待主车释放速度", $"MultiVehicle{CarNum}-rotate-align"); - LogMultiVehicleRemoteDecision( - $"ROTATE_HOLD_RELEASE master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"requestedOmega={requestedFleetOmega:0.000}", true); - } - else if (fleetMode == 2 && fleetReady && !fleetStopActive && !rotateFleetWheelsAligned) - { - rotateHoldForAlignment = true; - fleetOmega = 0; - Hedingben.ToastText($"车队原地旋转舵轮预对齐中 car=[{rotateUnalignedCars}]", $"MultiVehicle{CarNum}-rotate-align"); - LogMultiVehicleRemoteDecision( - $"ROTATE_HOLD_ALIGN master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"requestedOmega={requestedFleetOmega:0.000} unalignedCars=[{rotateUnalignedCars}]", true); - } - - if (fleetStopActive) - LogMultiVehicleStop(fleetStopReason); - - VisualizeFleet(contour, layoutX, layoutY, layoutTh); - - // 补偿量提到块外,便于诊断日志统一记录三类来源(检测/SLAM)的贡献。 - float xDetectCompensate = 0, yDetectCompensate = 0, thDetectCompensate = 0; - float xPosCompensate = 0, yPosCompensate = 0, thPosCompensate = 0; - float posBiasX = 0, posBiasY = 0, posBiasTh = 0; - - if (!fleetReady) - LogMultiVehicleRemoteDecision( - $"NO_SEND_NOT_READY master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum} canMove={canMove} mode={fleetMode} " + - $"cmdVx={fleetVx:0.000} cmdFTh={fleetFrontTh:0.00} cmdRTh={fleetRearTh:0.00} omega={fleetOmega:0.000}"); - - if (fleetStopActive) - { - chassis.RampStop(); - SetMultiVehicleMotionFeasible(true); - if (fleetMode == 2) - { - MultiVehicleRotateWheelsReady = false; - MultiVehicleRotateFleetReady = false; - _multiVehicleRotateAlignDetail = $"fleet stop: {fleetStopReason}"; - } - LogMultiVehicleRemoteDecision( - $"RAMP_STOP master={isMaster} manual={manualEnabled} auto={autoEnabled} ready={fleetReady} " + - $"reason={fleetStopReason}", true); - } - - if (fleetReady && !fleetStopActive) - { - chassis.SetOriginBias(layoutX, layoutY, layoutTh); - // E: 统一控制点半径,取 syncDistance/2,与编队几何一致。 - var controlRadius = syncDistance / 2f; - chassis.ControlPointRadius = controlRadius; - // #1 纠偏随旋转缩放:把每轮纠偏钳到旋转切向的比例,减速末段切向变小时纠偏同步缩小,杜绝轮向乱摆。 - chassis.RotateCompTangentFrac = rotateParams.CompTangentFrac; - - if (canMove && Conf.MultiVehicleUseDetect) - { - xDetectCompensate = Math.Abs(detectDx) > Conf.SingleCarSyncPrecisionXy - ? detectDx * Conf.MultiVehicleDetectBiasXFac : 0; - yDetectCompensate = Math.Abs(detectDy) > Conf.SingleCarSyncPrecisionXy - ? detectDy * Conf.MultiVehicleDetectBiasYFac : 0; - thDetectCompensate = Math.Abs(detectDth) > Conf.SingleCarSyncPrecisionTh - ? detectDth * Conf.MultiVehicleDetectBiasThFac : 0; - - xDetectCompensate = ClampBias(xDetectCompensate, Conf.MultiVehicleDetectBiasXThreshold); - yDetectCompensate = ClampBias(yDetectCompensate, Conf.MultiVehicleDetectBiasYThreshold); - thDetectCompensate = ClampBias(thDetectCompensate, Conf.MultiVehicleDetectBiasThThreshold); - } - - // 车队内姿态纠正(POS 补偿):仅当 useDetourCorrection 开启且本车读到全局定位时施加。 - // 这是"定位参与车队内姿态纠正"的唯一开关点——关闭它不影响上面的整队姿态计算/安全门。 - if (canMove && useDetourCorrection && slamRead && TryInferFleetCenter(out _, out _, out _)) - { - var supposedPos = LessMath.Transform2D( - Tuple.Create(CenterX, CenterY, CenterTh), - Tuple.Create(layoutX, layoutY, layoutTh)); - supposedPosForDiag = supposedPos; - var posBias = LessMath.SolveTransform2D(Tuple.Create(selfX, selfY, selfTh), supposedPos); - posBiasX = (float)posBias.Item1; - posBiasY = (float)posBias.Item2; - posBiasTh = (float)posBias.Item3; - - xPosCompensate = Math.Abs(posBias.Item1) > Conf.SingleCarSyncPrecisionXy - ? ClampBias(posBias.Item1 * Conf.MultiVehiclePosBiasXFac, Conf.MultiVehiclePosBiasXThreshold) - : 0; - yPosCompensate = Math.Abs(posBias.Item2) > Conf.SingleCarSyncPrecisionXy - ? ClampBias(posBias.Item2 * Conf.MultiVehiclePosBiasYFac, Conf.MultiVehiclePosBiasYThreshold) - : 0; - thPosCompensate = Math.Abs(posBias.Item3) > Conf.SingleCarSyncPrecisionTh - ? ClampBias(posBias.Item3 * Conf.MultiVehiclePosBiasThFac, Conf.MultiVehiclePosBiasThThreshold) - : 0; - } - - sendMotionPainter.Clear(); - if (fleetMode == 2) - { - // 原地旋转:底盘原点已偏置到车队中心,绕队中心旋转,并叠加 PI 闭环纠偏维持队形。 - // detect/pos 偏差(dx,dy,dth) 本身就是"本车参考点应在车体系移动到的位置/应转的角",作为误差喂给 PI。 - // 纯 P 对抗恒定横向滑移有稳态残差,积分项消除之。检测丢失(canMove=false)时不纠偏并清空积分。 - float rotCompVx = 0, rotCompVy = 0, rotCompOmega = 0; - var now = DateTime.Now; - // dt 限幅,避免首帧/卡顿导致积分突跳 - var dt = _rotPiLastTime == DateTime.MinValue ? 0f - : (float)Math.Min(0.2, Math.Max(0, (now - _rotPiLastTime).TotalSeconds)); - _rotPiLastTime = now; - - // 仅在"被指令旋转"时才纠偏:松开摇杆(fleetOmega≈0)时绝不再下发补偿, - // 否则积分残留会持续驱动车辆平移/旋转,表现为"松杆后轮子来回打、自转停不下来"。 - var rotating = Math.Abs(fleetOmega) > rotateParams.ActiveOmega; - if (canMove && rotating) - { - // #3 抗饱和:上一拍舵轮未对齐(gate=0、车没真正转动)时冻结积分,避免卡死时积分越积越大。 - var allowInteg = chassis.LastRotateAligned && - MultiVehicleRotateWheelsReady && - MultiVehicleRotateFleetReady && - !rotateHoldForAlignment; - var errX = detectDx + posBiasX; // mm,车体系:本车纵向(前+)应移动量 - var errY = detectDy + posBiasY; // mm,车体系:本车横向(左+)应移动量 - var errTh = detectDth + posBiasTh; // deg,本车应转角 - rotCompVx = RotatePiTerm(errX, ref _rotIntegX, Conf.SingleCarSyncPrecisionXy, - rotateParams.CompXyFac, rotateParams.CompXyIFac, - rotateParams.CompXyMax, dt, allowInteg); - rotCompVy = RotatePiTerm(errY, ref _rotIntegY, Conf.SingleCarSyncPrecisionXy, - rotateParams.CompXyFac, rotateParams.CompXyIFac, - rotateParams.CompXyMax, dt, allowInteg); - rotCompOmega = RotatePiTerm(errTh, ref _rotIntegTh, Conf.SingleCarSyncPrecisionTh, - rotateParams.CompThFac, rotateParams.CompThIFac, - rotateParams.CompThMax, dt, allowInteg); - - // #1 纠偏随旋转指令缩放:comp ×= |fleetOmega| / 本次峰值。 - // 加速+匀速段峰值≈当前 → 系数≈1(全力纠偏,不削弱);减速段当前<峰值 → 系数随转速同步下降。 - // 关键:旋转切向也∝转速,故"纠偏:切向"比例全程恒定=匀速段比例(远<1),既杜绝末段轮子乱打方向, - // 又不像按切向钳位那样在匀速段就削弱纠偏。 - var absOmega = Math.Abs(fleetOmega); - _rotOmegaPeak = Math.Max(_rotOmegaPeak, absOmega); - if (_rotOmegaPeak > 1e-3f) - { - var compScale = Math.Min(1f, absOmega / _rotOmegaPeak); - rotCompVx *= compScale; - rotCompVy *= compScale; - rotCompOmega *= compScale; - } - LimitRotateCompensation(ref rotCompVx, ref rotCompVy, ref rotCompOmega, - absOmega, controlRadius, rotateParams.CompTangentFrac); - } - else - { - // 检测丢失或未指令旋转:清零补偿并复位积分/计时/峰值,停止时不再有残留驱动。 - _rotIntegX = _rotIntegY = _rotIntegTh = 0; - _rotPiLastTime = DateTime.MinValue; - _rotOmegaPeak = 0; - } - - _mvRotCompVx = rotCompVx; - _mvRotCompVy = rotCompVy; - _mvRotCompOmega = rotCompOmega; - - bool motionOk; - if (rotateHoldForAlignment) - { - motionOk = PrepareMultiVehicleRotateWheels(chassis, requestedFleetOmega, rotateParams, - rotCompVx, rotCompVy, rotCompOmega); - } - else if (rotating) - { - // Preparation calls SendRotateMotion with zero delta; after release it would starve the speed ramp. - if (!MultiVehicleRotateWheelsReady) - { - motionOk = PrepareMultiVehicleRotateWheels(chassis, requestedFleetOmega, rotateParams, - rotCompVx, rotCompVy, rotCompOmega, rampStop: false); - rotateHoldForAlignment = true; - fleetOmega = 0; - chassis.RampStop(); - if (!motionOk || !MultiVehicleRotateWheelsReady) - { - LogMultiVehicleRemoteDecision( - $"ROTATE_HOLD_LOCAL_ALIGN master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"requestedOmega={requestedFleetOmega:0.000} align={_multiVehicleRotateAlignDetail}", true); - } - else - { - LogMultiVehicleRemoteDecision( - $"ROTATE_HOLD_LOCAL_READY master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"requestedOmega={requestedFleetOmega:0.000} align={_multiVehicleRotateAlignDetail}", true); - } - } - else - { - motionOk = chassis.SendRotateMotion(fleetOmega, - localCompensateX: rotCompVx, localCompensateY: rotCompVy, - localCompensateTh: rotCompOmega); - var activeWheelAlignDeg = rotateParams.ActiveWheelAlignDeg; - var activeAligned = TryCheckRotateWheelAlignment(chassis, - activeWheelAlignDeg, out _multiVehicleRotateAlignDetail); - if (!motionOk) - MultiVehicleRotateWheelsReady = false; - if (motionOk && !activeAligned) - { - MultiVehicleRotateWheelsReady = false; - MultiVehicleRotateFleetReady = false; - rotateHoldForAlignment = true; - fleetOmega = 0; - _rotIntegX = _rotIntegY = _rotIntegTh = 0; - _rotPiLastTime = DateTime.MinValue; - _rotOmegaPeak = 0; - rotCompVx = rotCompVy = rotCompOmega = 0; - _mvRotCompVx = _mvRotCompVy = _mvRotCompOmega = 0; - motionOk = PrepareMultiVehicleRotateWheels(chassis, requestedFleetOmega, rotateParams); - LogMultiVehicleRemoteDecision( - $"ROTATE_ACTIVE_REHOLD master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"requestedOmega={requestedFleetOmega:0.000} activeTol={activeWheelAlignDeg:0.0} " + - $"align={_multiVehicleRotateAlignDetail}", true); - } - } - } - else - { - motionOk = chassis.SendRotateMotion(fleetOmega, - localCompensateX: rotCompVx, localCompensateY: rotCompVy, - localCompensateTh: rotCompOmega); - MultiVehicleRotateWheelsReady = motionOk && - TryCheckRotateWheelAlignment(chassis, rotateParams.StartWheelAlignDeg, - out _multiVehicleRotateAlignDetail); - } - SetMultiVehicleMotionFeasible(motionOk, motionOk ? "" : chassis.LastMotionDecomposeFailureReason); - if (!motionOk) - { - AddFleetStop($"car{CarNum} chassis rotate infeasible: {chassis.LastMotionDecomposeFailureReason}", - CarNum); - canMove = false; - LogMultiVehicleStop(fleetStopReason, true); - } - LogRotateWheelOutputs(chassis, rotateHoldForAlignment ? "hold-align" : (rotating ? "rotate" : "idle"), - fleetOmega); - if (fleetPosValid) - LogMultiVehicleRotateCenterDrift(isMaster, manualEnabled, autoEnabled, fleetReady, canMove, rotating, - CenterX, CenterY, CenterTh, requestedFleetOmega, fleetOmega, - rotCompVx, rotCompVy, rotCompOmega); - else - ResetMultiVehicleRotateCenterDrift(); - LogMultiVehicleRemoteDecision( - $"SEND_ROTATE master={isMaster} manual={manualEnabled} canMove={canMove} ready={fleetReady} ok={motionOk} " + - $"holdAlign={rotateHoldForAlignment} wheelReady={MultiVehicleRotateWheelsReady} fleetReady={MultiVehicleRotateFleetReady} " + - $"mode={fleetMode} omegaReq={requestedFleetOmega:0.000} omega={fleetOmega:0.000} " + - $"comp=({rotCompVx:0.0},{rotCompVy:0.0},{rotCompOmega:0.000}) align={_multiVehicleRotateAlignDetail} " + - $"rotParam(xyP={rotateParams.CompXyFac:0.###} xyI={rotateParams.CompXyIFac:0.###} " + - $"xyMax={rotateParams.CompXyMax:0.#} thP={rotateParams.CompThFac:0.###} " + - $"thI={rotateParams.CompThIFac:0.###} thMax={rotateParams.CompThMax:0.#} " + - $"frac={rotateParams.CompTangentFrac:0.###} activeOmega={rotateParams.ActiveOmega:0.###} " + - $"startTol={rotateParams.StartWheelAlignDeg:0.#} activeTol={rotateParams.ActiveWheelAlignDeg:0.#}) " + - $"fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum}"); - - // 仅主车:读取两车实际 sim 位姿,量化"开环横向滑移"来源(节流 ~200ms)。 - if (isMaster && Conf.MultiVehicleRotatePoseWebApiDiagEnabled) - LogRotatePoseSample(); - } - else - { - _mvRotCompVx = 0; - _mvRotCompVy = 0; - _mvRotCompOmega = 0; - MultiVehicleRotateFleetReady = true; - // Reset rotate-only PI state outside rotate mode. - _rotIntegX = _rotIntegY = _rotIntegTh = 0; - _rotPiLastTime = DateTime.MinValue; - _rotPoseEpisode = false; - _rotPosePrevTime = DateTime.MinValue; - ResetMultiVehicleRotateCenterDrift(); - // 常规/蟹行:蟹行时 frontTh==rearTh(四轮同向)即为平移,与常规共用同一下发路径。 - var motionOk = chassis.SendMotion(fleetVx, fleetFrontTh, fleetRearTh, localControlRadius: controlRadius, - localCompensateX: xDetectCompensate + xPosCompensate, - localCompensateY: yDetectCompensate + yPosCompensate, - localCompensateTh: thDetectCompensate + thPosCompensate); - if (motionOk) - MultiVehicleRotateWheelsReady = TryCheckRotateWheelAlignment(chassis, - Math.Max(0.1f, Conf.FleetCrabStartWheelAlignDeg), out _multiVehicleRotateAlignDetail); - else - { - MultiVehicleRotateWheelsReady = false; - _multiVehicleRotateAlignDetail = chassis.LastMotionDecomposeFailureReason; - } - MultiVehicleRotateFleetReady = true; - SetMultiVehicleMotionFeasible(motionOk, motionOk ? "" : chassis.LastMotionDecomposeFailureReason); - if (!motionOk) - { - AddFleetStop($"car{CarNum} chassis motion infeasible: {chassis.LastMotionDecomposeFailureReason}", - CarNum); - canMove = false; - LogMultiVehicleStop(fleetStopReason, true); - } - LogMultiVehicleRemoteDecision( - $"SEND_MOTION master={isMaster} manual={manualEnabled} canMove={canMove} ready={fleetReady} ok={motionOk} " + - $"mode={fleetMode} vx={fleetVx:0.000} fTh={fleetFrontTh:0.00} rTh={fleetRearTh:0.00} " + - $"comp=({xDetectCompensate + xPosCompensate:0.0},{yDetectCompensate + yPosCompensate:0.0},{thDetectCompensate + thPosCompensate:0.000}) " + - $"wheelReady={MultiVehicleRotateWheelsReady} align={_multiVehicleRotateAlignDetail} " + - $"fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum}"); - } - } - - // === 诊断日志(节流 ~300ms)=== - // 用于定位“剧烈运动”来源:BASE=遥控基础速度;DETECT=2腿检测补偿;POS=SLAM编队补偿;SEND=最终下发。 - // 若 BASE≈0 但 SEND 持续非零,说明是补偿在驱动;再看 DETECT/POS 哪一路的 comp 持续非零即定位到来源。 - // 同时落 DLog(tag MultiVehicleDbg) 与屏幕 ToastText(tag MultiVehicle{CarNum}-dbg),后者无需开启磁盘转储即可直接观察。 - if ((DateTime.Now - _mvDbgLastLog).TotalMilliseconds >= 300) - { - _mvDbgLastLog = DateTime.Now; - var spacingStr = float.IsNaN(currentSpacing) ? "NaN" : currentSpacing.ToString("F0"); - var cx = xDetectCompensate + xPosCompensate; - var cy = yDetectCompensate + yPosCompensate; - var cth = thDetectCompensate + thPosCompensate; - var actualCenterThForDiag = inferredFleetCenterValid ? inferredFleetCenterTh : CenterTh; - var idealHeadingErr = (float)CommonMath.ThDiff(MultiVehicleAutoIdealTh, actualCenterThForDiag); - var idealHeadingErrReverse = (float)CommonMath.ThDiff(actualCenterThForDiag, MultiVehicleAutoIdealTh); - var frontRearDiff = (float)CommonMath.ThDiff(fleetFrontTh, fleetRearTh); - var commandHeadingSplit = frontRearDiff / 2f; - var commandBaseTh = (float)CommonMath.RoundTh(fleetRearTh + commandHeadingSplit); - var supposedPosText = supposedPosForDiag == null - ? "N/A" - : $"({supposedPosForDiag.Item1:0},{supposedPosForDiag.Item2:0},{supposedPosForDiag.Item3:0.0})"; - var crabDbg = isMaster && manualEnabled && fleetMode == 1 - ? $"| CRAB speed:{crabInputVx:F3} steerRatio:{crabInputVy:F3} raw:{crabRawAngle:F2} limit:{crabSteerLimit:F1} rev:{crabReverseEquivalent} " - : ""; - - var dbg = - $"car{CarNum} master:{isMaster} manual:{manualEnabled} auto:{autoEnabled} slam:{slamRead} corr:{useDetourCorrection} fleetPos:{fleetPosValid} " + - $"ready:{fleetReady}({fleetCount}/{Conf.MultiVehicleFleetNum}) canMove:{canMove} stop:{fleetStopActive} stopReason:{fleetStopReason} useDetect:{Conf.MultiVehicleUseDetect} " + - $"| BASE vx:{fleetVx:F3} fTh:{fleetFrontTh:F2} rTh:{fleetRearTh:F2} " + - crabDbg + - $"| DETECT valid:{detectValid} center({_mvLastDetCenterX:F0},{_mvLastDetCenterY:F0}) dir:{_mvLastDetDir:F1} ndist:{_mvLastDetDist:F0} " + - $"dx:{detectDx:F0} dy:{detectDy:F0} dth:{detectDth:F2} spacing:{spacingStr}/{syncDistance:F0} delta:{deltaDetectCenter:F0} guessX:{Conf.TwoLegGuessX:F0} outBias({Conf.TwoLegOutputBiasX:F0},{Conf.TwoLegOutputBiasY:F0}) " + - $"-> comp x:{xDetectCompensate:F1} y:{yDetectCompensate:F1} th:{thDetectCompensate:F2} " + - $"fac({Conf.MultiVehicleDetectBiasXFac:F2},{Conf.MultiVehicleDetectBiasYFac:F2},{Conf.MultiVehicleDetectBiasThFac:F2}) " + - $"lim({Conf.MultiVehicleDetectBiasXThreshold:F1},{Conf.MultiVehicleDetectBiasYThreshold:F1},{Conf.MultiVehicleDetectBiasThThreshold:F1}) " + - $"| POS self({selfX:F0},{selfY:F0},{selfTh:F1}) center({CenterX:F0},{CenterY:F0},{CenterTh:F1}) " + - $"bias({posBiasX:F0},{posBiasY:F0},{posBiasTh:F1}) -> comp x:{xPosCompensate:F1} y:{yPosCompensate:F1} th:{thPosCompensate:F2} " + - $"fac({Conf.MultiVehiclePosBiasXFac:F2},{Conf.MultiVehiclePosBiasYFac:F2},{Conf.MultiVehiclePosBiasThFac:F2}) " + - $"lim({Conf.MultiVehiclePosBiasXThreshold:F1},{Conf.MultiVehiclePosBiasYThreshold:F1},{Conf.MultiVehiclePosBiasThThreshold:F1}) " + - $"| LAYOUT({layoutX:F0},{layoutY:F0},{layoutTh:F0}) R:{syncDistance / 2f:F0} " + - $"| SEND mode:{fleetMode} omega:{fleetOmega:F1} vx:{fleetVx:F3} fTh:{fleetFrontTh:F2} rTh:{fleetRearTh:F2} cx:{cx:F1} cy:{cy:F1} cth:{cth:F2} " + - $"rotComp(vx:{_mvRotCompVx:F1} vy:{_mvRotCompVy:F1} om:{_mvRotCompOmega:F2}) " + - $"| ROT_ALIGN hold:{rotateHoldForAlignment} wheelReady:{MultiVehicleRotateWheelsReady} fleetReady:{MultiVehicleRotateFleetReady} " + - $"unaligned:[{rotateUnalignedCars}] reqOmega:{requestedFleetOmega:F1} detail:{_multiVehicleRotateAlignDetail}"; - - DLog.Log(dbg, "MultiVehicleDbg"); - FleetDiag(dbg); - - if (autoMode || autoEnabled || MultiVehicleAutoEnabled) - DLog.Log( - $"APPLY car{CarNum} master:{isMaster} autoMode:{autoMode} manual:{manualEnabled} " + - $"cmd(vx:{fleetVx:0.000},f:{fleetFrontTh:0.00},r:{fleetRearTh:0.00},base:{commandBaseTh:0.00},split:{commandHeadingSplit:0.00},f-r:{frontRearDiff:0.00}) " + - $"ideal(has:{MultiVehicleAutoHasIdeal},x:{MultiVehicleAutoIdealX:0},y:{MultiVehicleAutoIdealY:0},th:{MultiVehicleAutoIdealTh:0.00}) " + - $"centerPublished({CenterX:0},{CenterY:0},{CenterTh:0.00}) " + - $"centerInferred(valid:{inferredFleetCenterValid},x:{inferredFleetCenterX:0},y:{inferredFleetCenterY:0},th:{inferredFleetCenterTh:0.00}) " + - $"idealHeadingErr(target-actual):{idealHeadingErr:0.00} reverse(actual-target):{idealHeadingErrReverse:0.00} " + - $"layout({layoutX:0},{layoutY:0},{layoutTh:0.00}) self({selfX:0},{selfY:0},{selfTh:0.00}) supposed:{supposedPosText} " + - $"detectDth:{detectDth:0.00}->comp:{thDetectCompensate:0.00} " + - $"posBiasTh:{posBiasTh:0.00}->comp:{thPosCompensate:0.00} " + - $"totalLocalComp(x:{cx:0.0},y:{cy:0.0},th:{cth:0.00}) " + - $"useDetourCorr:{useDetourCorrection} slam:{slamRead} fleetPos:{fleetPosValid} ready:{fleetReady}", - "FleetCrabHeadingDbg"); - - // 屏幕分两行显示,便于直接观察(无需开启 DLog 磁盘转储) - Hedingben.ToastText( - $"BASE vx{fleetVx:F3} fTh{fleetFrontTh:F1} | SEND vx{fleetVx:F3} c({cx:F0},{cy:F0},{cth:F1}) " + - $"| ready{fleetReady} canMove{canMove} stop{fleetStopActive}", - $"MultiVehicle{CarNum}-dbg"); - Hedingben.ToastText( - $"DET v{detectValid} dx{detectDx:F0} dy{detectDy:F0} dth{detectDth:F1} cmp({xDetectCompensate:F0},{yDetectCompensate:F0},{thDetectCompensate:F1}) " + - $"| POS bias({posBiasX:F0},{posBiasY:F0},{posBiasTh:F1}) cmp({xPosCompensate:F0},{yPosCompensate:F0},{thPosCompensate:F1})", - $"MultiVehicle{CarNum}-dbg2"); - } - - if (isMaster) - { - lock (FleetLock) - { - MultiVehicleFleet[CarNum] = BuildSelfInfo(true, slamRead, selfX, selfY, selfTh, - layoutX, layoutY, layoutTh, selfAligned, ownDetectOk); - } - - if (fleetReady || fleetStopActive) - { - VehicleSyncNotification notification; - lock (FleetLock) - { - notification = new VehicleSyncNotification - { - // 广播"整队姿态有效":主车读到全局定位即为真,从车据此放行自动模式安全门。 - PosAvailable = slamRead, - CenterX = CenterX, - CenterY = CenterY, - CenterTh = CenterTh, - Aligned = MultiVehicleAligned, - Fleet = new Dictionary(MultiVehicleFleet), - FleetVx = fleetStopActive ? 0 : fleetVx, - FleetFrontTh = fleetStopActive ? 0 : fleetFrontTh, - FleetRearTh = fleetStopActive ? 0 : fleetRearTh, - Mode = fleetMode, - FleetOmega = fleetStopActive ? 0 : fleetOmega, - RequestedFleetOmega = fleetMode == 2 && !fleetStopActive ? requestedFleetOmega : 0, - FleetMotionReleased = !fleetStopActive && !(fleetMode == 2 && rotateHoldForAlignment), - FleetStopActive = fleetStopActive, - FleetStopReason = fleetStopReason, - FleetStopSourceCar = fleetStopSourceCar, - AutoEnabled = MultiVehicleAutoEnabled, - // 脚本驱动等价于手动联动,广播为 ManualEnabled 让从车解锁跟随。 - ManualEnabled = manualEnabled, - UseDetourCorrection = useDetourCorrection, - SyncTh = syncTh, - SyncDistance = syncDistance, - DeltaDetectCenter = deltaDetectCenter, - // F: 单调递增序列号(从 1 起),从车据此丢弃乱序旧包。 - Seq = ++_multiVehicleNotifySeq, - // D: 透传理想车队中心(仅自动模式且控制器给出时有效)。 - HasIdeal = !manualEnabled && MultiVehicleAutoEnabled && - Conf.MultiVehicleAutoUseIdealCenter && MultiVehicleAutoHasIdeal, - IdealX = MultiVehicleAutoIdealX, - IdealY = MultiVehicleAutoIdealY, - IdealTh = MultiVehicleAutoIdealTh - }; - FillRotateNotificationParams(notification, rotateParams); - } - - LogMultiVehicleRemoteDecision( - $"NOTIFY_SEND seq={notification.Seq} mode={notification.Mode} " + - $"omega={notification.FleetOmega:0.000} reqOmega={notification.RequestedFleetOmega:0.000} " + - $"released={notification.FleetMotionReleased} stop={notification.FleetStopActive} " + - $"reason={notification.FleetStopReason} useDetourCorr={notification.UseDetourCorrection} " + - $"fleetCnt={notification.Fleet.Count}/{Conf.MultiVehicleFleetNum} " + - $"rotParam(valid={notification.RotateParamsValid} xyP={notification.RotateCompXyFac:0.###} " + - $"xyI={notification.RotateCompXyIFac:0.###} xyMax={notification.RotateCompXyMax:0.#} " + - $"thP={notification.RotateCompThFac:0.###} thI={notification.RotateCompThIFac:0.###} " + - $"thMax={notification.RotateCompThMax:0.#} frac={notification.RotateCompTangentFrac:0.###} " + - $"startTol={notification.RotateStartWheelAlignDeg:0.#} activeTol={notification.RotateActiveWheelAlignDeg:0.#})"); - - foreach (var kv in notification.Fleet) - { - var ip = kv.Value.Ip; - var port = kv.Value.Port > 0 ? kv.Value.Port : WebAPI.port; - if (string.IsNullOrWhiteSpace(ip) || kv.Key == CarNum) - continue; - FireAndForgetNotify(hc, ip, port, notification); - } - } - } - else - { - FireAndForgetRegister(hc, BuildSelfInfo(false, slamRead, selfX, selfY, selfTh, - layoutX, layoutY, layoutTh, selfAligned, ownDetectOk)); - } - } - - private bool IsMultiVehicleMaster() => Conf.MultiVehicleMasterEndpoint == "/"; - - /// - /// C: 剔除超过 TTL 未刷新(register/notify)的 fleet 成员。调用方须已持有 FleetLock。 - /// 从车崩溃/断网后其条目过期被删除,使 fleetReady(数量==总数) 同时蕴含"全部成员在线且新鲜", - /// 主车不再基于过期 layout/DetectOk 继续 SendMotion+notify。 - /// - private void PruneStaleFleetMembers() - { - var ttlMs = Conf.MultiVehicleMemberTtlMs > 0 - ? Conf.MultiVehicleMemberTtlMs - : Math.Max(500, Conf.MultiVehicleSyncInterval * 6); - var now = DateTime.Now; - var stale = MultiVehicleFleet.Keys - .Where(k => k != CarNum && - (!_multiVehicleFleetSeen.TryGetValue(k, out var seen) || - (now - seen).TotalMilliseconds > ttlMs)) - .ToList(); - foreach (var k in stale) - { - MultiVehicleFleet.Remove(k); - _multiVehicleFleetSeen.Remove(k); - Hedingben.ToastText($"剔除掉线成员 car{k}({ttlMs:F0}ms 未刷新)", $"MultiVehicle{CarNum}-prune"); - } - } - - private bool FleetHasPosAvailable() - { - lock (FleetLock) - return MultiVehicleFleet.Values.Any(v => v.PosAvailable); - } - - // 基于 SLAM 世界坐标的最近邻车间距(mm);无可用定位邻车时返回 NaN。 - private float TryGetSlamSpacing(float selfX, float selfY) - { - List others; - lock (FleetLock) - others = MultiVehicleFleet - .Where(kv => kv.Key != CarNum && kv.Value.PosAvailable) - .Select(kv => kv.Value).ToList(); - - if (others.Count == 0) return float.NaN; - return others.Min(o => (float)Math.Sqrt((o.X - selfX) * (o.X - selfX) + (o.Y - selfY) * (o.Y - selfY))); - } - - private bool TryInferFleetCenter(out float centerX, out float centerY, out float centerTh) - { - centerX = centerY = centerTh = 0; - List positioned; - lock (FleetLock) - positioned = MultiVehicleFleet.Values.Where(v => v.PosAvailable).ToList(); - - if (positioned.Count == 0) return false; - - var centers = positioned.Select(InferFleetCenterFromCar).ToList(); - centerX = centers.Average(c => c.Item1); - centerY = centers.Average(c => c.Item2); - var avgSin = centers.Average(c => Math.Sin(c.Item3 * Math.PI / 180.0)); - var avgCos = centers.Average(c => Math.Cos(c.Item3 * Math.PI / 180.0)); - centerTh = (float)(Math.Atan2(avgSin, avgCos) * 180.0 / Math.PI); - return true; - } - - public bool TryGetFleetCenterFromMembers(out float centerX, out float centerY, out float centerTh) - { - return TryInferFleetCenter(out centerX, out centerY, out centerTh); - } - - private static (float, float, float) InferFleetCenterFromCar(VehicleSyncInfo car) - { - // 由 carWorld = Transform2D(center, layout) 反推 center = carWorld ∘ layout⁻¹。 - // 注意 layout⁻¹ 是真正的 SE(2) 逆,而非逐分量取负 (-layout): - // 当 layoutTh≠0(如从车 180°)时,逐分量取负会把平移方向算错,导致车队中心偏移 → 位姿补偿跑飞。 - var layout = Tuple.Create((double)car.LayoutX, (double)car.LayoutY, (double)car.LayoutTh); - var layoutInv = LessMath.SolveTransform2D(layout, Tuple.Create(0.0, 0.0, 0.0)); - var center = LessMath.Transform2D( - Tuple.Create((double)car.X, (double)car.Y, (double)car.Th), - layoutInv); - return ((float)center.Item1, (float)center.Item2, (float)center.Item3); - } - - /// - /// 由本车(通常为主车)当前 Detour SLAM 位姿反推车队中心位姿(世界系)。 - /// 复用 GetLayoutPose + InferFleetCenterFromCar 的同款 SE(2) 反推(center = carWorld ∘ layout⁻¹), - /// 保证与自动模式车队中心计算一致。供动作(如 FleetCrabWalk)取路径起点用;无有效定位时返回 false。 - /// - public bool TryGetFleetCenterFromSlam(out float centerX, out float centerY, out float centerTh) - { - centerX = centerY = centerTh = 0; - var carPos = DetourInterface.getCartLocation(); - return TryGetFleetCenterFromPose((float)carPos.x, (float)carPos.y, (float)carPos.th, - out centerX, out centerY, out centerTh); - } - - public bool TryGetFleetCenterFromPose(float carX, float carY, float carTh, - out float centerX, out float centerY, out float centerTh) - { - centerX = centerY = centerTh = 0; - var (layoutX, layoutY, layoutTh) = GetLayoutPose(Conf.TestCarSyncTh, Conf.TestCarSyncDistance); - var self = new VehicleSyncInfo - { - X = carX, - Y = carY, - Th = carTh, - LayoutX = layoutX, - LayoutY = layoutY, - LayoutTh = layoutTh - }; - var (cx, cy, cth) = InferFleetCenterFromCar(self); - centerX = cx; - centerY = cy; - centerTh = cth; - return true; - } - - /// - /// 预热:用主车 SLAM 位姿,立即(1) 对外发布有效"车队中心快照",(2) 把主车自身以 posAvailable=true - /// 写入 fleet 表。供 FleetCrabWalk 等动作在启动控制器 Track() 前调用,解决两个启动期问题: - /// - 控制器首帧通过 MultiVehicleGetFleetPos 读到 (0,0,0) → 误判已到终点、立即结束; - /// - 自动联动循环的安全门 FleetHasPosAvailable(PilotDefinition.cs ~402)在冷启动(无车上报定位)时 - /// 会把 AutoEnabled 关掉、循环无法发布真实中心。播种主车 posAvailable 条目即可越过该门,使循环正常接管。 - /// 需在主车上调用;getCartLocation() 无定位时会阻塞(与自动模式一致)。 - /// - public bool PrimeMasterAutoFromSlam() - { - var carPos = DetourInterface.getCartLocation(); - var (lx, ly, lth) = GetLayoutPose(Conf.TestCarSyncTh, Conf.TestCarSyncDistance); - var info = BuildSelfInfo(true, true, (float)carPos.x, (float)carPos.y, (float)carPos.th, - lx, ly, lth, false); - var (cx, cy, cth) = InferFleetCenterFromCar(info); - PublishFleetCenter(cx, cy, cth); - lock (FleetLock) - { - MultiVehicleFleet[CarNum] = info; - _multiVehicleFleetSeen[CarNum] = DateTime.Now; - } - return true; - } - - public long BeginFleetMotionWarmup() - { - MultiVehicleRotateWheelsReady = false; - MultiVehicleRotateFleetReady = false; - _multiVehicleRotateAlignDetail = "fleet motion warmup pending"; - return Interlocked.Read(ref _multiVehicleNotifySeq); - } - - public bool IsFleetMotionWarmupReady(DateTime warmStartTime, long notificationSeqBaseline, - float syncTh, float syncDistance, out string detail) - { - var pending = new List(); - var requiredCount = Math.Max(1, Conf.MultiVehicleFleetNum); - lock (FleetLock) - { - if (MultiVehicleFleet.Count != requiredCount) - pending.Add($"fleetCnt={MultiVehicleFleet.Count}/{requiredCount}"); - - foreach (var kv in MultiVehicleFleet.OrderBy(k => k.Key)) - { - var carNum = kv.Key; - var info = kv.Value; - if (!_multiVehicleFleetSeen.TryGetValue(carNum, out var seen) || seen < warmStartTime) - pending.Add($"car{carNum}:notFresh"); - - var (layoutX, layoutY, layoutTh) = GetLayoutPoseForCar(carNum, syncTh, syncDistance); - if (Math.Abs(info.LayoutX - layoutX) > 1f || - Math.Abs(info.LayoutY - layoutY) > 1f || - Math.Abs(CommonMath.ThDiff(info.LayoutTh, layoutTh)) > 1f) - pending.Add( - $"car{carNum}:layout=({info.LayoutX:0},{info.LayoutY:0},{info.LayoutTh:0.0})"); - - if (!info.MotionFeasible) - pending.Add($"car{carNum}:motion={info.MotionInfeasibleReason}"); - if (!info.RotateWheelsAligned) - pending.Add($"car{carNum}:wheel={info.RotateWheelAlignDetail}"); - if (carNum != CarNum && info.AppliedNotificationSeq <= notificationSeqBaseline) - pending.Add($"car{carNum}:seq={info.AppliedNotificationSeq}<={notificationSeqBaseline}"); - } - } - - detail = pending.Count == 0 - ? $"ready seqBase={notificationSeqBaseline}" - : string.Join("; ", pending); - return pending.Count == 0; - } - - private (float, float, float) GetLayoutPose(float syncTh, float syncDistance) - => GetLayoutPoseForCar(CarNum, syncTh, syncDistance); - - private static (float, float, float) GetLayoutPoseForCar(int carNum, float syncTh, float syncDistance) - { - // Two-car layout: members are mirrored around the fleet center; car2 faces 180 deg away. - var rad = syncTh / 180f * Math.PI; - var sign = carNum == 1 ? 1f : -1f; - var xx = (float)(Math.Cos(rad) * syncDistance / 2 * sign); - var yy = (float)(Math.Sin(rad) * syncDistance / 2 * sign); - return (xx, yy, syncTh + (carNum == 1 ? 0 : 180)); - } - - private VehicleSyncInfo BuildSelfInfo(bool master, bool posAvailable, float x, float y, float th, - float layoutX, float layoutY, float layoutTh, bool aligned, bool detectOk = true) - { - ResolveMultiVehicleSelfEndpoint(out var selfIp, out var selfPort); - - return new VehicleSyncInfo - { - Master = master, - Ip = selfIp, - Port = selfPort, - PosAvailable = posAvailable, - X = x, - Y = y, - Th = th, - LayoutX = layoutX, - LayoutY = layoutY, - LayoutTh = layoutTh, - Aligned = aligned, - DetectOk = detectOk, - MotionFeasible = _multiVehicleMotionFeasible, - MotionInfeasibleReason = _multiVehicleMotionInfeasibleReason, - RotateWheelsAligned = MultiVehicleRotateWheelsReady, - RotateWheelAlignDetail = _multiVehicleRotateAlignDetail, - AppliedNotificationSeq = _multiVehicleAppliedSeq - }; - } - - private void VisualizeFleet(List contour, float egoLayoutX, float egoLayoutY, float egoLayoutTh) - { - // 画在本车车体系:global=false,渲染时由 ClumsyLite 叠加本车真实位姿。 - // 每个成员按“相对本车 layout 的位姿”绘制(memberInEgo = egoLayout⁻¹ ∘ memberLayout), - // 这样手动模式无 SLAM 定位也能正确显示编队相对关系;避免之前用车队系 layout 坐标叠加本车位姿造成的整体平移。 - var fleetPainter = UI.GetPainter("MultiVehicleFleet-vis", false); - Dictionary fleet; - lock (FleetLock) - fleet = new Dictionary(MultiVehicleFleet); - - var egoLayout = Tuple.Create((double)egoLayoutX, (double)egoLayoutY, (double)egoLayoutTh); - - Tuple MemberInEgo(float mx, float my, float mth) - => LessMath.SolveTransform2D(egoLayout, Tuple.Create((double)mx, (double)my, (double)mth)); - - void DrawContour(Color color, Tuple rel) - { - var transformed = contour - .Select(pp => LessMath.Transform2D((float)rel.Item1, (float)rel.Item2, (float)rel.Item3, pp)) - .ToList(); - for (var i = 0; i < 4; ++i) - fleetPainter.DrawLine(color, transformed[i], transformed[(i + 1) % 4]); - // 对角线便于辨认朝向 - fleetPainter.DrawLine(color, transformed[0], transformed[2]); - fleetPainter.DrawLine(color, transformed[1], transformed[3]); - } - - fleetPainter.Clear(); - foreach (var kvp in fleet) - { - var color = kvp.Value.Master ? Color.Red : Color.Orange; - var rel = MemberInEgo(kvp.Value.LayoutX, kvp.Value.LayoutY, kvp.Value.LayoutTh); - DrawContour(color, rel); - fleetPainter.DrawText(color, $"{kvp.Key}", new Vector((float)rel.Item1, (float)rel.Item2)); - } - } - - private void FireAndForgetRegister(HttpClient hc, VehicleSyncInfo info) - { - try - { - ParseMasterEndpoint(out var masterIp, out var masterPort); - var payload = VehicleSyncBinaryCodec.EncodeRegister(CarNum, info); - var url = $"http://{masterIp}:{masterPort}/multi-vehicle-register-bin"; - _ = PostBinaryAsync(hc, url, payload, "register"); - } - catch (Exception e) - { - DLog.Log($"register binary encode failed: {e.GetBaseException().Message}", "MultiVehicle"); - } - } - - private void FireAndForgetNotify(HttpClient hc, string ip, int port, VehicleSyncNotification notification) - { - try - { - var payload = VehicleSyncBinaryCodec.EncodeNotification(notification); - var url = $"http://{ip}:{port}/multi-vehicle-notify-bin"; - _ = PostBinaryAsync(hc, url, payload, $"notify {ip}:{port}"); - } - catch (Exception e) - { - DLog.Log($"notify {ip}:{port} binary encode failed: {e.GetBaseException().Message}", "MultiVehicle"); - } - } - - private static async Task PostBinaryAsync(HttpClient hc, string url, byte[] payload, string description) - { - try - { - using var content = new ByteArrayContent(payload); - content.Headers.ContentType = - new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); - using var response = await hc.PostAsync(url, content).ConfigureAwait(false); - if (response.IsSuccessStatusCode) - return; - - DLog.Log( - $"{description} binary failed: HTTP {(int)response.StatusCode} {response.ReasonPhrase}", - "MultiVehicle"); - } - catch (Exception e) - { - DLog.Log($"{description} binary failed: {e.GetBaseException().Message}", "MultiVehicle"); - } - } - - private void ResolveMultiVehicleSelfEndpoint(out string ip, out int port) - { - ip = "127.0.0.1"; - port = WebAPI.port > 0 ? WebAPI.port : 8008; - - var endpoint = Conf.MultiVehicleSelfEndpoint; - if (string.IsNullOrWhiteSpace(endpoint)) return; - - var parts = endpoint.Trim().Split(':'); - if (parts.Length >= 1 && !string.IsNullOrWhiteSpace(parts[0])) - ip = parts[0].Trim(); - if (parts.Length >= 2 && int.TryParse(parts[1], out var p) && p > 0) - port = p; - } - - private void ParseMasterEndpoint(out string ip, out int port) - { - ip = "127.0.0.1"; - port = 8008; - var endpoint = Conf.MultiVehicleMasterEndpoint ?? "/"; - if (endpoint == "/") return; - var parts = endpoint.Split(':'); - if (parts.Length >= 1 && !string.IsNullOrWhiteSpace(parts[0])) - ip = parts[0]; - if (parts.Length >= 2 && int.TryParse(parts[1], out var p)) - port = p; - } - - private static float ClampBias(float value, float threshold) - => Math.Sign(value) * Math.Min(Math.Abs(value), threshold); - - private RotateControlParams BuildRotateControlParams(VehicleSyncNotification notification) - { - var parameters = new RotateControlParams - { - ActiveOmega = Conf.MultiVehicleRotateActiveOmega, - CompXyFac = Conf.MultiVehicleRotateCompXyFac, - CompXyIFac = Conf.MultiVehicleRotateCompXyIFac, - CompXyMax = Conf.MultiVehicleRotateCompXyMax, - CompThFac = Conf.MultiVehicleRotateCompThFac, - CompThIFac = Conf.MultiVehicleRotateCompThIFac, - CompThMax = Conf.MultiVehicleRotateCompThMax, - CompTangentFrac = NormalizeRotateCompTangentFrac(Conf.MultiVehicleRotateCompTangentFrac), - StartWheelAlignDeg = Conf.InPlaceRotateWheelAlignDeg, - ActiveWheelAlignDeg = NormalizeRotateWheelAlignDeg(Conf.InPlaceRotateActiveWheelAlignDeg, - Conf.InPlaceRotateWheelAlignDeg) - }; - - if (notification == null || !notification.RotateParamsValid) - return parameters; - - parameters.ActiveOmega = notification.RotateActiveOmega; - parameters.CompXyFac = notification.RotateCompXyFac; - parameters.CompXyIFac = notification.RotateCompXyIFac; - parameters.CompXyMax = notification.RotateCompXyMax; - parameters.CompThFac = notification.RotateCompThFac; - parameters.CompThIFac = notification.RotateCompThIFac; - parameters.CompThMax = notification.RotateCompThMax; - parameters.CompTangentFrac = NormalizeRotateCompTangentFrac(notification.RotateCompTangentFrac); - parameters.StartWheelAlignDeg = NormalizeRotateWheelAlignDeg(notification.RotateStartWheelAlignDeg, - Conf.InPlaceRotateWheelAlignDeg); - parameters.ActiveWheelAlignDeg = NormalizeRotateWheelAlignDeg(notification.RotateActiveWheelAlignDeg, - parameters.StartWheelAlignDeg); - return parameters; - } - - private static void FillRotateNotificationParams(VehicleSyncNotification notification, RotateControlParams parameters) - { - notification.RotateParamsValid = true; - notification.RotateActiveOmega = parameters.ActiveOmega; - notification.RotateCompXyFac = parameters.CompXyFac; - notification.RotateCompXyIFac = parameters.CompXyIFac; - notification.RotateCompXyMax = parameters.CompXyMax; - notification.RotateCompThFac = parameters.CompThFac; - notification.RotateCompThIFac = parameters.CompThIFac; - notification.RotateCompThMax = parameters.CompThMax; - notification.RotateCompTangentFrac = parameters.CompTangentFrac; - notification.RotateStartWheelAlignDeg = parameters.StartWheelAlignDeg; - notification.RotateActiveWheelAlignDeg = parameters.ActiveWheelAlignDeg; - } - - private static float NormalizeRotateWheelAlignDeg(float value, float fallback) - { - if (float.IsNaN(value) || float.IsInfinity(value) || value <= 0) - return fallback; - return value; - } - - private static float NormalizeRotateCompTangentFrac(float frac) - { - if (float.IsNaN(frac) || float.IsInfinity(frac) || frac < 0) - return DefaultRotateCompTangentFrac; - return frac; - } - - private void LimitRotateCompensation(ref float compVx, ref float compVy, ref float compOmega, - float absOmega, float controlRadius, float tangentFrac) - { - var frac = NormalizeRotateCompTangentFrac(tangentFrac); - if (frac < 0 || absOmega <= 1e-6f) return; - - var rawVx = compVx; - var rawVy = compVy; - var rawOmega = compOmega; - var tangentMmps = absOmega / 180f * (float)Math.PI * Math.Max(1f, Math.Abs(controlRadius)); - var xyLimit = tangentMmps * frac; - var xyMag = (float)Math.Sqrt(compVx * compVx + compVy * compVy); - if (xyMag > xyLimit && xyMag > 1e-6f) - { - var scale = xyLimit / xyMag; - compVx *= scale; - compVy *= scale; - } - - var omegaLimit = absOmega * frac; - compOmega = ClampBias(compOmega, omegaLimit); - - var changed = Math.Abs(rawVx - compVx) > 1e-3f || - Math.Abs(rawVy - compVy) > 1e-3f || - Math.Abs(rawOmega - compOmega) > 1e-3f; - var now = DateTime.Now; - if (changed && (now - _mvRotateCompLimitLastLog).TotalMilliseconds >= 200) - { - _mvRotateCompLimitLastLog = now; - DLog.Log( - $"car{CarNum} ROTATE_COMP_LIMIT frac={frac:0.00} omega={absOmega:0.000} radius={controlRadius:0} " + - $"tan={tangentMmps:0.0} xyLimit={xyLimit:0.0} omLimit={omegaLimit:0.000} " + - $"raw=({rawVx:0.0},{rawVy:0.0},{rawOmega:0.000}) " + - $"limited=({compVx:0.0},{compVy:0.0},{compOmega:0.000})", - "MultiVehicleRemoteDbg"); - } - } - - private void ResetMultiVehicleRotateCenterDrift() - { - _mvRotateCenterDriftActive = false; - _mvRotateCenterMaxDrift = 0; - _mvRotateCenterLastLog = DateTime.MinValue; - } - - private void LogMultiVehicleRotateCenterDrift(bool isMaster, bool manualEnabled, bool autoEnabled, - bool fleetReady, bool canMove, bool rotating, float centerX, float centerY, float centerTh, - float requestedOmega, float fleetOmega, float compVx, float compVy, float compOmega) - { - if (!_mvRotateCenterDriftActive) - { - _mvRotateCenterDriftActive = true; - _mvRotateCenterStartX = centerX; - _mvRotateCenterStartY = centerY; - _mvRotateCenterStartTh = centerTh; - _mvRotateCenterMaxDrift = 0; - _mvRotateCenterLastLog = DateTime.MinValue; - } - - var dx = centerX - _mvRotateCenterStartX; - var dy = centerY - _mvRotateCenterStartY; - var drift = (float)Math.Sqrt(dx * dx + dy * dy); - _mvRotateCenterMaxDrift = Math.Max(_mvRotateCenterMaxDrift, drift); - var dth = (float)CommonMath.ThDiff(centerTh, _mvRotateCenterStartTh); - var now = DateTime.Now; - if ((now - _mvRotateCenterLastLog).TotalMilliseconds < 250) - return; - - _mvRotateCenterLastLog = now; - DLog.Log( - $"car{CarNum} CTRL master={isMaster} manual={manualEnabled} auto={autoEnabled} " + - $"ready={fleetReady} canMove={canMove} rotating={rotating} " + - $"center=({centerX:0.0},{centerY:0.0},{centerTh:0.00}) " + - $"start=({_mvRotateCenterStartX:0.0},{_mvRotateCenterStartY:0.0},{_mvRotateCenterStartTh:0.00}) " + - $"drift=({dx:0.0},{dy:0.0}) dist={drift:0.0} max={_mvRotateCenterMaxDrift:0.0} dth={dth:0.00} " + - $"omegaReq={requestedOmega:0.000} omega={fleetOmega:0.000} comp=({compVx:0.0},{compVy:0.0},{compOmega:0.000})", - "FleetRotateCenterDbg"); - } - - /// - /// 原地旋转纠偏单轴 PI 控制器。err 为车体系偏差(mm 或 deg),输出为修正速度(mm/s 或 deg/s), - /// 带死区、积分抗饱和(限制积分贡献在 ±max 内)与总输出限幅。死区内冻结积分(保留稳态修正以抵消恒定扰动)。 - /// - private static float RotatePiTerm(float err, ref float integ, float deadband, - float pFac, float iFac, float max, float dt, bool allowIntegrate = true) - { - // 死区内:不再累加误差,仅输出已积累的积分项(维持对恒定扰动的稳态补偿)。 - if (Math.Abs(err) < deadband) - return ClampBias(integ * iFac, max); - - // 条件积分(抗 windup):仅当 (a) 允许积分(舵轮已对齐、车在真正转动) 且 - // (b) 总输出未在同向饱和 时才累加误差。 - // allowIntegrate=false:上一拍舵轮未对齐(gate=0、车没动),此时积分误差是纯 windup, - // 会让纠偏越积越大、舵轮更对不齐 → 原地卡死;故冻结积分(保留已有值,仅输出 P+已积分)。 - // 同向饱和停积分:起步大误差让 P 顶满 max 时继续积分会顶满积分器,误差反向后泄放慢、造成过冲回拉。 - var unclamped = err * pFac + integ * iFac; - var saturatedSameSign = Math.Abs(unclamped) >= max && Math.Sign(unclamped) == Math.Sign(err); - if (allowIntegrate && !saturatedSameSign) - integ += err * dt; - - var iTerm = integ * iFac; - // 抗积分饱和:把积分贡献限制在 ±max,并反算回写积分器,避免 windup。 - if (iFac > 1e-9f) - { - var iLimit = max / iFac; - if (integ > iLimit) integ = iLimit; - else if (integ < -iLimit) integ = -iLimit; - iTerm = integ * iFac; - } - - return ClampBias(err * pFac + iTerm, max); - } - - /// - /// 主车专用:通过 Playground WebAPI 读取本车与邻车的真实 sim 位姿,落 RotatePoseDbg 日志。 - /// 量化"开环横向滑移"来源: - /// - w1/w2/dw:两车实际偏航角速率(deg/s)。dw 持续非零 ⇒ 主从转速不一致(相对旋转)。 - /// - rel_in1(x,y,dyaw):邻车在本车体系下的真实相对位姿;dyaw=实际相对朝向−180°。 - /// dyaw≈0 但 y 持续漂 ⇒ 纯横向平移滑移(非角度滞后,印证 B 无用)。 - /// - midDrift:车队几何中心(世界系)相对起转瞬间的位移 ⇒ 整体平移漂移量。 - /// 节流 ~200ms;WebAPI 异常时静默跳过,不影响控制环。 - /// - private void LogRotatePoseSample() - { - if (!Conf.MultiVehicleRotatePoseWebApiDiagEnabled) return; - - var now = DateTime.Now; - if ((now - _rotPoseLastLog).TotalMilliseconds < 200) return; - _rotPoseLastLog = now; - - var neighbor = Conf.PlaygroundNeighborRobotName; - if (string.IsNullOrWhiteSpace(neighbor) || neighbor == Conf.PlaygroundRobotName) return; - - try - { - var p1 = PlaygroundWebApi.GetPose(Conf.PlaygroundWebApiUrl, Conf.PlaygroundRobotName); - var p2 = PlaygroundWebApi.GetPose(Conf.PlaygroundWebApiUrl, neighbor); - - // 邻车在本车(car1)体系下的相对位姿:rel = R(-yaw1)·(p2-p1) - var ddx = p2.X - p1.X; - var ddy = p2.Y - p1.Y; - var th1 = p1.YawDeg * (float)Math.PI / 180f; - var c = (float)Math.Cos(th1); - var s = (float)Math.Sin(th1); - var relX = ddx * c + ddy * s; - var relY = -ddx * s + ddy * c; - var relYaw = (float)LessMath.ThDiff(p2.YawDeg - p1.YawDeg, 180f); // 实际相对朝向偏离 180° 的量 - var dist = (float)Math.Sqrt(ddx * ddx + ddy * ddy); - - var midX = (p1.X + p2.X) / 2f; - var midY = (p1.Y + p2.Y) / 2f; - if (!_rotPoseEpisode) - { - _rotPoseEpisode = true; - _rotPoseMid0X = midX; - _rotPoseMid0Y = midY; - } - var midDx = midX - _rotPoseMid0X; - var midDy = midY - _rotPoseMid0Y; - - // 实际偏航角速率(数值微分) - float w1 = 0, w2 = 0, dw = 0; - if (_rotPosePrevTime != DateTime.MinValue) - { - var dt = (float)(now - _rotPosePrevTime).TotalSeconds; - if (dt > 1e-3) - { - w1 = (float)LessMath.ThDiff(p1.YawDeg, _rotPosePrevYaw1) / dt; - w2 = (float)LessMath.ThDiff(p2.YawDeg, _rotPosePrevYaw2) / dt; - dw = w1 - w2; - } - } - _rotPosePrevTime = now; - _rotPosePrevYaw1 = p1.YawDeg; - _rotPosePrevYaw2 = p2.YawDeg; - - DLog.Log( - $"car1({p1.X:F0},{p1.Y:F0},{p1.YawDeg:F2}) car2({p2.X:F0},{p2.Y:F0},{p2.YawDeg:F2}) " + - $"| rel_in1(x:{relX:F0} y:{relY:F0} dyaw:{relYaw:F2}) dist:{dist:F0} " + - $"| mid({midX:F0},{midY:F0}) midDrift(dx:{midDx:F0} dy:{midDy:F0} |d|:{Math.Sqrt(midDx * midDx + midDy * midDy):F0}) " + - $"| w1:{w1:F2} w2:{w2:F2} dw:{dw:F2}", - "RotatePoseDbg"); - } - catch (Exception e) - { - // 诊断用途,WebAPI 不通时静默(仅偶发提示),不打断控制环。 - Hedingben.ToastText($"RotatePose WebAPI err: {e.Message}", $"MultiVehicle{CarNum}-rotpose"); - } - } - - #endregion -} diff --git a/.task8-sweep/PlaygroundWebApi.cs b/.task8-sweep/PlaygroundWebApi.cs deleted file mode 100644 index 03ace8e..0000000 --- a/.task8-sweep/PlaygroundWebApi.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Net.Http; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace MultiWheelC; - -/// -/// Playground 仿真器 HTTP Web API 轻量客户端:查询小车位姿、瞬移小车。 -/// 服务端实现见 Playground/Web/PlaygroundWebApi.cs,默认监听 http://localhost:18090。 -/// 坐标单位 mm,朝向 yawDeg 单位为度,世界坐标系与场景 JSON 一致。 -/// -public static class PlaygroundWebApi -{ - // 禁用系统代理:本机 Playground 走 localhost,若经系统代理(如 127.0.0.1:7890)会连接失败。 - private static readonly HttpClient Http = new HttpClient(new HttpClientHandler { UseProxy = false }) - { - Timeout = TimeSpan.FromSeconds(3) - }; - - public struct Pose - { - public float X; - public float Y; - public float YawDeg; - } - - /// 查询单台小车的世界位姿。GET /api/robots/{name}。 - public static Pose GetPose(string baseUrl, string robotName) - { - var url = $"{baseUrl.TrimEnd('/')}/api/robots/{Uri.EscapeDataString(robotName)}"; - var json = Http.GetStringAsync(url).GetAwaiter().GetResult(); - var o = JObject.Parse(json); - return new Pose - { - X = o.Value("x"), - Y = o.Value("y"), - YawDeg = o.Value("yawDeg") - }; - } - - /// 将小车瞬移到目标世界位姿。POST /api/robots/{name}/move。 - public static void Move(string baseUrl, string robotName, float x, float y, float yawDeg) - { - var url = $"{baseUrl.TrimEnd('/')}/api/robots/{Uri.EscapeDataString(robotName)}/move"; - var body = JsonConvert.SerializeObject(new { x, y, yaw = yawDeg, stop = true }); - using var content = new StringContent(body, Encoding.UTF8, "application/json"); - var resp = Http.PostAsync(url, content).GetAwaiter().GetResult(); - resp.EnsureSuccessStatusCode(); - } - - /// 查询车辆运动是否启用(暂停时为 false)。GET /api/motion。 - public static bool MotionEnabled(string baseUrl) - { - var url = $"{baseUrl.TrimEnd('/')}/api/motion"; - var json = Http.GetStringAsync(url).GetAwaiter().GetResult(); - return JObject.Parse(json).Value("motionEnabled"); - } - - /// 恢复车辆运动。POST /api/motion/resume。 - public static void ResumeMotion(string baseUrl) - { - var url = $"{baseUrl.TrimEnd('/')}/api/motion/resume"; - var resp = Http.PostAsync(url, null).GetAwaiter().GetResult(); - resp.EnsureSuccessStatusCode(); - } - - /// - /// 暂停车辆运动(仅冻结运动,不停止仿真;传感器继续扫描)。POST /api/motion/pause。 - /// feedback: "zero"(默认,反馈归零) / "none"(不上报) / "hold"(保留暂停瞬间值)。 - /// - public static void PauseMotion(string baseUrl, string feedback = "zero") - { - var url = $"{baseUrl.TrimEnd('/')}/api/motion/pause"; - var body = JsonConvert.SerializeObject(new { feedback }); - using var content = new StringContent(body, Encoding.UTF8, "application/json"); - var resp = Http.PostAsync(url, content).GetAwaiter().GetResult(); - resp.EnsureSuccessStatusCode(); - } -} diff --git a/.task8-sweep/TireFollowing.cs b/.task8-sweep/TireFollowing.cs deleted file mode 100644 index 049d572..0000000 --- a/.task8-sweep/TireFollowing.cs +++ /dev/null @@ -1,511 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Numerics; -using System.Reflection; -using System.Text; -using System.Threading; -using ClumsyCore; -using ClumsyCore.DTools; -using ClumsyCore.Interfaces; -using ClumsyCore.Pilot; -using ClumsyCore.Utilities; -using ClumsyDance.ClumsyWalk.Detectors; -using CommonUsage.Chassis; -using FundamentalLib; -using MDCSToolBox; -using MDCSToolBox.Clumsy.Calibration; -using MDCSToolBox.Clumsy.MotionControllers; -using MDCSToolBox.Clumsy.Movements; -using MDCSToolBox.Clumsy.Pilot; -using MDCSToolBox.Clumsy.Tracks; -using MDCSToolBox.Commons.Controllers; -using static ClumsyCore.DTools.Painter; -using LineSegment = ClumsyCore.Utilities.LineSegment; - -namespace MultiWheelC -{ - public class TireFollowing : MovementDefinition - { - public Func GetController; - - /// - /// 车辆方向 - /// - public float CarDirection = 0; - - /// - /// 停止距离 - /// - //public float FinishDistance = 1000; - - /// - /// 减速距离 - /// - public float SlowDistance = 1000; - - /// - /// 最大速度 - /// - public float MaxSpeed = 0.3f; - - // 末段衔接:接近盲走终点时给非零速度,供后续动作连续接管 - public bool EnableHandover = false; - public float HandoverDistance = 200f; // mm - public float HandoverSpeed = 0.2f; // m/s - - /// - /// 钻轮胎数量 - /// - public int TireNum = 1; - - /// - /// 盲走角度偏移 - /// - public float WalkBlindTh = -1f; - - /// - /// 是否检测到目标 - /// - public bool NoTarget = false; - - public float GuessRangeX; - - public float GuessRangeY; - - /// - /// 检测器定义 - /// - public class DetectorDefinition - { - /// - /// 开始检测距离 - /// - public float StartGuessingX; - - /// - /// 开始检测距离 - /// - public float StartGuessingY; - - /// - /// 检测函数 - /// - public Func, LineSegment> DetectFunction = null; - - public Action LeaveSrcFunction = null; - - public int SrcId = -1; - public int DstId = -1; - - /// - /// 路径偏移 - /// - public Tuple PathTransformation = Tuple.Create(0f, 0f, 0f); - - public float PathTransformationAnchorDistance = 0f; - - /// - /// 切换条件 - /// - public Func SwitchWalkBlindCondition = null; - - /// - /// 盲走停止距离 - /// - public Func FinishWalkBlindCondition = null; - } - - public Func FinishCondition; - - /// - /// 多个检测器列表 - /// - public List detectors = null; - - private Painter _painter; - private List _remainDistanceList = new List(); - private List _remainAngleList = new List(); - private List _targetYList = new List(); - - private List SetFilters(float guessCenterX, float guessCenterY) - { - var painter = UI.GetPainter("GeneralFollowing.SetFilters", false); - painter.Clear(); - painter.Clear(3000); - - var box = new Vector2[] - { - new (guessCenterX - GuessRangeX, guessCenterY - GuessRangeY), - new (guessCenterX + GuessRangeX, guessCenterY - GuessRangeY), - new (guessCenterX + GuessRangeX, guessCenterY + GuessRangeY), - new (guessCenterX - GuessRangeX, guessCenterY + GuessRangeY), - }; - - for (var i = 0; i < box.Length; ++i) - painter.DrawLine(Color.DarkOliveGreen, box[i], box[(i + 1) % 4]); - - // PC filter in car coordinate frame - return new List() - { - new(CoordinateSystem.Car2D, - p => LessMath.IsPointInPolygon4( - box.Select(v => new PointF(v.X, v.Y)).ToArray(), new PointF(p.X, p.Y))), - }; - } - - public void Stop() - { - _dt?.Stop(); - } - - /// - ///计算车体中心的位移和角度增量 - /// - /// a轮在车体坐标系下位置 - /// a轮在车体坐标系下位移增量 - /// b轮在车体坐标系下位置 - /// b轮在车体坐标系下位移增量 - /// - private static (float, float, float) CenterMoveFromPoints(Vector2 a, - Vector2 aDelta, - Vector2 b, - Vector2 bDelta) - { - float th_x = 0, th_y = 0, th = 0, x = 0, y = 0; - var eps = 0.0000001; - if (Math.Abs(a.Y - b.Y) > eps) - { - th_x = (aDelta.X - bDelta.X) / (b.Y - a.Y); - } - if (Math.Abs(a.X - b.X) > eps) - { - th_y = (aDelta.Y - bDelta.Y) / (a.X - b.X); - } - th = th_x == 0 ? th_y : th_x; - - x = (aDelta.X + bDelta.X) / 2f - (a.Y - b.Y) / 2f * th; - - y = (aDelta.Y + bDelta.Y) / 2f + (a.X - b.X) / 2f * th; - - return (x, y, th); - } - - - public override IEnumerable Get() - { - _painter = UI.GetPainter("GeneralFollowing", false); - var lastDetectX = detectors[0].StartGuessingX; - var lastDetectY = detectors[0].StartGuessingY; - var detectorIndex = 0; - - var controller = (MultiWheelGeometricController)GetController.Invoke(); - controller.BaseSpeed = MaxSpeed; - controller.FinishDistance = float.MinValue; - controller.FirstThAccuracy = 999; - _dt = new DriveTask(controller.Track(true, CoordinateSystem.Car2D)); - void HardStop() - { - _dt?.Stop(); - ((MultiWheelChassis)PilotDefinition.Chassis).DriveStop(); - DLog.Log($"Hard Stop!", "TireFollowing"); - } - float WalkBlindCarPathDstX = -1f, WalkBlindCarPathDstY = -1f, WalkBlindCarPathDstTh = -1f; - bool WalkBlindStage1 = false, WalkBlindStage2 = false; - var angle2target = -1f; - float _lastLFLEncoder = -1, _lastLFREncoder = -1, _lastRFLEncoder = -1, _lastRFREncoder = -1; - float _lastLRLEncoder = -1, _lastLRREncoder = -1, _lastRRLEncoder = -1, _lastRRREncoder = -1; - - (float, float, float) GetCurrentPos2Dst(float lastX, float lastY, float lastTh) - { - // Read current encoders - var curLFLEncoder = PilotDefinition.Self.LFLActualPos; - var curLFREncoder = PilotDefinition.Self.LFRActualPos; - var curRFLEncoder = PilotDefinition.Self.RFLActualPos; - var curRFREncoder = PilotDefinition.Self.RFRActualPos; - var curLRLEncoder = PilotDefinition.Self.LRLActualPos; - var curLRREncoder = PilotDefinition.Self.LRRActualPos; - var curRRLEncoder = PilotDefinition.Self.RRLActualPos; - var curRRREncoder = PilotDefinition.Self.RRRActualPos; - - // Average delta per wheel pair (LF, LR, RF, RR) - var lfDelta = (curLFLEncoder - _lastLFLEncoder + curLFREncoder - _lastLFREncoder) / 2f; - var lrDelta = (curLRLEncoder - _lastLRLEncoder + curLRREncoder - _lastLRREncoder) / 2f; - var rfDelta = (curRFLEncoder - _lastRFLEncoder + curRFREncoder - _lastRFREncoder) / 2f; - var rrDelta = (curRRLEncoder - _lastRRLEncoder + curRRREncoder - _lastRRREncoder) / 2f; - var deltaList = new List { lfDelta, lrDelta, rfDelta, rrDelta }; - - var xs = new List(); - var ys = new List(); - var ths = new List(); - var chassis = (MultiWheelChassis)BasicPilotBase.Chassis; - var steerWheels = chassis.GetSteerWheels(); - for (var i = 0; i < steerWheels.Count; ++i) - { - var sw1 = steerWheels[i]; - var a = sw1.Position; - var tha = sw1.ReadAngle() / 180f * (float)Math.PI; - var deltaa = deltaList[i]; - var va = new Vector2(deltaa * (float)Math.Cos(tha), deltaa * (float)Math.Sin(tha)); - for (var j = i + 1; j < steerWheels.Count; ++j) - { - var sw2 = steerWheels[j]; - var b = sw2.Position; - var thb = sw2.ReadAngle() / 180f * (float)Math.PI; - var deltab = deltaList[j]; - var vb = new Vector2(deltab * (float)Math.Cos(thb), deltab * (float)Math.Sin(thb)); - var (tempx, tempy, tempth) = CenterMoveFromPoints(a, va, b, vb); - Hedingben.ToastText($"{tempx:f2} {tempy:f2} {tempth / Math.PI * 180f:f2} ", $"{i}_{j}"); - xs.Add(tempx); - ys.Add(tempy); - ths.Add(tempth); - } - } - var x = xs.Average(); - var y = ys.Average(); - var Th = ths.Average() / (float)Math.PI * 180; - var moveTup = Tuple.Create(x, y, Th); - var moved = MathTools.SolveTransform2D(MathTools.SolveTransform2D(Tuple.Create(lastX, lastY, lastTh), moveTup), Tuple.Create(0f, 0f, 0f)); - - _lastLFLEncoder = curLFLEncoder; - _lastLFREncoder = curLFREncoder; - _lastRFLEncoder = curRFLEncoder; - _lastRFREncoder = curRFREncoder; - _lastLRLEncoder = curLRLEncoder; - _lastLRREncoder = curLRREncoder; - _lastRRLEncoder = curRRLEncoder; - _lastRRREncoder = curRRREncoder; - return (moved.Item1, moved.Item2, moved.Item3); - } - - while (true) - { - if (detectorIndex > detectors.Count - 1) - throw new Exception("detector index out of range!"); - - _painter.Clear(); - if (WalkBlindStage1 || WalkBlindStage2) - { - //第二次盲走时或只钻一个轮胎时 - if (WalkBlindStage2 || detectors.Count == 1 || TireNum == 1) - { - //controller.FinishDistance = 10f; - controller.SlowDistance = SlowDistance; - controller.SlowingPow = 0.7f; - } - if (EnableHandover) - { - controller.SlowDistance = float.MinValue; - controller.FinishSpeed = 0.2f; - controller.FinishDistance = 50; - } - (WalkBlindCarPathDstX, WalkBlindCarPathDstY, WalkBlindCarPathDstTh) = GetCurrentPos2Dst(WalkBlindCarPathDstX, WalkBlindCarPathDstY, WalkBlindCarPathDstTh); - var walkBlindPathEnd = Tuple.Create(WalkBlindCarPathDstX, WalkBlindCarPathDstY, WalkBlindCarPathDstTh); - var walkBlindPathStart = LessMath.Transform2D(walkBlindPathEnd, Tuple.Create(CarDirection == 0 ? -3000f : 3000f, 0f, 0f)); - var walkBlindPathDst = new Vector2(WalkBlindCarPathDstX, WalkBlindCarPathDstY); - var walkBlindPathSrc = new Vector2(walkBlindPathStart.Item1, walkBlindPathStart.Item2); - var walkBlindPath = new LineSegment(walkBlindPathSrc, walkBlindPathDst); - - DLog.Log($"盲走目标点:{walkBlindPath.Src.X:F2} {walkBlindPath.Src.Y:F2} {walkBlindPath.Dst.X:F2} {walkBlindPath.Dst.Y:F2}", "TireFollowing"); - _painter.DrawDot(Color.Purple, walkBlindPathDst, sz: 3); - _painter.DrawLine(Color.GreenYellow, walkBlindPath.Src, walkBlindPath.Dst, endArrow: true, width: 2); - - var track = new LineTrack(walkBlindPath.Src, walkBlindPath.Dst); - track.CarDirectionBias = CarDirection; - controller.UpdateTracks(new List { track }); - var rd = (float)LessMath.PerpendicularPosition(0, 0, walkBlindPath.Dst.X, walkBlindPath.Dst.Y, - walkBlindPath.Src.X, walkBlindPath.Src.Y); - _remainDistanceList.Add(rd); - while (_remainDistanceList.Count > 3) _remainDistanceList.RemoveAt(0); - rd = _remainDistanceList.Average(); - DLog.Log($"盲走投影点剩余距离:{rd:0.0} ", "TireFollowing"); - // 检查是否达到盲走结束条件 - if (detectors[detectorIndex].FinishWalkBlindCondition(rd)) - { - if (WalkBlindStage1) - { - DLog.Log("达到第一次盲走停止距离,停下或开始钻第二对轮胎", "TireFollowing"); - //if (detectors[detectorIndex].DstId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) - //{ - // detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].DstId); - // DLog.Log($"释放取车点{detectors[detectorIndex].DstId}", "TireFollowing"); - //} - WalkBlindStage1 = false; - _remainAngleList.Clear(); - _remainDistanceList.Clear(); - detectorIndex++; - if ((detectors.Count == 1 || TireNum == 1) && !EnableHandover) - { - HardStop(); - yield return false; - } - } - else if (WalkBlindStage2) - { - DLog.Log("达到第二对轮胎处,停止移动", "TireFollowing"); - if (!EnableHandover) - { - HardStop(); - } - yield return false; - } - } - yield return true; - continue; - } - - var target = detectors[detectorIndex].DetectFunction(CarDirection, lastDetectX, - SetFilters(lastDetectX, lastDetectY)); - - if (target == null) - { - DLog.Log("无目标,等待下一帧", "TireFollowing"); - controller.FirstRotateMaxSpeed = 0; - yield return true; - continue; - } - else controller.FirstRotateMaxSpeed = 5; - - var targetAngle = CalculateAngle2YAxis(target.Src, target.Dst); - var targetPos = new Vector2((target.Src.X + target.Dst.X) / 2f, (target.Src.Y + target.Dst.Y) / 2f); - var dis2target = (float)Math.Sqrt(Math.Pow(targetPos.X, 2) + Math.Pow(targetPos.Y, 2)); - //距离较近以后角度容易跳变 - if (dis2target < PilotDefinition.Conf.TireFollowingCloseDistance && Math.Abs(targetAngle) > PilotDefinition.Conf.TireFollowingAngleIgnoreThr) - { - yield return true; - continue; - } - else _remainAngleList.Add(targetAngle); - while (_remainAngleList.Count > 10) _remainAngleList.RemoveAt(0); - angle2target = _remainAngleList.Average(); - var distanceLabelPos = targetPos / 2f; - _painter.DrawLine(Color.Cyan, Vector2.Zero, targetPos, width: 2); - _painter.DrawText(Color.Yellow, $"{dis2target:F3}", distanceLabelPos.X, distanceLabelPos.Y); - - var path = DetectorHelper.GetApproachPath(target, CoordinateSystem.Car2D, pathLen: 3000, - bias: detectors[detectorIndex].PathTransformation, - biasAnchorDistance: detectors[detectorIndex].PathTransformationAnchorDistance); - - if (path == null) - { - DLog.Log("no path!", "TireFollowing"); - NoTarget = true; - } - else - { - lastDetectX = ((target.Src + target.Dst) / 2f).X; - lastDetectY = ((target.Src + target.Dst) / 2f).Y; - - var currentY = path.CarPath.Dst.Y; - if (Math.Abs(targetAngle) < PilotDefinition.Conf.TireFollowingAngleIgnoreThr && - dis2target < PilotDefinition.Conf.TireFollowingCloseDistance) - { - _targetYList.Add(currentY); - while (_targetYList.Count > PilotDefinition.Conf.TireFollowingYAverageFrameCount) _targetYList.RemoveAt(0); - } - - var trackDstY = _targetYList.Count > 0 ? _targetYList.Average() : currentY; - Hedingben.ToastText($"target Y:{_targetYList.Count} {trackDstY}", "target Y"); - - var trackDst = new Vector2(path.CarPath.Dst.X, trackDstY); - _painter.DrawLine(Color.GreenYellow, path.CarPath.Src, trackDst, endArrow: true); - - var rd = (float)LessMath.PerpendicularPosition(0, 0, trackDst.X, trackDst.Y, - path.CarPath.Src.X, path.CarPath.Src.Y); - _remainDistanceList.Add(rd); - while (_remainDistanceList.Count > 3) _remainDistanceList.RemoveAt(0); - rd = _remainDistanceList.Average(); - _painter.DrawText(Color.Green, $"{rd:F3}", distanceLabelPos.X, distanceLabelPos.Y - 200); - if(rd < PilotDefinition.Conf.TireFollowingReleaseDistance) - { - if (detectors[detectorIndex].SrcId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) - { - detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].SrcId); - DLog.Log($"释放预取车点{detectors[detectorIndex].SrcId}", "TireFollowing"); - } - } - - if (detectorIndex < detectors.Count - 1) - { - controller.SlowDistance = 1; - if (detectors[detectorIndex].SwitchWalkBlindCondition(rd)) - { - WalkBlindStage1 = true; - //if (detectors[detectorIndex].SrcId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) - //{ - // detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].SrcId); - // DLog.Log($"释放预取车点{detectors[detectorIndex].SrcId}", "TireFollowing"); - //} - WalkBlindCarPathDstX = trackDst.X; - WalkBlindCarPathDstY = trackDst.Y; - WalkBlindCarPathDstTh = angle2target + WalkBlindTh; - DLog.Log($"切换至第一次盲走时刻目标点:{WalkBlindCarPathDstX:F2} " + - $"{WalkBlindCarPathDstY:F2} " + - $"{WalkBlindCarPathDstTh:F2}", "TireFollowing"); - _lastLFLEncoder = PilotDefinition.Self.LFLActualPos; - _lastLFREncoder = PilotDefinition.Self.LFRActualPos; - _lastRFLEncoder = PilotDefinition.Self.RFLActualPos; - _lastRFREncoder = PilotDefinition.Self.RFRActualPos; - _lastLRLEncoder = PilotDefinition.Self.LRLActualPos; - _lastLRREncoder = PilotDefinition.Self.LRRActualPos; - _lastRRLEncoder = PilotDefinition.Self.RRLActualPos; - _lastRRREncoder = PilotDefinition.Self.RRRActualPos; - _remainDistanceList.Clear(); - _targetYList.Clear(); - lastDetectX = detectors[detectorIndex + 1].StartGuessingX; - lastDetectY = detectors[detectorIndex + 1].StartGuessingY; - continue; - } - } - else if (detectorIndex == detectors.Count - 1) - { - if (detectors[detectorIndex].SwitchWalkBlindCondition(rd)) - { - WalkBlindStage2 = true; - WalkBlindCarPathDstX = trackDst.X; - WalkBlindCarPathDstY = trackDst.Y; - WalkBlindCarPathDstTh = angle2target + WalkBlindTh; - DLog.Log($"切换至最后一次盲走时刻目标点:{WalkBlindCarPathDstX:F2} " + - $"{WalkBlindCarPathDstY:F2} " + - $"{WalkBlindCarPathDstTh:F2}", "TireFollowing"); - if (detectors.Count == 1) - { - if (detectors[detectorIndex].SrcId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) - { - detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].SrcId); - DLog.Log($"释放预取车点{detectors[detectorIndex].SrcId}", "TireFollowing"); - } - } - - _lastLFLEncoder = PilotDefinition.Self.LFLActualPos; - _lastLFREncoder = PilotDefinition.Self.LFRActualPos; - _lastRFLEncoder = PilotDefinition.Self.RFLActualPos; - _lastRFREncoder = PilotDefinition.Self.RFRActualPos; - _lastLRLEncoder = PilotDefinition.Self.LRLActualPos; - _lastLRREncoder = PilotDefinition.Self.LRRActualPos; - _lastRRLEncoder = PilotDefinition.Self.RRLActualPos; - _lastRRREncoder = PilotDefinition.Self.RRRActualPos; - _remainDistanceList.Clear(); - _targetYList.Clear(); - continue; - } - } - DLog.Log($"投影点剩余距离:{rd:F2}", "TireFollowing"); - var track = new LineTrack(path.CarPath.Src, trackDst); - track.CarDirectionBias = CarDirection; - controller.UpdateTracks(new List { track }); - NoTarget = false; - } - yield return true; - } - } - - private static float CalculateAngle2YAxis(Vector2 point1, Vector2 point2) - { - return -(float)(Math.Atan((point1.X - point2.X) / (point1.Y - point2.Y)) * 180 / Math.PI); - } - - private DriveTask _dt; - } -} diff --git a/.task8-sweep/VehicleSyncBinaryCodec.cs b/.task8-sweep/VehicleSyncBinaryCodec.cs deleted file mode 100644 index e7bd154..0000000 --- a/.task8-sweep/VehicleSyncBinaryCodec.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace MultiWheelC; - -internal static class VehicleSyncBinaryCodec -{ - private const byte Version = 2; - private const byte RegisterType = 1; - private const byte NotificationType = 2; - private static readonly byte[] Magic = Encoding.ASCII.GetBytes("MVS1"); - - public static byte[] EncodeRegister(int carNum, VehicleSyncInfo info) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream, Encoding.UTF8); - WriteHeader(writer, RegisterType); - writer.Write(carNum); - WriteInfo(writer, info); - writer.Flush(); - return stream.ToArray(); - } - - public static (int CarNum, VehicleSyncInfo Info) DecodeRegister(byte[] payload) - { - using var stream = new MemoryStream(payload ?? throw new ArgumentNullException(nameof(payload))); - using var reader = new BinaryReader(stream, Encoding.UTF8); - var version = ReadHeader(reader, RegisterType); - var carNum = reader.ReadInt32(); - var info = ReadInfo(reader, version); - EnsureFullyRead(stream); - return (carNum, info); - } - - public static byte[] EncodeNotification(VehicleSyncNotification notification) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream, Encoding.UTF8); - WriteHeader(writer, NotificationType); - - writer.Write(notification.Seq); - writer.Write(BuildNotificationFlags(notification)); - writer.Write(notification.Mode); - writer.Write(notification.FleetStopSourceCar); - writer.Write(notification.CenterX); - writer.Write(notification.CenterY); - writer.Write(notification.CenterTh); - writer.Write(notification.FleetVx); - writer.Write(notification.FleetFrontTh); - writer.Write(notification.FleetRearTh); - writer.Write(notification.FleetOmega); - writer.Write(notification.RequestedFleetOmega); - writer.Write(notification.SyncTh); - writer.Write(notification.SyncDistance); - writer.Write(notification.DeltaDetectCenter); - writer.Write(notification.RotateActiveOmega); - writer.Write(notification.RotateCompXyFac); - writer.Write(notification.RotateCompXyIFac); - writer.Write(notification.RotateCompXyMax); - writer.Write(notification.RotateCompThFac); - writer.Write(notification.RotateCompThIFac); - writer.Write(notification.RotateCompThMax); - writer.Write(notification.RotateCompTangentFrac); - writer.Write(notification.RotateStartWheelAlignDeg); - writer.Write(notification.RotateActiveWheelAlignDeg); - writer.Write(notification.IdealX); - writer.Write(notification.IdealY); - writer.Write(notification.IdealTh); - WriteString(writer, notification.FleetStopReason); - - var fleet = notification.Fleet ?? new Dictionary(); - if (fleet.Count > ushort.MaxValue) - throw new InvalidOperationException($"Fleet count {fleet.Count} exceeds binary protocol limit."); - writer.Write((ushort)fleet.Count); - foreach (var kv in fleet) - { - writer.Write(kv.Key); - WriteInfo(writer, kv.Value); - } - - writer.Flush(); - return stream.ToArray(); - } - - public static VehicleSyncNotification DecodeNotification(byte[] payload) - { - using var stream = new MemoryStream(payload ?? throw new ArgumentNullException(nameof(payload))); - using var reader = new BinaryReader(stream, Encoding.UTF8); - var version = ReadHeader(reader, NotificationType); - - var notification = new VehicleSyncNotification - { - Seq = reader.ReadInt64() - }; - - ApplyNotificationFlags(notification, reader.ReadUInt16()); - notification.Mode = reader.ReadInt32(); - notification.FleetStopSourceCar = reader.ReadInt32(); - notification.CenterX = reader.ReadSingle(); - notification.CenterY = reader.ReadSingle(); - notification.CenterTh = reader.ReadSingle(); - notification.FleetVx = reader.ReadSingle(); - notification.FleetFrontTh = reader.ReadSingle(); - notification.FleetRearTh = reader.ReadSingle(); - notification.FleetOmega = reader.ReadSingle(); - notification.RequestedFleetOmega = reader.ReadSingle(); - notification.SyncTh = reader.ReadSingle(); - notification.SyncDistance = reader.ReadSingle(); - notification.DeltaDetectCenter = reader.ReadSingle(); - notification.RotateActiveOmega = reader.ReadSingle(); - notification.RotateCompXyFac = reader.ReadSingle(); - notification.RotateCompXyIFac = reader.ReadSingle(); - notification.RotateCompXyMax = reader.ReadSingle(); - notification.RotateCompThFac = reader.ReadSingle(); - notification.RotateCompThIFac = reader.ReadSingle(); - notification.RotateCompThMax = reader.ReadSingle(); - notification.RotateCompTangentFrac = reader.ReadSingle(); - notification.RotateStartWheelAlignDeg = reader.ReadSingle(); - notification.RotateActiveWheelAlignDeg = reader.ReadSingle(); - notification.IdealX = reader.ReadSingle(); - notification.IdealY = reader.ReadSingle(); - notification.IdealTh = reader.ReadSingle(); - notification.FleetStopReason = ReadString(reader); - - var fleetCount = reader.ReadUInt16(); - notification.Fleet = new Dictionary(fleetCount); - for (var i = 0; i < fleetCount; ++i) - { - var carNum = reader.ReadInt32(); - notification.Fleet[carNum] = ReadInfo(reader, version); - } - - EnsureFullyRead(stream); - return notification; - } - - private static void WriteHeader(BinaryWriter writer, byte type) - { - writer.Write(Magic); - writer.Write(Version); - writer.Write(type); - writer.Write((ushort)0); - } - - private static byte ReadHeader(BinaryReader reader, byte expectedType) - { - for (var i = 0; i < Magic.Length; ++i) - { - if (reader.ReadByte() != Magic[i]) - throw new InvalidDataException("Invalid multi-vehicle sync binary magic."); - } - - var version = reader.ReadByte(); - if (version < 1 || version > Version) - throw new InvalidDataException($"Unsupported multi-vehicle sync binary version {version}."); - - var type = reader.ReadByte(); - if (type != expectedType) - throw new InvalidDataException($"Unexpected multi-vehicle sync packet type {type}."); - - var reserved = reader.ReadUInt16(); - if (reserved != 0) - throw new InvalidDataException("Invalid multi-vehicle sync binary reserved field."); - - return version; - } - - private static void WriteInfo(BinaryWriter writer, VehicleSyncInfo info) - { - writer.Write(BuildInfoFlags(info)); - WriteString(writer, info.Ip); - writer.Write(info.Port); - writer.Write(info.X); - writer.Write(info.Y); - writer.Write(info.Th); - writer.Write(info.LayoutX); - writer.Write(info.LayoutY); - writer.Write(info.LayoutTh); - WriteString(writer, info.MotionInfeasibleReason); - WriteString(writer, info.RotateWheelAlignDetail); - writer.Write(info.AppliedNotificationSeq); - } - - private static VehicleSyncInfo ReadInfo(BinaryReader reader, byte version) - { - var info = new VehicleSyncInfo(); - ApplyInfoFlags(info, reader.ReadUInt16()); - info.Ip = ReadString(reader); - info.Port = reader.ReadInt32(); - info.X = reader.ReadSingle(); - info.Y = reader.ReadSingle(); - info.Th = reader.ReadSingle(); - info.LayoutX = reader.ReadSingle(); - info.LayoutY = reader.ReadSingle(); - info.LayoutTh = reader.ReadSingle(); - info.MotionInfeasibleReason = ReadString(reader); - info.RotateWheelAlignDetail = ReadString(reader); - info.AppliedNotificationSeq = version >= 2 ? reader.ReadInt64() : -1; - return info; - } - - private static ushort BuildInfoFlags(VehicleSyncInfo info) - { - ushort flags = 0; - if (info.Master) flags |= 1 << 0; - if (info.PosAvailable) flags |= 1 << 1; - if (info.Aligned) flags |= 1 << 2; - if (info.DetectOk) flags |= 1 << 3; - if (info.MotionFeasible) flags |= 1 << 4; - if (info.RotateWheelsAligned) flags |= 1 << 5; - return flags; - } - - private static void ApplyInfoFlags(VehicleSyncInfo info, ushort flags) - { - info.Master = (flags & (1 << 0)) != 0; - info.PosAvailable = (flags & (1 << 1)) != 0; - info.Aligned = (flags & (1 << 2)) != 0; - info.DetectOk = (flags & (1 << 3)) != 0; - info.MotionFeasible = (flags & (1 << 4)) != 0; - info.RotateWheelsAligned = (flags & (1 << 5)) != 0; - } - - private static ushort BuildNotificationFlags(VehicleSyncNotification notification) - { - ushort flags = 0; - if (notification.PosAvailable) flags |= 1 << 0; - if (notification.Aligned) flags |= 1 << 1; - if (notification.FleetMotionReleased) flags |= 1 << 2; - if (notification.FleetStopActive) flags |= 1 << 3; - if (notification.AutoEnabled) flags |= 1 << 4; - if (notification.ManualEnabled) flags |= 1 << 5; - if (notification.HasIdeal) flags |= 1 << 6; - if (notification.RotateParamsValid) flags |= 1 << 7; - if (notification.UseDetourCorrection) flags |= 1 << 8; - return flags; - } - - private static void ApplyNotificationFlags(VehicleSyncNotification notification, ushort flags) - { - notification.PosAvailable = (flags & (1 << 0)) != 0; - notification.Aligned = (flags & (1 << 1)) != 0; - notification.FleetMotionReleased = (flags & (1 << 2)) != 0; - notification.FleetStopActive = (flags & (1 << 3)) != 0; - notification.AutoEnabled = (flags & (1 << 4)) != 0; - notification.ManualEnabled = (flags & (1 << 5)) != 0; - notification.HasIdeal = (flags & (1 << 6)) != 0; - notification.RotateParamsValid = (flags & (1 << 7)) != 0; - notification.UseDetourCorrection = (flags & (1 << 8)) != 0; - } - - private static void WriteString(BinaryWriter writer, string value) - { - var bytes = Encoding.UTF8.GetBytes(value ?? ""); - if (bytes.Length > ushort.MaxValue) - throw new InvalidOperationException($"String payload length {bytes.Length} exceeds binary protocol limit."); - writer.Write((ushort)bytes.Length); - writer.Write(bytes); - } - - private static string ReadString(BinaryReader reader) - { - var length = reader.ReadUInt16(); - var bytes = reader.ReadBytes(length); - if (bytes.Length != length) - throw new EndOfStreamException("Truncated multi-vehicle sync string payload."); - return Encoding.UTF8.GetString(bytes); - } - - private static void EnsureFullyRead(MemoryStream stream) - { - if (stream.Position != stream.Length) - throw new InvalidDataException("Unexpected trailing bytes in multi-vehicle sync packet."); - } -} diff --git a/.task8-sweep/VehicleSyncModels.cs b/.task8-sweep/VehicleSyncModels.cs deleted file mode 100644 index 1c21428..0000000 --- a/.task8-sweep/VehicleSyncModels.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using ClumsyCore; -using Newtonsoft.Json; - -namespace MultiWheelC; - -public class VehicleSyncInfo -{ - [JsonProperty("Master")] public bool Master { get; set; } - [JsonProperty("Ip")] public string Ip { get; set; } = ""; - [JsonProperty("Port")] public int Port { get; set; } = 8008; - [JsonProperty("PosAvailable")] public bool PosAvailable { get; set; } - [JsonProperty("X")] public float X { get; set; } - [JsonProperty("Y")] public float Y { get; set; } - [JsonProperty("Th")] public float Th { get; set; } - [JsonProperty("LayoutX")] public float LayoutX { get; set; } - [JsonProperty("LayoutY")] public float LayoutY { get; set; } - [JsonProperty("LayoutTh")] public float LayoutTh { get; set; } - [JsonProperty("Aligned")] public bool Aligned { get; set; } - // 本车本轮是否成功识别到邻车(关闭互识别时恒为 true)。任一车为 false 则整队停车。 - [JsonProperty("DetectOk")] public bool DetectOk { get; set; } - [JsonProperty("MotionFeasible")] public bool MotionFeasible { get; set; } = true; - [JsonProperty("MotionInfeasibleReason")] public string MotionInfeasibleReason { get; set; } = ""; - [JsonProperty("RotateWheelsAligned")] public bool RotateWheelsAligned { get; set; } = true; - [JsonProperty("RotateWheelAlignDetail")] public string RotateWheelAlignDetail { get; set; } = ""; - [JsonProperty("AppliedNotificationSeq")] public long AppliedNotificationSeq { get; set; } = -1; -} - -public class VehicleSyncNotification -{ - [JsonProperty("PosAvailable")] public bool PosAvailable { get; set; } - [JsonProperty("CenterX")] public float CenterX { get; set; } - [JsonProperty("CenterY")] public float CenterY { get; set; } - [JsonProperty("CenterTh")] public float CenterTh { get; set; } - [JsonProperty("Aligned")] public bool Aligned { get; set; } - [JsonProperty("Fleet")] public Dictionary Fleet { get; set; } = new(); - [JsonProperty("FleetVx")] public float FleetVx { get; set; } - [JsonProperty("FleetFrontTh")] public float FleetFrontTh { get; set; } - [JsonProperty("FleetRearTh")] public float FleetRearTh { get; set; } - // 联动运动模式:0=常规(前进+转向) 1=蟹行(四轮同向平移) 2=原地旋转(绕车队中心) - [JsonProperty("Mode")] public int Mode { get; set; } - // 原地旋转角速度(deg/s,逆时针为正),仅 Mode==2 有效 - [JsonProperty("FleetOmega")] public float FleetOmega { get; set; } - [JsonProperty("RequestedFleetOmega")] public float RequestedFleetOmega { get; set; } - [JsonProperty("FleetMotionReleased")] public bool FleetMotionReleased { get; set; } = true; - [JsonProperty("FleetStopActive")] public bool FleetStopActive { get; set; } - [JsonProperty("FleetStopReason")] public string FleetStopReason { get; set; } = ""; - [JsonProperty("FleetStopSourceCar")] public int FleetStopSourceCar { get; set; } - [JsonProperty("AutoEnabled")] public bool AutoEnabled { get; set; } - [JsonProperty("ManualEnabled")] public bool ManualEnabled { get; set; } - [JsonProperty("UseDetourCorrection")] public bool UseDetourCorrection { get; set; } - [JsonProperty("SyncTh")] public float SyncTh { get; set; } - [JsonProperty("SyncDistance")] public float SyncDistance { get; set; } - [JsonProperty("DeltaDetectCenter")] public float DeltaDetectCenter { get; set; } - // 原地旋转纠偏参数由主车广播,从车运行时使用同一套增益/限幅,避免主从补偿强度不一致。 - [JsonProperty("RotateParamsValid")] public bool RotateParamsValid { get; set; } - [JsonProperty("RotateActiveOmega")] public float RotateActiveOmega { get; set; } - [JsonProperty("RotateCompXyFac")] public float RotateCompXyFac { get; set; } - [JsonProperty("RotateCompXyIFac")] public float RotateCompXyIFac { get; set; } - [JsonProperty("RotateCompXyMax")] public float RotateCompXyMax { get; set; } - [JsonProperty("RotateCompThFac")] public float RotateCompThFac { get; set; } - [JsonProperty("RotateCompThIFac")] public float RotateCompThIFac { get; set; } - [JsonProperty("RotateCompThMax")] public float RotateCompThMax { get; set; } - [JsonProperty("RotateCompTangentFrac")] public float RotateCompTangentFrac { get; set; } - [JsonProperty("RotateStartWheelAlignDeg")] public float RotateStartWheelAlignDeg { get; set; } - [JsonProperty("RotateActiveWheelAlignDeg")] public float RotateActiveWheelAlignDeg { get; set; } - // F: 单调递增序列号,从车据此丢弃乱序到达的旧 notify 包。 - [JsonProperty("Seq")] public long Seq { get; set; } - // D: 自动模式下主车路径控制器算出的车队中心理想位姿(世界系),由 idealPos/idealAngle 透传而来。 - // HasIdeal=true 时各从车按各自 layout 推算 per-car 目标位姿做前馈+补偿,弧线路径不再只靠事后纠偏。 - [JsonProperty("HasIdeal")] public bool HasIdeal { get; set; } - [JsonProperty("IdealX")] public float IdealX { get; set; } - [JsonProperty("IdealY")] public float IdealY { get; set; } - [JsonProperty("IdealTh")] public float IdealTh { get; set; } -} diff --git a/.task8-sweep/ref/CommonUsage.dll b/.task8-sweep/ref/CommonUsage.dll deleted file mode 100644 index effb81ec918baad01b420552b70e9dd5766da42c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134144 zcmd4431DPJwKraU`}Xbb+nvtbbkd#7mduiHC!Nf`WWq4}h#IMZ~ARCn|^wC=$Nk@6^5Bx0B4M?_2)=z|^fe zb?VfqQ>UstQ>z@&xyyMetP-MJtyqyshoLw&nc((Y(MmxAu$jDYTi6vWiO;|!Gs?AE&@>Wry*`%3pHl?df0kbY$-z4Xny-tuOq zqhJ3 z{j)2`JEMQ0;Vwb})qjE?G4hulfpWdM==B~j%>Io*Ms!*lbj|*=lu<*^nZ1M2zCvr% zjAhJBEMt|SVjz1ga-keLH;LSGufS0+8CCsCWtT_QYo-0lQAW8B;8cDvqT-b#ICS?z zdayNKL38{>y3igqs~IS#UFbnrpdU3V{JWy_%@I-^0>TM`#TQ1|!d~|sG^Rb&4pb05}CKH-won4_#nPa><)!U`L zTIM?`Uc(#VBkH||j}Gq`K6Wz;L?I#&Wuh)~Lv#t`lu@|=;___vaOG2srX>1HMlnzU zU4&W1Ymq3(pW#8L%#d<`BPs{f5YTQ))Np(F!1yxN3#bEGj;dY(Sa06Lgktu(8_-1W zOhzl$kmgwu{Q#q9OZ4N6zD1&UG5S_S?dkz2R{1f~?ncCnn$%rm)l~?`&XqR{m`x~e zLDX`$A}nu1kS}j%bO(Y|6+Lgq-2)MJx0hduN3MJj!t%ig(nj@Fc(l8RAoN;6%BV3+ zln*7!VF+5;jYCCtVpufw$9Vs5bABZ(j~I3F^AfFT3n|9oTL{0on^@Mvis z?WsJbns}TgJTia)i+|kU(@RjyQ73+g4In{gp`o?s9pKI-B|?K03TIAG?`_ae}D;L6e;77kxRL#v)PgbUd&EXjS7# z+-HmLo6=&X<}8oN+6WDBl&WB6I>WXqufW6IiB7l#(^e$vz6Mcugh7!#qn?9s-wUU<~A%!oZF!HoZF7&!}MOyM~8PD zAG?VGkzlMsBuw}ssn3^fq=vijX+Pw6O~7ikcmhHAYl*w3;*pz- zWMfWjARc$eknB?`=8hwhwHzz&o`#sMo{0CMCJ3#9Zaic5r4SJ;g9hard-Ks7dppe= zd%LU~-P2jxG?HNrKkK?uv_O)CF+ zHIIEY2ev{Dx$Trev|1*YR{0D(24-31H{sc%)Q8lmlSUzuOT`C{QD)_8RMucw3xje< zt7yWlAfhTi1WKYwP3E+y%HIJ^&Y(e9qqPogQS*+~I#jnU<{LE#$%L^H^&|oSlt2R{ z0swkY10(_fnxg@J#5Yu)YOF=&0f^MaGK)1RT3*h?20yWs2`stDF}ZkPdFl%a_Jepw zKwKgK0>I zqeL{SXEI6@pXL5@j0zO}3~v!(I=VQN9hNk-LsuaY)L94s5&-~G(-;x~U@!pq5ns~h z1Oh|1L7rUom+=wvAZoM!FvGPm9%v#!pO z2mqS{fJ6Ym>aH;)0>IV)AQ1qt=xYp#9A;EuSlAv320{eV_5evD0Kh)br6d9XCOr+1 z2mr7eG(aMt`xeqAdWbG;3XLQYl!AGo0TKb=-~b>I0A3XUBm%%80YD-E92x*5a*k2O zlwo;9H>&3|O2iNohXwc&0fEBN72PW6ulBe-x?|%6gsDojj1xg z+}((Jp+X;i2#5; z&_w-6W2jsizmzP}N@z7hQZqdD(`e7GsjA|IA$n$a62+QR5ZC7NOMlZ0roFc#O7eM_dqSe1TJL8+X$Orj*Ddu1hC`Q zHu+lcl;e7BM1|Czw+F@FfykrD4wCsP{H+_VDH zI*RNJ9~sNZcu%2)meaD!`-P4q$5A3NUMhP3Nq|)o1PM!4O^_t?NqWBoR`o?}k$N?% z2k(K2I5EAub)1yE&oLgyi0zx}$$Yi$G1&g;aKwyl8{dru(m{EZDqvf{F$$|i%_hIC5f`(o-KykK zY&F~)bdrx^3EAN^vcuS=;&^?YG1YMqj3tpl`(y+>B-*p)&>;XCE{0qoKWPNpri6i< zmGvAum+G`imkwi(QvEQhO&fc@PdRg%)uS-M&bSd-y%WqSe*{sL%UXrks)8BM7gO=h zmCF+G&SDuIVJ85Qh~ZfBbnk>2R2RZz6ntQdgFzA>d>|3;&5xu4%raII8*Q-iBizeb zlO~2xV(>bv6Jsb2pj~)fP^DwfTxW7$b*7xu;Fp3rHD*v}GS6|E%uSk$#HRt5t(*b4diD&( zCu3S-aDSs;Ppj7;x}a+amnw$oHf4+kIV>6EFkUQ;_?ifbn~pgUR~G6-S0qv@ksnDX zbTg5QefMzZ%P+rdq5IKMTyqR{EZRL!bkFbR)%Q=DShmxPUiqne-}l^)&e@nQY%+qp z&65V&kzREF%a_l8=-wZ0O&1nP)`1=CwiqQuCpKOD{I|A@oN8O$Jtw#y2ZNzCc|kCC z470ck|5!l&Hb+a*(x`hkl+7O&;7$AI=L>I`d3(MzX<|Z1APR3=VTVE3mPrE>Kw{y7 zU){MgRagPg#3p7;Y#kk$r3|CAufQMEQMO-oOFcecti$K&o+&(?i>w~OE#HjR=@v}9 zVe@EXrM8XoEy%KD#g0n6G-A12r=#UhAS@MAhC~7&(TTz(-bWcp8&W}3!jYDEBI#m| z2vk|@h2MY|$P_+_n3yElcw5|s?Pg<3P7|e`9<_apl%JE1i#akL7jtCXg_UO~G$;`Q z6qpF%5qr9EqNsR&)&n;NII z>S|>!#HARo33Klt+d7i_48tjelWl-JDyg(|F zXbh{*Ft1b`gCr#y$hVHBWvS6d#vo1B6qQg9E-VVc;G&Mb3Op?qo@$>3rct{?!aEVV zM2!kKD&T0X4tBr9`NAr*UQ1YM(i>B4Y0!O-P~`ZD3Q0Y*^oWWHWS`XZJ_mb`u#7*YW@ZuA)ls7rn?mKNN6%||Fh#WO zh_#Q)Uo=+4|GuSNw+Ewg_RvSUwW*x<70fKQrOs2ybLK}|B zu#UsukAeOOL^6^B|4Mrq!9i z{(1JkU|(VysluK*c$1hREBz8`bG>K-!>=87MQNA&d9;~(kPZ7P(b&AyOVrVe^?n^e z8<&vZSSD8A04?4;YMU7|s1wN)Zb(4YVub3A5Xu3bwv19dS{g}=$QISIO7Xa_kfSXH zm6SaX*wuAJ%P=|gA{j0JHi#B?p)-26jE-YIm3&TYem9jH%G13Usd4uU2+^<3#(pF-8)Q%&7y8e<9soy)H0Okvht)XD^c&~ zcw%TXS%wW`cVffX?z9gar7A0NFc!+uS29ipj{5+D-a)gky$~xuC~>Q=ay8K1hmcY^ z7i*#|LG@w8vK>QzF~EB;jj6heOf@_$E%n@NouRL=aISDrTaN9svw0PF@*-vbQ5qjf zCH(mKNIKcbrPPtz;+TNRKpFvE5c(Q225-ZXl$*?9p$L|wc&Mf0C28`YsPKZ+7%MsP5$|UL(rw#(`DX7Ybri+~k^S@>FGFfe33zlIi{3rmfW%+ARMGNZYJHc#f0IwmEh->o`sd8a~^ZJ+M+and}^A&cOU!+gx49ncIl@ znp@c9w4&&&Y*rQeupgc0%=-rgGI^)X$p*!h)6vg628Q~S+8K+8U1O-NXDM zLbaUZ+UR*{+e_O@Jr92HK{XQXiCqXi%#R#z_MCRu!&^!{>wz?Kd}AK{Cd#dlIZOIZ zPc@jSnob@tKTTgR(=jQb_Z-FX7QF?DiEvs_Rh*y~5- zUgS!)4U?7jYQUOM%KbVhw?(2;L0GpRL5^(51QC{*9178&L;!4X$k(^aX50}6wqTTg zLf_`3PpQ6P8IuI|&cv)Aw*r@UWpGoF^Rv^K4eVHrx*3zXpqPq@%Em;ui)mO}=-ZK& zMexhv@0oM6W)OWj8&feoAy=NUie50dkVMwu~FtDs*tHkJthn#co9 zsIjr?H^8o|$pJNk0+tWdv}uzVJTvok&ixIlG~C}JbTMLWeF88A=l6p1EOEXS!ima# zb*;NXK@W5xDN*c+H2gg%WoS5~MeW<0$yg2M5AL*Z*eK(|WzB|5E~>vkAUFpbwcC00 zTl)+`acA_O3L%RQc#@-iejfAtfelv)u9q2Gol>542IYy{|Gs*!b}#hqVy^pb5T{XXSD%Ljyt|2Z zwUPI*3o`M*4j?$S2a6tdw!F84U98z3zT*9WhCfI+`*=HK%s(8;!xZA#JO)Xa5G*DV zg2luOK!TGoHBu(#!R#@OjOAgBkX}sPrpYOAgh>{s_1^`@y&WCZ@8MbgJ_6O-k!woj z!>L>oX08b{*MymG!pt{e=EImC=47^gTNsl# zWAS+jlA1Bc&(_xS=w!ralnMey)#ebRs&Ghyn%~MM(z$<$8dg^vuz@knQN1->pNH3A zLKV436_i@7h1{!iaVp4r8o$H*j0>T^$3t1WpT$G3+_)C{2WBmN`Q>xZ^`1f0sIhZ# zx;u{tijP8oE>2AfE{8O=(Fpz5(K3V#Ys(cLl zx!@9RTsD8WfE%WL$r)IdaUs{S{M@c4`Z`AFXPEieshFzLjM8s;j$M@FjuNts}e z>a<{fi^w}oP!ZT_H7JcK3lZnN0^~cS;VdoE&ll~e^q7bTI%wwX!jUT9Qdq~XARdm0 zMJ8+81Arv!^5n^BOeJmHwn*kjTF^6f>iXn#@J?`oY1P!SO+f|PpaFF(&K}eOD2GgB zLB*C|aZ%$2J;QSKEbJbgxHJGA%}A)Dp;H6>0&?P@AsqN5Veo}nDvhKHsS0LU|K$V~ z4-U=f*SH8`Ur)J3b1PF(!!o6E?rM>UsIjnVsLr~Y{AB|u_9(Dftjmok23U){FZZ$(X>Q(WK+6B zQyUviRsS1Q(p)ue;Mh;l$A?Lz3L6yH+HqN8=y<}bF#POnB9~1L^)ro~XvV?y0J;CO z{z5<$OmTEA%uU=XD*v93Ohd9+JDiOrXc_`MAbTm#j>x9N*;C330`3~^^yP{Ax%Ntz zX8k4D!9*B>VnSt41(>>u02W~Sbe-f_yp(Nqcz8d7jdan$iEyivDvi_uX#!f{kh>c7 zmY+dD5n<|;X_88!Z^2;WK79Y=%P%{YlPJSlB^qaFj3BK;j){?ooqF5w#X!R~uJRu= zR(*|R7Xl^gWQPv}ijm+Q%mUv?EO??P2@f`)$vq$ESAt|obr%z@muQ@2_m$6Kii;Px zY=ZkQ(xWZ8Z2QokHAR`D+mm_Sh3#6X6jd@e4OASTg7=>3cm(ak&>t)Ri4t(ybCc0* zhtn}|s-8BSc7L934=oo?yFa_g;SEHvy6G087KfvG4iYFqZIN9KN}RKObbQpW6t$vr z;7AO~5wg!Yx#2%j_Of2VrlUym!iHv6bBGK|GMtgm<_qh5{;>)%+tK+!0=mc5kE(?+ z?zcS;F7gfv)O(W?cDB>$9Q@3_I`X@qzps4G;~}EB?9^2_5K6;x4%R{a^jY%d)g{sj z68=yT;f-i+mD7bAOfKVOJO_e+ zvn#|i#4y*HJ9rZ&I?{IgsNh_40id+@{1>LdkRjJai|txSxOaj%d{wsK!KAL^|93O~ z>;^t_G(Idyo@(j}e$!lFPQy}Uj#!F(x3=ItjU--H<$4AiKyAR-S9%|6)W|{696cpA z(mhM>FpU~(AuMB#^zcRZ*N=2}zy1V&uD8^QpmBiEz*S|egDasj6PsdKW;wH&B0ZS` zOR!Z06wGTJaa?cvf)co~Y{pz0;~lt6jQfPvgVR~a$2KAQ^8N3eO95tC+({QoNyKn|ho5v=C z^Ib!gz$^ba*d{i4kD&t$cL_Tki*$HbiM;}Ml|SJl=KYip?2}>E;u-U>SCD=3U3|K| zXa;PE#eDSBKNpvG*)~xoMva7VgVMs`-lVV`kvxJUvmICSko!G zlwoNR#v(xgQ zWIp!=MYNB9nq{LV>_{7-P|a(Y++-TViQK+$u7WhjN@9)3)qcu#@AZ)Re^k%RIMeEZ z(4S(X@WeJzp4b}66DG0$y?~fH)lfV?<^0FV!Pw3I@m+!bl&C@f$T)x(U2poqKVB=A zuthfhc&oC;aWwnH{Od76%Jz4h`x=y3rlXD2Cnw7n0K3G~T&i3zu@3AM!zd$XA#jgH8g|X~iL8i)M?Q=9gwP zs=nq@;B^F25WC6uA-whhEUvIv2W)XE+_4RZCtB?wMRu61`dDvBaID15V>7F?V+YP` zQfl6vnn|B$fQxQv6}}uT7d=i{%}wJNz-f#oy72MVgEp6199+avacmcodRLyst~`qY zKtJYeC`mW<4kgant{XENG(c8X;x+BxDLu*I`N&-CeYWmp{?+mXVF*eKlXpGt*!DW3 z#`OUA#V`-rr=*QU=g6g;PW9R#VsFtr&|4?BpfxQ(Id#I0q|V;NGP!K5a0cEIj*V%m zzT%DHU<=|$ea*rh%VJ+a8cr-fVz{%gl;G_QJ%upRkg?&mAyclRiln7B#bVTk1T=vc z;@_sJ;bykhV2#y0g>RV)-G`iQ{Iei0RH`R(q&WxSb$7K*WT;yiQatZTuxiv8qA5zD zR*Itywww=xY&B9a{}JBQ|K87q29KVIA-{gTjM#vf zsHXb$w_`r2_F?Blzy1rb^5NJ2D<1Ic{5|836H`&Hw};j-q{>0o18u8O4$&X8_NEQu7%=9gAag>KOnt z>yCb-CQ0Aj)|B-xoB;?2zT-#E02(amGl2Q=f9?zbFA`2a10e8UJOiNCHk|>40J1p| zHJo||FwIVU9%RH$#MTGSbJ_B67Ux!oiw~b%efZ?+!+l|m2#wPMy&MtF0)YPs0|y>} z2HeNTvfqI76hYv@DT2SMO*ungt8~r(;0&PydzV-E_8IB=%aF%L$+3VfhXE-W!-h+V zkB#b`7bdCtSfQDXy~q|@sATtlY~_2$ZAi>>1D95`Hb32Y^#<=R&b z2fwZIc%gYcSQ3Ix@bm|{20{?ieA0La8e0SG4s{8C7Q;+~J>TG`_a%BL^*+0X6N^z3 ztVY-7xtCJP9}x{WT-I4}EEC_!>T*uj=h*Mbtn_1$@Vm9yWu1k~8b>O=%Nid4!B6Vx zjxT%!_2#o3QWrUNI&{9%QFmgC)^vpHVxBCNTmkD&><$PH2N@y7h1hVWAWm%3)=q3r zC@{WBb@p|c}|FB``RZhHoF>wmi0~$>K166#gX>6+CmErR0ndt zk*n7F!ZjxsxO2GM#d!n|+d2D0-Tg7I7%n#GxPvJ=39kdpNyA;HK_cwBw*Y63)=0X( ziPcy#Q$XObZw7C|MG9Q)v2nbWC~QSVFk*B?8yXcQ{ECwJR)SwqLicLDq7<$hqEl%E z2`51vm?8#miR(JxYjwt|(0Uc>f(Hb?PIBkuIKubamoAf%JVE8mu zT}<-;6nFO*PsNKB6QGwLnMM(>QNewH*2bV2+%IBVsSc{Q7dzPLQR=-n}y+{vdc@>Xp5#05SrE0uwC&{e7U2E7_QJthnXWj zD>#7(WFaI`1Rv!Xam*1XhLc`MgWqC^s?#Y<{MGO9iT-;p>S4@>e6hv*y8w0z;8qMM z@1+Jn7Nl`QGG#H^`-RCdZxZFCAEN41U3H`G=>Lm)4IHZ|b+FhLQE_EdH$v$CWI+Gi zI^@4f@*(49SC;=q`j~z>OuDs88K$R{G# zVjAqjc?R#aRevC;#mbZR??Z9J{bl zP^fuM#)mS2J??)A1uuq;j@ePGn2N&e!X`~5oA5;Ak z*4+?XugE7u*Zx*KzlE(d%~QDRQuzsA9s2ZQ!S`03VWevBqOI6MC=^Ff4MZp_>gVm$WHf zM(5e-#F@BHjL$*A_~04i`uotMDp})dpR8ewak6{mHc^%-hi*IsuQQgDdosEl?!)Ht z{0KE{s0PLg!Kj!Cei-@V5R7n{D1Ru33d5R$D-8Qwls^m%FmT7#kz|eIct&P0vDKMb z!3!of?v`4~pzzF1$mOT=xljsMdW6qib>`T=@YK!BSaBbW;ov#M7^3{xVEli?7z_K1 zy`h=0xMoZZUC2=#s&T!$9||R;KmG7u7s(2(b?mxCxa-(44PDdZeMY7#GJOIAZkV&# zJizfe)7W*bslU}zAItDl;Min%%S(6*hW8IHK`UVcJ9w=fOjV9W;6l^UJO04L#0QgW z8?LWm-m_B*z0fe&MUl02AFpi>G)kW&Q}KZu!NYq60^WcF8}{HG`Uc#l#tpb&eCr*E ze;;nXI^y);K3r10AAI6Y6YUn?B9aY;djTW|UX9)89n3LP?4q3ePUa&UBJ#8I4YMil zXJg&h#feq7d}MS?3EUd!^9@J+Ze!G@RA7Ga>M6a_Vh{P!;r)B@6TbX6)z{uj0Z<8IE9`Omdgtab{h_A7+PYnooF|7M-3DdLWnegKB zOnUgHCEzhHDNoC@UQONLS^+ZV(ytNiWkr1^U)Y{O@1^Q zQL69-sx1ilP2RsqTdQ+i=n9(m$;>eDd6TX5bqhVYep=!;0;A|>m;mgSS z1A`g)a^rlZv{oG2sgpqA&>5*CZloM%Pg{;z4S^~re zc3fEexZImy%A=gJhNwV<=ydnAl{SsaQJSQpH4oc?_p4 z_+o*4^g@Oher;rkFOuOZxyHy~v-=3lTD$|*DqCEm#vK}j6YIeDpJO~ZTfY@1zG}mp zG7Gen@Rf_J!ABd;xd_=MV^ly+yUf&EM_lZ}^edubgWwFTL+0V>IeDkuL9UZ`9|s@3 zHSF2!Y-bJoH5+f5biywpA4BQ)`}P5}J+22#9DCqwV$IDM3TE3 zB>5<6v37&`_h={|_%g|KBxk-x>6WY#l0v6jp!fhvkJBS<^xM85s7RA4jt6|`n)daM zR(Hu+fXmO>g+7x$Hz1+g;4}NrkXYm_%p?LOAIUCu7B!N^Y_GG}>D9{E=k!fcMrh)G z&VK7Z%1(0TI*ZXbY@ViSnd{7R=sTSuYY9p*dr4T$`n8%BoPMg=fHN>K;H%ljG=7q2 zu^xK8GbvgNu2MLKnH5bH9OW!dIlcJtA%ApHw;apkY+Jb(w8LXt&px^m8Q&@}PmL-<{ndP=|Ed=zjiC6M(O1)(6{L(4k3Y+EinIE0-6 z^`G{LGbjd0b}6L3%2~A@gB^`s+VEb7IDj7b8PwGfxBF5EXI6KKIq|! zP&x6&#PtP;AD??N3whX97LJ=9IY57nX4n}XKJ;&RuT)s_gt>|%Gm&ZHaHu?h0N>$&H;npQH6rfMdftpMOYujYti5g?itjuv9KE2 z!QFtf(U<2&sVa(l>a;qYjbe6=mHcZf&PLR|k;tKKL3Wd~Y49%mC=?1%xSO5L1Fun) z--3h6tV~@!rN|yw$@*r5cDya6<86P4w0D9z?sDvhj7jTKw6wYbV0Q%o*)4{%MUKzF z^H$z|akl#GZf$0FtIsZPAaGcs!RKkAZ3W|7SuD)>HfI}N$ZR;c!xlxp@jiP({Q=AC zaS%PELFZC-YPvtVGCa`0+y$-&FG{-;N=3gYe8l7kmt1IQv>blh23 z;)>=hUfb#{T6F2}?)=d64}W^29>cv(F9to9}Udy+G~DxzB7N! zX%Ef7-3Dsw7FD&J?B!I2IG)szwx0T&h^2U-Lo2k?Q`hDf3~HLp1=wR}uhi2c-q68# zf*Bc7F-(fC;|?#!q?pStDP#?2iRCQ0R?k|_65ua!mNet{JN<)?$^@R(`}J%lmn{@d z!gZm-wHnnah)$G0h#?^Bi3}1f>ltT2@9Q)88Il2mi@xq^e(%=Or#Z*~b-7NA2^^o? z$h8#aVuc4U1*g!=%Tj0Q;M@& z1*)5X$JM%WKBk%Or2pfD`j8z)LsGV#{1{5cHyXP9J;jZ^vz=BS1`k67#QfA;z4y4W zcU~p{&CmESu`9}(QE6W;yL#wOs;A!#tM$-a176H~yVrQa6Wk%j7Fp`OAI}p=L;KO9GN|=ub$oAH z7&O(VCVsPAy5|m1!8iQJiit5xd&h7oDmhk6){~g=o!GjRyb_E(6n|M#UchWsdFE8N zqNTWUhwE*Qxi;m0u}GuQZ`7(f!e|-1#?mN+yLLEJN~rCI;ydtMLwRx*G}TY}CLUd^ z`Yv5j_RWy5AZr7NtEG0sY8;shrk)vNc_Eb+oD;#nhySIlH5R56CZca`;A70#R>$^! zoE_80*%m$>h{GLh=%xEh{w-0hZ@Nbtj$g`fJuWEXcSq^V(BDwXnEnZbSi*Q~LONcU z#8+3uRF>~AVZW2u`#Dmo3A4BlO#CK`2tGPFm(jA^hMwx3V2-;IhUA`E%{aK_VpVal0vzVJtAOLKMgX_%9;X?1MFW>P?i%9u zBJj9J0e)LE{z~Fw-vipran};RailEw#BjX`+X=mFiC>|53+4iSW5&-5IUPco`huC> z9=W)LSMazw#9=gf(bti=f9ijrX@2vspzY)ES>MlzsT-iOyC~a?ID+6o(=Y4l*T40x zrcqm5x}iLpYkpp;@hpqyqQR58+Y<+Oh(+-JWAQSpRj(fSdz}ru9N1^G+ zCcH108{P@DPTL6ZqVYUj7x8xh;oU_(Jp}dQB|y0%K~I}GqOVH`1YF?FEqMNLmXP9Q z3S8IFn9X|w8y^!JIy_{HTYABd8~DyDx9<=x&Xq4h(|T(MLRQ&C?Uj7sC+lRf$9ow3 zN)oVn94YI(h^sBtmTqgQTbcO%I9iz$q|@T>N7GU<%=t9jq?i!J+O+T{hHS;LiaE&- z`7fi1dyjriGzRdLwl4kF^NVzxO#N5Tgohao!!Z#-%>HM3IhgT$6#+*j&p5Ezq%l5Ba=a}d2PJ8 zQSUO?+I(;0zP(v5dUWWshX~XI$0PXtddStbB}_?}# zv~equu5%{w5N9xc5>5(l{#-1B;2<*9+FLuA&sn@EXlJ34t=erAcV_8G!f^seQvCyC zQ~GhZ4Yq0U@s)5MKN|LWD2u+NYPpA?4t-ZNuH%ysaBo85jaQLEKM^q|H&3(exWwKb z*pxp*ho3@yK!-uz68i6NNZj9vnf0G!;{_VdrTzy$#OQ)w0Fw%a=D>V>6ND4L{~2$M zLGKF1nJAk2;F?frXu=QL7M%r*$PTLs$Txu+}A! zmPMy1wqBxoC_5p6*5Zd*Igf8f3r}TRqtV06=waHX9Fv%~B~uWfze7tingaoT{JzP= z+X;I^{|*9PR0b_WUgE?J34P>{D$I-Ob7WkLCTIdb=7`&lMf~gses+bIZpjOlY5jdb zGX|WQFZy5t>t~npI&@3av~oF20&+vC|DI^=Ffb(Ovv@;;Y78^oPk+TMi8Hbmczi>c3H1{jxiVFkm7ai0aBKAv29f<4VL5(ncL_zhJn z;k`L{@62&rX+*z>`>5{67{QNe=x;ETvSx74TCnw{%xT2*@!OczaO7h76b1L|*jp(M zQTQv#WW#NQ;`lP0*44(Z=j2v2%wEe|3bE2BhG~g4xtW+)V05$u~i4~j;)gh6n^Cl zRk=r??rG#ZJh*R7aY*lrxqufz<~T9RbVIdT=xs&wwG`PO zz<%A^U2ptOqkq|3o5UC^MvW}+J53op_j&pI8mI(Kx>@8TCs(%h+-j2V>G!{B5~5M{ z_eAI?M~x!Y`gJH*Q*FAUq%VSV?!hFdPHT+cy3c^52D?M5stvC|VPX04rik2;%Io(} z0xvzLD(&K$6@g=!lVSO2w9BX^K(d7Wk2X(blN+4w#oX(p>5bL|Ys$awABn36(5BO6 z-q9WDZ}@{d;p}JOYx@3P<6C3sG-CN1cr%Tk{_27*&UhQ$knMX5uBglbN`3e4?M0pR zCY5&gg34E6-)Wbdu1~B$IZx4-HsrE5wI{gjt;ZDgkAKwLGq!lVh{Wg}4|s5}>F8Du z?x^63Gxn;u0iH2&4)9Tm84iDq8Piz!9{Wb3Wa1v&f~MjB_K^qQjQ^dksC?J1EP7%d zhE?nXpo5N1saXDK)7pto6mR5#p# zyCCMlryJlR!11-Se5BAFFYI9kA48F1+{98&?^_zFb@PC}rcrN`?K07 zCw@GmMj4im1dYZWnizlQD0t^ZWUZSAJ(HHPM)5i0);xPFxHV6iG~LT*rjX@)mTl1F z&d8`ScSP4Zr5w9bGH~sla^*W=tQqr}saAr{Fl6q}gF?$i_u0@CgO~leOS3%Q6NTnr zi^lKgV2h^TCun*Ucd7==AptY9r#y%qS1&+y)Y!>8W7hihGCub#oqWYnlZ2~6*;hNN`eHN<9JEh$M z9|Qan=u<>{13(%>^ocGjw+y$f*tP*!&3yX5OQ}Fs8rv>%K#F9ZSb{GiI;J;{xT;uL zik0zliE=oKflpaIF&n7pQsG@1ivwJ(K<&-&Ky$XhP%$8dWAN{1__rE;*o9~CN40{2 zC0Dp7bEs={G08=_pWoNf!!vZBf!{wI>LWa=Qtm1LOo|e=c=pVQN>%u z(0ki!hf5Wop33nN^*Z?I@UndDW)_yQ1JKnv!MAzr#>5oRJexGvhG=%NxZ%y=BkJKp z!la2Gk^oI+f#yb`$;4i0VvcdD>q9gbu(;tZ;v?$e>s6%L!^duBfhL*i)4+1lLD_H-}@X$^~8&jZBdlP8s39rgYz^L&d ze0!z zV050v2Sz8T>xpx-jLzHGx*9VvI{66Aoh%v5ow~lt1{oO*UO6&Cyc;#L&2Rf!>WM*_ zZKe8lPukwDTimW&Agly)?-CB3e}G#bPjlHPdVFBPR#+tYyN)t$n#TuzlNovENS~e! z&ip>ToTbqdZzZ#w>AV_ZK~J26c&(+;6U>vI$VpG=KG@9yH9^&VK)_!6fOVq}JU-9| zZ^2V$$Su+bA0jt3W}*-H2=xI=Hu`||`F+sfQujeUZPduH-v@ot2U2~zZe+VBtP2l; z@E20}dqWDO-dO+s@Pg{&95q$eheOsiku?-auUE05K-M51T3S4a0Lu|N9O{E^uzjs3 zc$%!yG+?1sc1_EgfW69^bwk!@rL0Dfr(TWF&sdGLzB3cD<|8C)mTbtHTvc8PeRuaD zhQo$_bP}2HJ3zSBvS-w&k#{Y2^@OX}2;rq&H@4lAcCdr(`idcv-y2pS`M^$i$&Aq4 znrzZC}Ul6CJNj08+yOW46nC*Rh|*fDF|?5AeoH@12ii&>a^b?so^l z)4JnA;x-7E--h&@NfTQie$BRCp;04Zqno%J#42YGZWp70tQtx0B0eCj-ET*vE!t+f z+oYc+WK1x(#n{b9sGnFA#16;61#XL~T*zMX*Jl?FRA{r1Z`bW=_k?As!io87wMc$% zSb^jlI{&g2$R4|dt1!UitzC%#aCHEX2msdv0Eqx_Z2*u60M`Woi2(4v03ZJeFKq3HqAOJ`NfDZ-$i5wgJL^WJ{#0WMEK)fM9ln4MH3IGxT;KKnxB8M5(cR=bM zb3H-tfFJR9G|0g!s>Li30B#Nde#A%Rehurc z@;CZ68eb?_@hkDQ{2GxG zufs2EVDRGy4HDkvMDI>{Fv#ozULWMIj9HhRvhrK~`2KtZ(~DAjzymx`?*T>`{%crd z1p}*L8TK1K<}vsue9;{r6R~__RPO-{)uP(E^=E)Lc28EXK;d4)doLahL+UA`%exW~ zquchNb{MVDHQR%Jg&OBUyVxM;l#Tr;G|7em$M;gs#IQe?yj7u1Y~>Jlad1IRTF@rD z3jMI%?HEn~Kqu@fdfIk(0N}m?0mVbwd`;9&%cPFI7kb@p!7WjI<&$ro;d6F{A>8Uf zSL2g*C_y%Gxg9-iC+Rt~<(f-74(r&rfT17xjWrYWQSKI`2Jgcgm9fd(>%q1!BuDPK z^7X0NRALaDV*GD-0!#b_QQX_c<^iz_IXxtL{4(#`C=ni~fA4G1eA&;ew6j0ONx0PZUxQ53zG zMZeHgbbi!5h?yaOMpX5J=Lzsy{UUH}>;mq?)2Q8#5MRRm5+2nD5V!|7mE3C7h!=HV z1u%A*4Vn*YN#TIvDaf^?C4rze1uxW?|H zdG-)U^3~+8Adynhc$*;#2e0%DAj17E02Gw%;qT8bU4WO1WLt;vxH-oMvvzFmr56(L*(Ce>ZorZY6!bQHt za^FVaVQ@n&>V+$Rk{xj~ad8-hi|ZO$RBsR}Ry#dR0yv886XVq^2*;qjbNu; zYJH+JP0N<;F~skZ2)msc;JWb|>>D2b#!UW2Q(^T~T1ND{P)2magk?nUOl^!ZN{3|h zN|dY1s8OULqq$_URxj+!sDVjw_%dn+8Z{QxGQuWB66-M~d-!Qmvx4hh5E`Rp9OPP; zz}~|b+a--|vqFbN+~EiutB+OwF8gghiWPR?>j{jw-y;aWPim^ljY5_2@;YEw?rEe^ zwdH=F%<9brI)ZpUjUOOm@U|!Na5c7R&}0tBG=bUT3t(J&Q-AA`ogn+8q*0`y0gKt5TD`EZ0S(NM1~dcl1tb)$ zYk(wpnBny{D4s4NB|sl-aDYgK?Ske05ONkz7|$q=VK3<;1hy7dd>!2@T_+%IhIUdQ8M*r;U&+s^B zrB2F=82Zc$Lsq@5-q!BPRk%Y)p@?`^rO-r?mR-o^WM74!P!N7)cV&UT`y}Zvt^OR3 z2^#f{l>|!-w=P_AxLxVr`s#zXMie>_f0z%|S*w>WU%G1fsuh?gaOs@)dekGQD79!0 zzF~hVz6pZu)1p_uxpKId=f0cWm2HVzQ}Ob^4q3$;<9aFA=D zmMdbC+s#@oT*+|xQbJa)VEpD7!|%@hEWX@w>&DM6&!`7C9=NehRo4$~%&7782X4%% zuBDY_;Cb2ksW!EB-Oxr${V?`?%U$Yl%)V`^V_wSvmU>_TdHxT`1h=C5pIMSq*;Suk zpHurSX4%8LS>q^5S*l%l-YGI{mH50ATeN?YExI;N%=bw6?}DFSL=JyDkMN(Ynz!Ck zPtIewL)x1V3LlM=^82NwpAg8K=8%*AZiWXfV%Z}kcLclvBJnq_IDTtRtyx44|0dFY zw2!$_$Y-`Ht~wkvpB{W>Nt@cef~{V({e8nZb>o)GvWz-&1*P=zwhgP=)cd!$900iu z5pvf?Vjf_T&P(&3S&~s77xgYjEN)%7f_elmNPshMw~}cm!-~My{m7witKlI%WUK1Nf)LcVn63$lwr~rB00U zmqmiqj)hOJI~1vybc%hhnrP0GXvOUUkph9V)7llG?NVY*4W- zTWyily9Sw>hr9YLw_qVtz4)>;Q|Y-(_2WCkOx?DbsinBN#ne^vP8_~at-zfsrtU@! z7vc`YF_L<64lxga&A~iNXl_!Yk{VjhyzS}@l6qnmQ?FEINnJa{)M4;Xv)uKl{X%>b z_5w-0M)*7iYZdculln$5TQPO>dX^i-Z(B3<*jlD`skck&2g5q`4oNK)UUsRACH2%2 z=Dkr}CaI5$TuxD!OX}DH^TzSpnWP!n!PJ{@+l#3!^OmmNfz$gdBz2sm&Qb4`)M=7B zS6wM7XMmU&tE(jSYRS7?-6*MF&1K%TxUoZ;1CsY?b(^Hl-|rWzcc|OdrzExCCY|~@ zcJQ;*x3*89++FH9$$NZ|sn4nBRRn5yR^PLK-4$@^CC z#M-;ne_-#!+HcJ;^_Xe_ky+|-$@_2BC#i3{Ecc>1Ai~-oU-#y9JJieS6_WQ3q<*5Z zk)4v-?Y?>4vuaM{BuQN_c|DO+B=uEE4M#51dBFS=D864(4B(+Uaw=xBq4@rHu`>FSCWx2Z|FGg(0>_C_wEs+Nze~)C5 zS`5c?3%LBdl-ncazKT?I!7Oz=4l;MBZ$=^pQ+G*=eh^7Y>V8Q*8o}q_LFHmeJr=Zmp_%h%@@s6)06M;55hZaDy9IR48P z!oM%!tO3Sf+`#Y^Ylbmae!ua6d_-L%;eqXhtnFazcEJo47~#w{ieEZ1= z|Gexpgoh5i31PStb0g}6*=*Mrr0s9$JPVLw6R}$}%aEXLJ5cqnDe_rJLUkD@W$NBdmyg5giKiql)= z;V&07;zM&8A6h2i4u-naz9}4A_#|q%dHtsm{$ThHgpaMg8{w1N??sD_Z@&TI7o}e= zmtM$pvS*(a$PYU22WDv&+k5ew{HZ%Vj8!cR%~eT3L2^u&Sx_+T30O{*M) zcT0HUcGh^s07C1r^Z}8b6W4dgJ9kdB&0%=4#9x}tvVY&)f$-Z>>hdj&KR27<4}|9j zCDgee-;XtZXMi)(=T>)hM%1k>ozcYVh%!Gf4;q?+8F73Srv%RTSFWl8>s*=nVZTqqB*3Y4| z*Gc#rhw)u446lJ4EY&OUk#3bXRY&&s_*t<<`=1I5pYCP+CJDDg_af>B)EiNsb>9qr zz9?bNC1j^a{sR(fsh%M5TM%06ouI!!y?^CfA-7u*M${7U5K#vUCpQZZE$AIfy%$_T zV(qN!5{yJ>6xxd)?OQd0Qb$Lb`_mXhg+8^^xdQ*o7L|^uZ9^9VXX^sy4oUdDXh^$g z%aDYJ&1b*#NPJMj->$z5kY~2C)GbnbkM#R}Xmvz=+`SsPCE=Fs!d%zKw)}^*rFX^q zk^3jCFX$Z!?-WTM1}XKalLpD*WfFd2fRN9iRG(@=DacAYzndRIl(s*eEv&9<8n z-r06L!r_Ii@v0Sr2v5MeY^hV%t%OExH}65c=bQI9%{Y7J-G@@dnF?PE$RDuE2NX&s z=Zdr9pPq&FJ&|!D>LW>pzfN``Jl}k7Rx^dne$5m}=NbbZfwqUFM#eSI8m?P06aUx1 zh|>J;h?9qpCGM0O_tqBTuN7`Di0=(E%-jDFe1Dc3NNvsUoLDX4VhNwO3Hh~z&mxSd zw|9IUeNo2x7*ThL-kvOW$kh`5LBfGfuB3mG)%<6&zPnv)`}-u^(e-Uokab5x&KLMY z*~b7c$=alIw~KZ00oWD6JiH}!G5GNi29N_&?*rs;36m0<2p6bB`#IA-h1q+7I;e%= z2iNA?BI->;i`o{b!-faiEOnsGHRV+j9xCBq#VR;R;#rHi&&L_&CH|tsACWPB5@sQ+ zuwtkEVis{6S>%6!SLiH!><^>?Ih+0qdE2O>I#G>S#gt)+s&Nnh+J=?MO)8{ z!k;DK3&L%?aIO;MaIVCEfcY8oVK*VuCBU|m=3h|bC#@F|zE-$uGs(k(_=_B;TU)v~ zBIedZL)N}8%WUxF<(vmPvj;?SYWJK4U8FYH6;Qh}LOOR#8@~ejGD1OTW(pX^n5iuF zda-Xmz__ogmv7sJwfGZ#3}Y_C#XEL^8s~G^(wLDfwL-$@WJcx6i@H`3zIEls7`a*p zu8xmvKzONjAi}HED8jR&rz6~EF|-org17LvZtJ^3T76BI9;Y+sUhn5d)XDJ=BOXb9 z4B^$uPwj=;XY2T{jo*#9%yoWCXQjT5)?F>(q|Nx75Ra(&vpLFENJtJND!ZAZ@~y4X z(sf5dyN)sme|7u-%(ipv_V%3mbM&Qb4sSxdqWuW$yOg@9vjm@IM2$%5VbwMC1=N4G zpBk9EIBuwO{L~|Tcfreco}_fSsCs8T^*~oteWafH3R0h~ry}sjJ*ZRa>)qc#-p~Ei z+qOQ|l~jMNW71P+Vwb7Z)#}P6zvxPfMt1Rpj6ef9P`5PyAHt z%ICYD}PZgz*d*-woll~;e0)J1CH%9hz( z%3P$&{bob=Y@QwQ@8KBhqgS7Q56772A2lJx+Ue!z-@`H1K3^x0V(s1a6l?FPr&#+! zb%fN%);`^}SlunD^VG@k^7g6!#oL#_M_pa}-+L#?Ou{5&A*f_wkR|LSA?yJ%*&vXx zC9H~Jk{ObbNhZ!rAaSWlLEN`jm-@sOp3rKm)mAJjR-jU=6Uw z|M#4Ge>0N>?dQLtzcXjK%enWSd+z%C<;gJ=xL0&DRXdg_T_VT~Mfnwo$>X$)^c*j` z3`KfQ5JwF~dQKEBoV`<;@8_puuX6`eZ%fi_viOamNVBN|e=Gq1c8ZU43q}-*Lf*0M z6@SQ|I%2wbmMPsvGsH`pa*RnofIZUVhAK)=2lb|*cDoLUnc}|<^^gm@5aMG)ZJBxi z?~wn)P`y*rLGg=!VyB}8@n#Drc59%EqYLq7i)>A?CC?H0QL1Fb9I-Y^VJ;&)CLU=v zPxuW*n#~g-Ly=~s;!;DAW~E}gp-8j&;(9}oX7j~uh8kZwf5ZZ@%TRMlD?sft)aQt| zP&{a;v}x&}er%}1IZH;AiKh%zHD@)bpBrjRif4pd;EE%c_Og@!sNWdsk(90x<>I8F zo=({g>WrqiCMrZ0_NJ^JULlJ6Q1dk9cw)#QXN9OR)ZrmFkEjr}hFax3dhmn#g&?3%@&DkqSO+{BJtfQg&hlV zun%=qQ;x?g4nTIyP`{{12lbkv7K}ZBzs>iCp;nGf2lck5IKNu)fsws6N$Z>YItIGYj^4Yjx|9n^F~ zO`LE*tPt}IHD^LPs7g(-W-G-yBYO(DuN0dM^*nN4Dg1`|$&C9)tP+

ctuRL0xL7 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 CNdjE} diff --git a/.task8-sweep/ref/LessokajiWeaverUtilities.dll b/.task8-sweep/ref/LessokajiWeaverUtilities.dll deleted file mode 100644 index 7ca4e8fd2e1804f92daa5e6d85a58c931dee6cfd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10240 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 SS9XlS&il$=*;6}6f8>4Tuk0;~<$dMV_FcvD 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 z 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

4t6iOx^8vDf4(0;x6zp=_ZA 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

g{puy*N2br}{q2c0%3zECD8kb>D53;L( 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%0DGk{7x? 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 z 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

L^lp)jh;++t(13WpKCftNT5N`-qXlMxBXVooQ8g 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

=7I;2U05VN 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 z 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

)C8)uIAiD7AF`YH5j=~v*` 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�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

yJVJuyBprNe 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�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%0A@P@W)}gN=>(W@fU`Y|-5#~u17|(Qh_5n>0x~i% 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�=s5XOauI z7ar$cU@BU?1?fG`z0lOKI8Ph+Ywks+f#5XiS{Pq98N1lbRqX#<#=JX$;-l{q-5tBs)P-O3`M;jeePCt?o=I^SPm7QJz$_H3`-?|omzhV3 z=^KRmQE~*8AC0KQz-U_Mv|Aq<&=93r@5CzuIioaDP|CDwMq1?6Kk6AJ&*7 zHvB2{k3z|7%oz=r{<%2i6LU|)X(sHzh-%6WZIQmN}q&M;gdb-n3h@$R>ADD_W`BOdr8N8BKqiSt58E z<)4UK0;${05y9I3qiLr(6NX<)-DPfv;V)Bmo4`oP-``&5uRW%u;1qv%;4f45m`Z|Y z(pSiTMCx9%(Bk@d{xmLizd3Aiw%T|*IrX3^J&MztNql}aC-tzIF4)pGB!h%^e$FAM1gh zsmIJi!72V`z>9!gqqV$kCH%OFvbgkYuHWM(pT${`9^;}Ge^5bjRg3?|*OfTDsm0T5 z_=^_%Z2SQh&$Z#BEY4%Y-?Dg(4S&z#tnkwnNPm&wRDW*Z)xaM);id3pz0?!tGbbE) zHSiuAuKoR_xvZF|1a1+gp2W2dDL>T|e9F|Z_$z7u)254Hr~jWeX@Y0c^s9KDnR?o6 zRekZ-eW_+9JzVmK?YVF?o-gNYZGEgpoVWG# zarKA`wjwh2cQ4uMIvM=A*csUWrX`ncUCB+M{oSydh^w|^O$XW?z(i2Jsldo2(A~!5sz&V&~M=9`}=VItc!SRD<$*s_3tB? zRO#cNA|xp!8sqcp&94!Pl<(s|B2-C-b_|YJ)UQ!XG^wkw8{W`>w_YOb(j*KQ<2C)5 zE`3VI_CuHEp;@&5Lr_);&^)A^Rbo`DC}a`pQt7E^6@IAmNb~$ACBjh$Y-Pp|)1LcuCwNAjS*~r1tX>#X2FdiBCrW6S61aF^mHp26y$hRau?~R-HN7qFT zlPbt(D_|9Z$l+334~E~H2G5ToM@UD7J!~74x9NIUS^&q%e}C?2kz=I|5BV1IB$lK9()+zWHy?GOmh6ZSBk zeplCr(rRH$e~>FwVp%-Cn>~8S*f7@w=>=hz{;b094<<@MBjc6}zjOP*efd!{snJgF^wl*Tnn$`p2q`{DIrv!%zv9##eaE_BV7{Bn8v9+n9C zcDd$A4TP~glU;Kq{63valu!MzdD3(d?q)M@!S%&8Pbw02vb^j3ct4kd^Dw_kSKj~s zTpB`NH3F_bt}mpX`4~P7+PBrN`I26M{(3WfeaN*?3MJQp_ItZ4OzKvM;eQN;)p}gv zQV3as{Z_aZOF85d&`$j3ijc-F=keXF9P)G46(tps--7gRx}v2==`hg85JvyA?q-IRgQ|23eoyk@VmD#We$TaB|xmpk2zGq6+OZfI~(f+&_nIqK|_OMr> zJ?s>fFTEh_J>LqY*U`GGJ8#dHOK*~Mz&2yKG!iYguOC(l;NJf&W2^Hx0Ijuaerre);dg zpKlG0S|z>f<6X|x(mXOB&)DT$BW03{;r?fM)R)p>G+kf6k}mpqm-8!$t>o#$^DSsc zcRANeeS}^5qkrLkC2E}%f)?8ka;=l1g}v?7dT9&U@egmmHb|Ffdwkx#QM!Q^;}81> zZB!J8!8Dm*^y?|dy;HH{}@x zP0ypYO8*FZ>%&$_UoG}0j%RArcTyuX{odb8^Mql(1hiLkqP~|73wu~=xK1vN+96#d z4}t6B!l+%6zUII5qNDam6Vdujs5gH8d!=b)1%6kXu~$kYXKmo&`y>x}HP~kCleVDg z{QF6o{3TD{$ugkdk{tDuu~P<(q!^7Xos_+Dx^|!TWD_zqYg-2*YfyoR=WX= zheiD?g$p}bi5srhQHLaD9j5m^cyH8EX`wLI=hIQgq_5C=9$fcA;OUPDne* zFG6|v`Ja^bl6!z{#!2Zg8SlqVNoU9{yTJ3`s8iBaG&}5u@mz4=ddv?WFS!$STFN0$ z*v;6Zs58=aVUM_;7|~~?KqxnUKH8sStY-9islPC`|8=7;N^6CkOs;KXjiN6}3iKm* zdGhOB%jnC}baLxq(4URIDpiq3!}YUg^k33zn=n4s&l}PZGC#itN8gmjk^NKP>kH9; zOKXL_*RNaBSu#I=L!)m=m1MsCC(*a1F<$@rE#}t+Z)5!$ zt;<)*k#PMz7wwQ+ZN>0%Xovoct}gc>=fZh?H@c==M0SDyjIJg7ZR7FX>>{)mkD_bK z{e_)u9kdI2On^L(%-j1~G4*f&7iI_j=S&{sm3P*GN7~#^Y-wSEA{9(^ww; z9pAsVy=*LhB#h}zjcO`KpsBrVE^qz*zxchQn#(^6d-vZ`zJ#Xzw~}v=asRF4YCHJ$ zF8!9c9=4WilP!3D<;Pmf&!H)QZREFwJ>vO)@2EENhd!7!}LCoLxm&QUC2+1 z$PeUXa&}k7%40s1j|qGG&!O@KVQ+gKDp&eADJ)ceh}Q4J^(2T*kZl!we=fboa3~+J zNqzy&Z$JNuaxHRCu+5k#<5$b~Q2r*#9S`vBovaPqFC2)OBo8MW!k@^|!fti}_IE7i zQ@O@L9^aeanR0z$4|73#`=s7X`4w{A$=tK#;baZ^7fxb}$jzub4UV779N% z5PFF*bLE^vB7Jrm+U+OxzL3j>J?tce--wwnzi}ADOEVdh!WPQY$q%V(+FC;NG#T>}Gj5nST{( zRO~nM0y6(P)Tr34auInwTqh>Rekbn{cCyy_ygmM2{+&D1##rEa0@;l~t5?Br2Ew@3_@$Qkk`MBkxJ#vUJ&fi-Y z`=cCzHokg~w`cq0D6*#sI#$^G{B56{ChQUAvm|Vve4Wh8XGz$8xz8D%9}nZ@vn1?* zoI~d2vn1?ixr%%i&cDdm!?N@T#$Vo?`xm(m`MXf=U*)#KSRY$1Iwrq{){7kc{`$B) zko*JK4?K)q8qf28LLN=t3HAerk_#aHAa+uoLjDTu2cCy!Jb#|BQ}S+Mc(22s=jXJ1 zhWs-8j_%kqGCRxj?`G3px3OihXJvn3cz*dBV`Z`D<;G-VEFXWoAipS#$D@W{l)Irt zd_Vt7atN8nFO0qDa&JVZxaHp4i*+6k%`rRdNM} zv%{}K|2FoX?0tdJY~K%OXE&IT=m>qG2A`BSn4w#EG`&!hN%1oHBJEW3og z<@ZERC1d$LmGgzY<;N7h$}{h8@bdQaw<+I|vHWbxDPeE<*_42bn7`+`L;D|RQ~LWj zAWl(|g*~j#RXE?`bmgKj?yqy4p?rKv>`zY_%-HL3)s)Z3+2Fz8`Q)$Qeb5lOx)Mzu z0poYW`%V`+DaORGd^F( z4dF+9JT>lF#Sh}~`ohEa53Zx|ucvZ%dJ9%_t+@8e zH8R$x_R0f_kM*OyqCr1}Z;$oyCBuSeP{7O%5P}B_8s_&4EO>W$5(@tN;K8KAVt1`+ZRH8 zxgQs#)D(vIDq;W6#=oL8^6}ufPD+0tPmBvzM$q;pyk8mLS@{sH*Sf&>-&vVX;om^` z{J74_Lh?ay0|<|$`27591x^u;&||?bgR?0FM zr=nNl{_*&GDz(Y{`T5BBo=Q`+DDOe>y_Aj={>uZ#-j9Dx$rN_7Ul+suJ$M`W$WZv| zXMAs^QW);{p*$AG_f^K-;`{T6`;U6A{>oGzC&dp?7La?v{YiHGAZ3@Z_xd(i;p19R zej-1`@oy+qXp!Fb@FB{;+dSOKer^WiXYoUnk>vFY;CU>#h#U|9ZjXOUnFaNX$FJ22 zRSJ=+T@4emf&{6-tri$^!0IZ{CmnL zXx8-|-d>GU!iC}e2Jr3paY_}2>&FXt|9QOfh{ESUc|D9DuZ+CM_wQuOVtD)yl#S%S z%D6vNu9ELUes9NzDkJY>{7>NifF(>&9*_qMPgIUWJ>mU9UcUB(NecfuAOHR9!T4dl zgpZXVVLYCe2~(6e(bS$zRYsy&6VX0TRdxuwSuYq5eKBFG(&`_c9*l28IR+(6Q;w4B zL4T`%!lz2`zZiaR9?ZW@n4#G(wKPp>c1pR065v#qW8_H&g7XtDjL33HXUPsRSk_Hz>EDf7q;eu34Zz-xqEl-}pc z7Br>zxsn5W<;VLx?9Y|(xpGz5&ANco6Ba0M+IYD4_!lX&(3D=7vH&gG*J4)~%*Yk- z*;gXJ;mQ;V({BaOk73P7Wrwgw)W?E^aOD8`W&zB{OK>VDg}wE8v2xYN#qmoNc!iAT zN52o{5X2%CzN#y?1CGlN9Dt_!?owJ%IM#2MGJt#*w!h;IYOp+A(6pmZe9o(%K55)zfJ!rt;oQU;;*Kilx*PgaJJtHbtw;4$Qv!9mQegpx;z zaJMqmyS)fcQRb7CBhX7pNKul7ooq~7=pTXe(0cx1o?e==n!@{w^F2-Z(2nK7{aZr1 zvcSg=<2_2ckAF|dP8UR z$d#i6z;h8EpPMBvQ-=6B*_Eplk=5q#mUDETa+cf#ob1Y1+FH2%C$qQ?&3Ikbg|(>6a;M$^Ol4 z?AgSX%2jeXoG;0))k+}L3!dI_@TUoDlsCywTQErfOJ%aK2UgsJ`5K8|DTTsVKm6Ew zWtZ?}W9NJP_u8QRisAZP_&vEVkV{`@tV7}k%;a#6SpcQ{-GQqS0BKAR%rzDbiD0f zvFn`D&Bwnbo>xW*JK6Xg7%xt|q1mDy;0wm-ifTvp(fOGwYDyIRBQc8Qmj zXmSSlCfH3r3H|dRc12l5E(hODyrOJDGoF8zbVX5No*SQE#>>}C`cvsB4A*OLgQT0v z6f!@)7G|ZgSJ*A)zqd@PRGLA)czsJ54$ps+ZYx`aU5p=p3-hkh2c8>4%Rv44yUJU_ zSl?LEU1dC)uFvrQ#Z$SN-kR++4{aVn{XI$hYqqE<7Oo>Ws!7slg{j;f`uLhH35 z9M*?Zw~+Dt@>h40@%-{vkC5^F@>l;LuM+2%zj__bI*I;`zuE-mA@l2-o5I_m*>d>3 z`2OA?k8<++dw+F`u#;Ve_DT)+SND>qLTGMMZM90+&3?NN&!du_RgXfyiI>Ml=$~v& z3Q&XLyy5ks4fKz0Ce~HQ2)o&?g^V3cs;9byz3+zxs>x_(Hs$@lKvjqLP$0j;CzBee zJ$-yW={Yq`*eSxVB{Wpe`uJf&BefPhXYihHHr80J=i`cm#%i#S|4eGCzJ{jy*GwHq z9yy-3Z_U)9|uPowlb-iszAHL+Yjz1NzK(- z!ub0ibGB3)`M5@MD|LpjQ>^BnzNn2FkKtnb?cr_Iw(y*vmv6-&#-2-VqxL8NoeR$c zlAl*c`Zzebt-4j%EuM$0R7B*u1@MbG(8^)R-b8v$9EgrmAT2mYGWTqCU;ib3A^-`gL(V)sydn+n-7=U zAKQr=Z#2kX{;uZxP1#l?s!)sH`#Om*0=7;a$~6!Z_ZphIdo>b60*m znUMaM$=%g;!dTum)rr%?L z`j7AueW~bA4NxVh$FP6>E3h9MpgM%z?5FqPYkbKA)wX0AuCE7^L)88lpLH9@*dNJn zsKw+7(Eg8#eM_w%?|c{fo5{me){Lj`X7^%Yes1#H>SSRj`v>|ZHQn#1=@ic0+&xMy z62|;@a*tK_3*+(ka=)*h#&9~G@#PDl$Fn5tQ?*i<$|Gim+7s?0d3(dp z&+Xwe)wRM-c0P=^hcnemAK#9jsrG?%dHwp+u(4?OEHy?L*Q-c#&sFo#BE3SXc}xW4RjFH%pF$HVnypL?;|`*{&x z+`qvcmpaJDXWTAzv5&91qt$q{7!Ui~9iwKU^&7Cif4~J4&cBLw70g$+=IvYacsRdO zV%0uv@pva^!u-{gICU?1_j&$2IYGThwxB=gpOUCfc>#~-d4E_{J0)3NAiRg+__kZE zB;)>4)K)M0#!pq}q3QV2RCuMImydAMlr(h*#qS8|KcAAWUWIbuzlV7SR?!6qw?}t} z@NOv{wUXQy?&DRKsg8RI!yCeL2bE>1?V+6c_HPL*9pmE`kw2(g$V(vn_k>-l<5i6R z4Y;6{lG(n%Nc*aR1L3CFOqk|^&p5HP_N_mqP{1)4yZM|iuCnxX!qcKO0}-AQ%?c= zftv|?)B9OXBV&4p)EwNN(mSN?L{t6_tNVn#`8%v0rR_0)M^qht7hayfz&KrD@-ON* z@>0_V_qRvYZNlXY_xGz>F6?4e5dX)NU)5IKMgH0CHSqi)<(PUNP3QLswOJ1y?qOTM zfcg6=C)6tPKIo6%N;#$W=_$4s@sq+%tK-n(ednhszpHD7v41cz?u@z}!}TL0czOPz z?xpzgVEE3bYQlAfx3?dHi(P-HZGEhzo>eEK^*^%t`cUW8AIX2h_&^Xlr~XZ@3HLeN z_sJIfOP>Mb0}WFztILI*`h2hkUQOY3#Qo$Ibu;+~ zQ9f7H3i3Y?AMO{`seh?gh24yg@AXZ+ruON}_vaGvJEvY(fh=`+#b`9t5hGN^j%Ia!6vymTt_}l zy{pzDzX*Psa#yWS-Uh!{5WATn!+yKYRk*t)asI(f&IWu(4xF; z%+i8=><5lUi}B@SPKS2L$FN?Oc19T6kJRYuTFo~xKlPu$SD8|4XpP7?-?4@^hrCnd zzlIh;ZVBac1LCKUD{sL2MQJs(d|@X$IE()tHMDs{uzY$#`BsPU z-=c7+-)+*K(aOo~h5fa*LowWf{JoM^TT2th{(L=G9c?>#Ka5Y#b_Hm6g|Yq&OslKa z9VU*C-tVZdHAd5V9QC#5DIA}V25P}%{vJvY3)I$9{7+WFeEqaQ?Hsu-)Q2%?4Ya`F zn7=OI55Nn_Ej%zDnbt&06LyO6v{gzot(?MFKMQY@r8U7_2^^Z8n8 z!NRZ}`3ATjPiv)R3d8f>w_yFAwANbt5g31E6X?IBwb6zPd)uoQw1dLl^{8LaPE-66 z4c`AudqHda4&UAb&&FUpJ?%wpim;o#oC@;`(mH6l3y|w^0&}`elfkD)_)A&AD(}`(qGrkl8=@#HXwa~)@m$!vzznC5s7w?D8M=G&wX*M>a+VkHN-qX^^ck9?#O8WcSMq&JY z3ev}E$9=4_54A^V9p66*)s%63e=Z%zM?y6R`4h-rh#acvyQuRT$^@ zm!^kmTQI(8A2+5?)I!IL^hJB9hJU1m31j)kMt-d2lll8Y@Xd9t?FShD<3Q+-r%%;F z$@}WV{Zsg-TBfj*jezU(uJq|zn-4{Nu|G9@rq)>)*F&jDpQVNPgm;RWt<6H~ZAS3+ zWv&*1W@qwY{ekqkS}~cozt`gDX60GGDK9mGXslfIPkduaEP!TjU*Z zpA{l6&>W$7JXgo?@C8}`T0c~s_vaRBJ;(!Vphr;rPvLsJN?E9VK^_F@pH5$-trd1M z{=D>Zdbnnrfaza^_V-4*Q*)q2dE7}~qP0frL*af6o;PYi^KFv;};r%;VAmCi3**c@Ud(bOf;pRsBJ;Bp)e1ml_yE7pzS{f z!#B6Iu9Gmmx5mQ!5(qyc4C^_-c=4;AWUY$AM~nC=+Vsg7{}m{|KAse919>yFw{Sk1&3pd+3d1Wi;PDRSW!>hr8KsIDZCv(zFordANR#@T6Cs#`o zcCrCr7%$Lv2)o&{&>kju^0dHdJU+Zn4)eV-JOx@iatpYg6?m3wJ;`TbKEWDKu{KHA zd%l-yvxJ>=JulVDg+0t4;&1bmY7fXdw9h|zR%s1B7009BJ__?oJZrSx!gxIA!5^db zZ@cjFS+9L2T(0AM%Jte86wdq8W75`Zi_xqNJfErZtk*Iq9On~m)bhx9f3s05AvbHq z-*4Nf9ir`bRKoS$vr!v9U6h}Gq879-8Jo26e0Y4?(z^u~}q5 zC3hF`ztM(}asJ^q+DLK%oKJr28*MzAZ57A&tu}|k3!y)5WqhkGB7X(0363JyhWnj> zjICM{xjFbbaHg=g{%zAHf5wm3sV6}HC|usA#R+45YM!x8`yQ?1{p)t^M~ctymx^86 zwL=t+*R$`n;}rh;2%i7%wR04X_pdv&>tuevYh~=vo{;(dZcT9gnWFr~{e8`homyLA z`2LIr@55*Opsgh@faB?$u}iy%rt@dF#=ii{&tEG{!*+I0%Y_1b>a2F(`fiTTWG#$Ih0na}SV3LZ@ky#nKd;K^vV9>xnNgQLmY zApY2lecE+0?|;ky2h73z@$uAop8eW5VJv?b(bB?wyeOkWJMLqZ9n#v&75St4r^DJ5 zAID}K*4oVz;VhtlUyqMyeaJKV!g_-lzi7+IVHn$mFU9iu`7B(YvoikFrjaK?`@Tx~ON%D+_BcP|n$~0i#(xvaFF)g^ z)}745vodaJL&(R@^X+eI5oG>7t}$tMw0L3f`F=;sq;P(JvMS?_c2yYe*THKuswjnbtA&Q~a5q~s1jY+99&7o+PBt0F+hM#z^AE@E|Aq78c*aw$GdTpV&(Nc? zk0ihJl-Dnty;RuE9>DeeuMEk~oS6P>DBpiGH2Y*>C(9M->-N*+MsR)}&(Q4wi*bJj zTyJY+n)Z(5rf0Yv_IbiCR(grCx|!AOd&&Iz(Va;R&97im*q_muQn& z)BYFvOn3MiSY|EzBaAQ3*WQ`W+UG^!_PD-IJ-c5d=7&LlbXaCRdn@w%=KOhBeS2T> z%++xH&J47VB=5-OZeU+Ye$t2gIeP_pU0oZSnAy*_qAl3&?ytqcpRneFr%h`kP;8wz4bH7{6%? zSRXsHwY{xyDhu8O&rdU-x2Fih{5L3{Q<*Q?3w?YevxEIxA4^%C?1z2qpVh^FRyb8W zuZxWBZm;xl)2yC$zZkxMtgjuidfOv>{94xQ_IzP{{`Y3qK>KP8*L(Np=WB?4BN_Yq zA@=Xci`ww;!S>zcc(5P%Ah}&{-k*5G{u}u0mj1sJ%U!*4G$j4~*mK zIaxlG|Mu`<_6l+=oWJj84YQAn$MDlmxIYJPBM)8-->1tOZof#*g8KD&*4y^O2|T`= z@%y>Rtat3^gq_S*kEcJjCCzCE1$!hG0k)(7^r3n-P3mC_p zY!4>~zYL?hOH=Jdp}mxREg#ySrA78Ca_52Y{imfR_O_WAzdf`skxN(Ew+Opg zEY!a?nP1v33VRs;p2n4|uj~O?Jido@Yz*T$OV`U;FM|8SU%<)2PPXPTV`sqE$#24V@vWsh z?Txbe{$YHeBlPYvey~p#cC%5i{o|#(?58Q5-+%gLm)qOtVEo|g@Vq#Cw|z3X4EoD` zGWOUH3AF_RA4_mcIYF#k6Dh~1Hg+cyDMX-DnteOwm%t3A}m1zEq@ zONF6+`~&y1&m6Z`f_3;k?m+$e>6sJum-8{bD1UffpMBCkoZKDKFUUG&kN0s|?CmbVf;P67KhWA_R!FWLSRXbbG!`2C_VSxhhZigdQ3hY4f;kHxEc z4qCjwb2?koucP%l&_BAGZP%x);QPb)cffgQ@w}JinEEzhC@;9a!1EqGxde~5FPzVH zGc8>y72C6iP@dIu9QqJ)1oY3U=hV=T3p-glj4##AtfhyP;r4C8r?a2Yw~#AFG1e%@ zU#}F#{Itq>R{uwM4_nmE#yaE#=+a8Q{T{~GsqT_fS9b`z*d{n%VMT4dKbfD8eRJyT zNodi3YX#1t?J>OudI1^Jdrn_T#`ztM^vyoo!~915dtr}$tSPK7Yc$sPkne!cE^4eF zM2r5Be^wLyESdM8hU7HYl~p|d9?_o~mD5t6B8JEYHL6`<%{xQJ*Iq!CFK4&&g@0KcMiJ;k_FhYp-AZ65|gQ z{p66J$IRBwXte28M`vP4>PPl*`w+^@e4fcdn|HsBU>K_TaSZ6;zUf5BOA;$^_>6sM&cr$1Zb6(L8k=+AezG6(!7Lq5KlGf=OyiEj_z7lP}FoED_xgY^Yp^Z0IN|w-dgfU+N)P--umke#f#8^h4x` z8aB3NS*YHAD^K6a=77tWP0(kN6TwH9P1c{^#=|}A#`DlVFP)-y5q7in-9`UTA0mvu z&y{7L>5tGNe$}$sdgJdz{`JobpnjQi^tQrI_NpD~d-xnZl+54fsaiHyFZJ=i%jW6+ z+xhlx#_zAx+%NQC^05H8KgeC6PbTlG3$0J?B7KT5tgq68U!R@&Hu5mx#rid$@Pe!* zI{O}vFA=V9EpsFFF68;M;eH`ES|2Ek$ESwJ>0{7r`Ffat5gV^Zka_%`xrzEAVHh93 z%~=23WL?^U`JVy%ADZjd{mI`%aHr@)gfab5xvBa@AAgveuFn(pu8)wZuM&2$ZZN(v zHFv4L-zWZp+#LO+kEcfE>W-a!|L{KBS$_YMultj`f&IV@gx#!ebN>9iKo7=naeSk) z3-!suP8NI)<~!sr*H4o#bcLrWxy5?m4?KM*yJ5om`?)3hJn{l4&z#&cJ#ZHf$Ns#3 z)++sZGG7m6N!XWq5Sha@;@eZ<}6?jQiWJ*CFHncIb`CxW6Ct=Y_rdE7#u6K)@UdxKypY##Cd46$!Nn!i->1fg3t;?;@ z!-c)iKMv?!_lWr7d((x{2lO}5qP^)HcR(M5*7Rqu5@@`8r2* z9ZmCXj_C2i-uX7a=o>Ixqz^5GzLnz7ZVcyV_))#W$A!ti>VbQC`ri2_zv(^6eEx!s z9n-Iqzl8RmyNY~4;?Dz*>5s^r#P#i%K4l-K_cx5+@$f_B4mIKZu=rzo*Pk%_CAgp6 zo_k#XT-e1{-Q(AT6M7Vy@_$0l5%#dpA^!ylC-naN`S!TJL}BttJ=w=<_$fVCcnRar z$5W$E>jx>Ee>X9R{jQ%Tr^5cWhySi$7k09V{dj(V*QZxt{>DN6f6V<|zbfozb0GW} zxY+?74&Tq{3-?30XY|$NoLVrSA@>hG@F0eFUIOno<(}2!$*bUc@1J*3KSW+wAHKep zcS)c3GsfRB0oIGkyR07-cCs`5VSS{$D|+dn|H4P)UDYduvHscEpZZ_I9`<}acpokA zPyIf5Kz;7NbmcJL9@g6zzNXg{#`z$V^RDYb!h6`Z$-Mo)sdpvg`j|KMzU04p!}=ME z|JKL*gwM#U)Dy_Lb>aP*#kchgvJU&Zx%iG=K<3ZuZ!WIVSCRdn<-V(bO}4@QKhL|T zpCnfUhvhxc&ykn)fa^!tzxq{jz4xIO2)8?$`8CQmiiq8U%m&tEqJf8pu7 z7~j5rm~IRthdzbxdFJWHNMR>y+6&%?1?Q6=fNjiSTqU;*=JNv_M*pKcJtzAl3f9lf za~K=RLxgJ>>{kru{w=ST5hV=kV?+Hvmsi_Jqi~-7{=8?69lv4xHOpXpIj^o!MNWtN zfxq$sjXuXPT!-t|-MogzQDL{Ze`NVhj9JGqd_{A}ZGJN&-~_t!XjmUQzol`M{8LkS z{+-{(7;+NBUuX&6C&+)n$Psq4YT)4f_C~u?JRHk6+11e)BkX1$K>qvX2N}`iJ~8mV zT>dM@>C-$u%s+zkN9K1jVt*Ig>$M>M`21iag^crMgN+=tm>=CDGT5jThV|7a!uON& zI~&c;i0wuA%=|9KY+*NR_6)4wnBUFF^l?mn596Y+hYf_^Gd;hTq5Q$O$Mb1<{%giF z!ruO0Z)1qChw<+>e3jqFnC=t*yZpXJ*#8&5pRtnSWBq*H*h0qoIl#C;#`+mz+!FTI z&o>O^tT-Ogey+(IV)Q{%{TybjC1d>@W}G2o{Tyao7l!xkdO>|Oh8a(Myf1&a@ya=I zybSB>2xBt2L2q7vM;JLiK9WDeaGb~ZJp5GtNCQ5hBI+~uo&3?pUh+3^{=pluM*am1 z&xiY8Tfuuq$BXEG)!}+mFy3%nLXU*=%T^F-K05gDlTIV`mu^Xd2I$WB6Yc+tc@+=NL`VG(T~U5lrE;TJZ7lxkdXP zt~Q3>;^E%$i?58+!tlKC0rVdV*BZlbi{oQ|g1Z;4GftB~yASi33O5*y?_m1l2J+u` zqw%6J9`ESFjmB8Cj``hUOdw-^w;0oD`zbJ9crxi5BL&0Rq&tj#RQRn?O5tl~!1`g} z(`4*_erL?9!u|8{Ks9{3QA+0XrPT26jYs6n`8<4wG4(DU&xTAGzYp7K%oB$7nsfO0 z;Sa|6dzjuvsNYkgb{X(VBT;@2p})4caJNw<>}KOzF_u>NlhOY^rWXV4PeI`UBb+?t zH5*$~__J}H{Fd-xqvZo0-@AVDFGi3sme(NHuSN)3te^aK;crHcFx+2WRs_?4uo3KaJr`}Q5jEg?LUwFf~<6~ucrJ?`J(@$l;Z-nQ=%kLON zgx!q454t4mu2D+n&qM1jzi%8D_MV>)joUtMwfrCBnMdMyss21N>XWhlJTjUIyYxmD zZ%-Z@FOh@5Dtm17K#Tje4$Gey!*P4Y$Ftyhx{*)ro5!ygPmL-ctKdeDvAp++aAr;> zfB7BEuUXDa_ym=xA7daK=2V-%3*+&?9?XCzJe-aXJi*63mP_VrAHTj_F%!{rJi6&2 zwAg-ocr)_`ZGWJ)jqP6E%yg)jKKC!nTbhBwPIeT|-!scwn@7p5pM&>Zmp^X? zXqf+6&|ckH{(?E2d@UQYS=8R#LgwGCH;P^|6+4fQ>D4OgU``=Nnl{#`sH3??*v;BB zg88&Xoz1Ic3EJO2McvIeble~Ro@jfympMilk9TNMZ}T&>9;NX5(AQjmWSAZ zPA22^Z@PJ0*n9n(ZMLn6>EZQno*7EU>)#jVAu?Y77MNBo9^czOE;64(>x<)ge`t}} zf}9R+32sNOfa|NDf0)^cd>L#r!pxp%HmN3`A02Ma6LzxU6Z!KEr@4c|$IRvD>teIf zGh+WVA9As|K-k;ATx_OcxcL2kELv=K_s95+-sbIJq}gAXzK?1|nwv134TJOJNRi9j z?c+a+VoY0YvAwuos~;9`RwrYA;?4Txrm+2;sCcstIUdem8%s1_BQGS6A?JXv7bTig zp5^H~*&*;+-5)kx{h$FSyh+Y&6>dWkBZXG@_O7ZR_kl1-_9&E zG!WxYfcC*G&NeTSKLZC8=b48Z;P(7}wpsCVQ+f{LyCA$>@d~pQc`&q3or_D&z2q+7 zfyHag$qjLTo1lNvBy7F8m%RA{?hR&iBN0y5hmB@7n(D_!b2%C7$3}BC8SBR;b0Zn+ z$0l;!N*|jm|-&v18fBD9Y5Oy*AeZDo%k@ zAuZ8ZpAMO<6&mZ)Ve(ei0MH>vq`uVGQQP^9*el<5fkKtIqPME`AKx6$nWjbC& zWBvNW?A{KI_345cN`6Vyr%R^N9>ci@7XM|IlGC6*r4IVrtR%1bfcuuY>?I!GCFUn* z7vC|9$#}nc$6SkM{QJfQ5I(sBre8jQ|K3&RA##Im{Cuo3i(ba?H0Y0vNvJZFj_6lj zhxvWQcg_A}xdE)NReaz4QrO$xJv6`b@gUbj^C-pV?+33h{>N+|B=XDn_kVU4|7#9K zi}PWB@ndreg|~zL$&tlR%p76({o(gKR{Yd_;Nwfh%nEr0^T)#<70Xs;CvNP|*N@Sx z9X_tU!mzITxZa9t)&pTTE1LxGH?OE+g$MKP(d|~$vNAiP3nJisnibDl$H`42q5rcY z!18;QhvV@NUQyqgf~NUg4XnT}Jlx4v{Q&R7tY~0Wkv|>|^XXPJwkCJQa0}X}X)Bsq z736Wy9^YKt%Bm!vhw^LLyS3%tjmL-e)a%<=*orn*C3(R&4BD#~t+w4Uyy+Z%{=8&4 zg)x6>cn2#R&9ZwkcB$ZH>ua?Ba&;R^SkckiAq?x;!FY@s-qETe4~FZJ8Xjad@4?f@ z^io&6Vuhf^^U)yI+4>2sj~*=Ef3%K}e<|es+s@WWa=#<|c|vFFqOg;t!ShmRQLVN; zdHQfaPtFl`vqm@h`bC{B)(gY8^@8_l@?N!a$cKfyS-oH5;rM(vcSTQYBsm1~Q?{bF zwMf{T|9(~xn({xus@a=w@1pz!L8cj|<^fSB$j$UdQ8qwkPk8jJ5*E`2Npm zs}Y*U^F~{3$hJ)Af3Fy0_3?3`bF8(1d@qBCzi*}axX?MyszB5A@B>ThFZM6|m-7QF zQ24Sjs)LR7W1&_v@{|tU$zB3;NwG|S?>yCeT6#;E0n@< zf1g?Lv_0S7yCpNNqduNmGTUl92-Ek2{Vgb&Yn2Oo)1PNm2z%H_s2xcq^QEBty+X$IHs)J>&}@om59V9RWIjIvR*tUhS*JYpmGeIM^AS!ltJ0e^ec zW2<1jgOafS8OzW4KVx|>wr2V4?^yg|E0OkBAM$g@xx~s8_O5@q#2WSn&!2a_%_wVx zu!qfp@;+7)Wql}&<#{YV#`+Ac*FVA6i;lIvAn%6fLoLi$Yccue8eV^6typp!ate7C zc`11fxkwnkm$n+-XD*4gs)W7yO|-7R>6_n_{~bTB=lMTZ!}UWw-tcO8{{3e>|Nb+c ze;NOakLTNe#`Ez%tFa&{{D2f6+rfYRhDfv7WUTX z9IGuEf8QJ{O&H%_|FdM7bqKeo^Ec1B>Ert)c~-Tz{;O|KO7g9j(R$@0SRbUc(CR|w z?|at-_ZHs6u>KTUL&&)QLaS65-XDeY8@@noB@gB4bZ~SW;wlK#TXu zYnB#UU4>zNVmQAGONy;z^1s107FfE%suaF#+pXGI)6x>F&v2fen>B>}bto;fGKF#f zy-HVEUEk*6@cnUUUxt*fvBvoL!_qIU)xuc*e>vA$2Ynn^y52f3>}`)XSbveRK5Ve= zV0``Q^YDIt=|<~c3dj1e(Naf<pxfW@!m~VGYZH0v&rg1 z+hhB^$@*B>&GtZhzOeLbE1be#gwl>F-C~uJ`T3txy49)>zHGzx;CsvQ4$seJ1MA~X zt3Mg*;}6zwVQ>3iZcP;SunynDe4x^DYa00t;oa6k@+ZQ3tWBd_-}9g~lsp>RpBCmpYZQ3{lsESW zxUh*rU-l6ql4D@cX|HZY%|QCT3dR=YCP&cmzSQjULi*X+SsAeGu9wsZ+m;z z8Yhh9r|d+iIKI70ua(cQFORME!ru1tu{DN_?bl-~S{S|uJQSXnl|8YJ`*>9L zQ>*KEo}QDv58-+Vb4>B^AeYUtN%%7Lk0eLAFsx4s*T>#vvcvxaJRa^5Wr}0Eus1)N zW1cYP-^Mfte1e|mpW1KDk>KNF@tPwCt*_h7kI(KXBJTkEf!C1N!~H}M(;b`0KZE_i z+sRn}4aXibUVjb8LGm5Az6LSF@f#WIyWu!P#`!Y5BJVs4;}x*|1M)NoPb~{@jGV;dyIF82_&!)!T}P=f+^@j% zhqAIjN6pEY-n1d`y^gX5j<)3XFut|3tfAwqFnr(UHosmpcJ%)U<2Sqo?-!Oec3c;B z(fXCm9DP0(;iCSWDr@feMEJ5zgYzGHV2-I6&Qf4~2kxa9Zrp0b?>|~NiYR_382=9f zcmIT^ciFZ8#%p-|5Md81{(!ObWi1@Fr|@uCzj`TrPhwJQM>Ao#fB%$!@9=p?4`FY8 ze!&5e;ct(M*Yg(~ult1mUDnPq-pB9+!Z8KSQsDVPVM%+(Mq%t<+$(#@QSK8SSo*Rf zaH=?d*5(V?&B~6BuEH+8d24t+vNFgqKp5U%xexR2z{AN~gR{HpUNngh|!Y+L~ z++T#O?C01?{uO+`q~HI3AI#6||1HQriguU(G0LK$yCwmOz_i%sj z@SDN&=WXu>Is%08cv>zR=xB-7pElv={~$*@a_y$Py&B{QMvL{ChOP{8ToiV)X7Kw= zSUK1+{4+fMYv6e+-*D^@#`NP>zUip&aq6Ij@ujYJ{$Z#U!Uq7M-`c`-`OJa z9mn)pB0u8(uEI6aagDqOj=#b+%Hf#J!<~%VvvQ21=N$CK4|(`FN6)#qy|X`Kxhp?# z%n|mshoO!X3>Uvw%SEA%Efk+We_F9}g2R6vZXbAod!i$h%){5Ooa9(54Bu;bm3y+| zIGMkHC`Wzdctr8}c<0uYA3HjJF47bGJFs%9qd%JQ`P3&?PIH9$_~ObLj(B0GD4%O9 zKXYvJ*}mnXnU3?q-ug4k@i!Uk=PXC7FR(mnLVl|LA7S?yUq$gf41BIh@CNSg-A+|P z5$T}xCLkR`Q$$2WP!tptP&!Bw2$2#>AcPhmG->ICB&2sb1O!0>0)iNNKtQDEb7s!D zu)qJC=f3-X&TN}IJ3BKwySr<$%b(nWw?}qQbp?_mUxE4ByQaI&3&ZpJVCVK3u7)#l zdG5-)XS${ed&=i5*Lq>k_J45gBIEXda3#_5eEWX8XS<#W!+gs}Fn+o-z%^hNuK)5A zUVjC;qR0oqKkW)~m60(&=eUd?Fgdarb;z>i{129Dk@QLpVs}N5JmIuDXFDPUU5(tEJ5j-({|k z(N5l8Ua&jNHA>iX{SR~b2jTV$+QaikyH~n?685wQ*1C2I!+eqz@P3uuYh8uHp8Q_t zx`gRzd??a&n~eD#>3Sv{#4x|tyNo%wy*-eh&g~mqW5}4_8(kS>%-<;2C7Xw?+vKV= z7njHU+w4ju?-lvC#nn0(obf~{$#nvkuiU=T+4*< z`HxGxvt75y<=XK6b&hM&kGTETmtnkecdlz0nV+9S*X6m6llgdA)1CP)ulYQ^XZr=N z*U{p9_WH5V^%mOsK0IHf7)7o& zwDTjFukuRte%E+g+&B7wYbwS2Lfkj{pesPwvplx^AWEIA{{k?9w4v@d7=@99CmSFl3aDC^?HzCJgKz~kdPM#tR&!>ZZA$>5# z_tfX>yXA5&#pB`j1=ki1VRxH3*vRPHuASr&(4JI`JFZl6`~<##cU-w>=l&#qeZJ%R zliVNLi`;(8aDOL|KN5~$g-{;HMBj0JNq*J`>dWZ6u1S>s9jH&nN0+%`Fzy@(@hQ>w zTt~^D2><2!mD2wS`87WJq02W6x0fsY$km?wNceBpIPxjs$F3-0?B4`N|KlpNxxCjC zSGVQ3zFTm926{bpjT81<-=Df7Dc&3U%RKJC0@Hs&c7=1p{B~cspAh}jHDo2aGqhg= zy`H;f35T%}efab0&t1jB5o`74<@-bR4fB)S!k**xll;-_IP?!AqWz>`n>R++lC}!Nd~7Ix+oS79rR(hLt1Fe+ zoDf}CDiVA6DdR3b9`?}zt~*j|CV&i<_FQuB-i?v`@cD;xzyI?ngK1Pp=f7rC4RoP zl0GB1s)GKS%*P||%xNVpMvMOV)9BVxEcu$|U{9mlNXN;WpuY5uX)9gx7|Ne_Ogrg0 zxgy-(t`O5+(l%iJ@%z_`(Ozm!j)nQN+=IvwaQ)%wSCDymdK%qc+D1OP3f|Wi(?PmQ z=JWTf#&nb_Zp8J!QxC@bVme99$+O^owqkUade~ewrn5ARJUfvu|E?5GUJuXrddGB; z%E<2wg84HsU8N~ewEiWG)r;vSMU%7O_`H7XE*<|B8=4_K+?KUvuE~tA|u7?5Q7m zNbNWA^?A2p1(b%1Yx|sHI3;hoh0+?Q`4B<(qCkLy=xldFBzNp`XZeC`u0n7 zKdBy>r*9wAU+QFY_n3jwhh%mKvI^sUWJV?L0Ul6n0)Am&3Uip=ZR0Wlv*Nn~E$ zhsJy?okcUA|K&qImbz{c`vY&)}%g>JaLb@u9 z<-caYXi3>B@?W%nHwTT8hS?17pOvQDJU`|eDGE*HeY~`TjOBg26i*&om6wn4QYu=M z_hm8TrE6qf-j~Hpkc@3&`)m`G?=>+KrDkNlzfm#YNyEwfc$dX|FO4Q+d7mV$5{CD) zL;2qxGfCP{abDhIV_MJG>*)#e`Q`%q$Dzym#LCtyI5YdhepIq zm70+G|34yTy7UQoDb$CVF*BtRWL|zo#LSYGka_vZjF~O%vMpZ}6CfS5xi}_BDkbys zQyeo_dUXeGpL;~iJgEhlm!ESnKT6%ny!@PtSs)EVv!CGnT(w}4R7~c_e>G-_)M+Q? zM<$G)+>BW&#gY$!XUBv|zmlB>%;t+;A^lF~-~ahLW|eeL7|V}O;9808;_C|$-v=re zyH2Vp9LD(kvnnC$rB1>@>_95qZ;Ra^J;L;&y!ix1Nj|%A`J6g1ULLzi^0Rqkz-DO# z`8hm~S2=d8v{@L&A0{(aBlagL-sZZo+ofzu-x%WQ(L1H19v_7BId+$Ho~%N=b!@cs zdNgjI&kszGj*%Looom~|{SzZrYDI1a^L4npQu?akPrPHL5#-mQzi~1sPFf}$#ByQ$ z^JGxG6eAqL`2KW?O_0((hW+UsyGJ^N%Ts6XW0|5K&bF?@gFeeKYG zY#N&;6$``n^FD|7Kg4E8S}c!8i1#;)h|QM#Z2menS876zgZWRBV+*7X!k+)LNb*OE z|yDYa0uh;pC5Zf8X3>yxc;R9 zN2Qr)=Qp=`d-a$!mmCC!@25ziXVuQ#nN`0Z*4sxouKqz ziR;DB(s_!@AM^3KpQULDe1F2&;%_zEQn*%mpmc|O>^PYvVSEc2)_|e!>DF@?>*EjHd zwiLKmtY5T`eFCpbm&nUtyrj;88`7X;jQU@Rx1};P<;NZAF&Ur7 zyCbm_%&+(1c%Z)|ImzW<{v-5UGfBIDH9VwJt5#|$Lh`lR4AoKT!+z&02LR0zr zJojJjNjq%57JFZ6z7O}e{a^5Y)!4tJU}5Ax+G&`+MP2?q zr03Ek@^=m3d7rp{rF1fX-b9Z3PdX;-xjy|TJrfRN2g<|zBZyZ{7we<)$mCAKFhAfk z{(b<5`~k)puTQJSy&{jYxpthFyxQiC0Y35}GOu6D#l9;4O6K)d<2YaWPcpABTg8=^ zA9~_YA9aqaAbV%v@$vezahxP;Xi)LLJACWOXN2*1Pk7arFWEdf zu7O;Jb{>WKB>{1bd6LZ$ac{{1Hm{F+TV81Mwz%eU0owU+B+OTcYbCQ>@qatZe+H!p+>IO# zaeuGY^84f+VM0>*NVw z1za-^_iw3KzOCGv94VG>D}P0vC6;d|2a*?v<=e>#28Tm@5jhyHS1nk8 zyn-ACR=|A&(Od5`GZBTCRVVrw?XY9%KK-uajGo`TEKwM9SedD+wFq4}Qh; zJYF?nlRQEg*Y`%k7I~R1-ZbGSd7UlZHerXHWQ%u8*ew^>+&>{kF1DrrC?QVnaPH;y zMkOT5J%w?5-zFr4sGUQ6axcusbEcrWId`m*E9B7Nj zB;?DdC|(ZQhiBu9%--rC!Cfm z3FG#?6MvD%+TyOnv+^`sTu=N}-e-$fO)QZMZ1I|j=jB_rc>ToRh&4&nJH<9OC>|oS&EFPs!88`FUBMO2+!&cR7HJ^}+A*W-^}tSL9t}JpZrA#lm4M z2=33cNxUjQM$`PgYqIxczCWJ&`&v%^_ho;ZXC?k6?;`W--J-;Y@&Vy6_UkhkKT3Qg|4eay{ac^-w|t)B zb05Rw*olwjo>%Po^;rJEW;6b=Jm2P}0sqJmXy@#D{P>>88_1i%3V1uYNW`DYapW5! z{!~sSJ4f;5pUFAo%3uY2kc{6ie=eUVUx4`R-tMNtL85<%Tr1J|o& zd#bpHUB~oS3gCIUJyqSM36W-S-B@Z?xm_*W5$MZ@$j`x;u*e(|+#i?(^iw zkiPdGKX>3wzWiMFQ&R_fZ%<8k*=<~3laJv2KznMr$KK(F=e^&C_l57N;|?Ud--7uF zd+ND!$j@N;IeVa&aTn7!g!Ey1nz&QR!ytXsp10kt%P?LO9Ji;1do_7BIBQR9_ha%d z@X?+5Zshl&y|^U4pSv%4G&meQoQ%)Q_ji9u#^<5?yT=Phu$FLsluH`m zo+ga#4;T0prWfsvOYsBUbszHm#kijIzIzzi7wQLB(g*G+oAso@?j&J&Ukt=wOB&*4 zkNEOIEECF0gQQ{Z);70E8sUELZ;Vfdc%P&%-LuKR!};+^(m3}n@_z96r0?9nkY9uJ zK}plx&&i9xE0SirYdyyGJ%{w$l7ij6$SokAlC;o0p8Ov8VA3-8YVw*f(0@*faDV&{ zUp|bz4d>&nq;>95!a?jhq<@gK-km}I1^jQ)M)#m6xV>>ue$~A{xgU@_!uhZwDaIZ6 z6yt3nzAb5w+xZN=0-Tbx&mBrG1RqSwc6&X?c;H2N9(8Y_yEb_=%-?Ce_n>>2u%|p8 zaer&`(tsoGS$4m2)VHg~M2#j_@_cy%*gD!VzpPtS?~iW%pTOPkvl+KcnTx z!ujlASKL13`1&#Zk9)7Un+S(ESHXB?pjWB875OKy$x7Xw$asEUclRXY`FY*_AsO$t z-Ee&#O=X=9FhWrN1cWA+Gx+jp|0xRI@-Ii$=aEms`1-cJkKK>R>n6keqfSrVwVn9?-SmR@S?zu14kUZQ z^)PAgzwWbS_bhl`d9OpM>x=1^LjU!^UTMvi3pIR4FC%@rK0Yx0E3jc|45y!T5c8^4m(VaDn5` z9uAhD++6unIGi1Y<&P$}RQxK5^wd6RrMx8^?)(tW_gUVpl-9zY|G$;eRT#fdbTYY> zvKr&!eE2oFwNh&HrT8{V-O9MXynOwh+)iOtUiwyY2c;gl7yLiBk~=D`ZT>sCv+_U~ z&o4(x7bT|Z%jGMibW_gSY^L;3ysEv7S5N7sG_|>2N+0Et&COE!D&eoaOy41;zcS|a zm+p}=P-$QNr3a?GuiPSk8qE8arMgpZVI zHoFpsDxo%4P5fAivblcZFlD=Nfupbo%uhBxQT7RY{-5DWt}r|wvl7aK*Kp-H#+}ce z!uw8pf2y4MpY)$9P*~tsk~3oF>CVt3t<<2gZY0cHr#93htfM!7`J0#8c$Mrm4$uOGgr4*nr!yfRHVjMe)T z#8~T?iQDkI*&^%X&XC@m#e*-AbM-t8UoBEM?E{JNZ!AC*5v9RL4A zDGQW}bwqwS=fVHef`uwI$#HQ0aW^9Og!=DvN~qF;><9A&{Jlbz4&)YKA4uPwTw@Wm z=Ta6c1B5;I7Z)o#FfQ(AUrt$~lnMti{(cqcu_`^^6#K)vzXI<^OIfOH6^8e3f$yd) zQ_3(-OL7#Mum9|;>y%_#K4b}R zKde)-$oM?!I^{6=O?bZ=d>>dTL9-d}!F;wZ>y&50p8bndn$+XRkKWgPy)w||{oOYz zql81K{BL@(zx;bHuf7Ye&zCQ7Ooij=&}FldBOJ_rfbx0i-L1-L;c&JF;-8P+rd$$+ z=a-&9{o8rFQm28)f9EGKf9aDhJC&Bg!OkziM(R$bGsRE!=H+FV(u<78w@VpM#^c|m zEW_p5>o8vMRhL}~Yl!Q|`R2QorozFDzhCF8F1wZ0WM037JEN6iGOsVDb&pl5Hxl_H z&OfPVoH9%}h<&~g+B01elro#2cS%x$8;k9+U;4oPz-lQ_ zsxqJnuAkReReGi>^M%9MQux2`bWKyP3I{PBpVu`*S=JQO{|f(qjnr%;*RzuUSUR#ppR{iCpAWry%J$F|Slcv4R&rNR-6 z@85vbpOvo7`SM}neZWhiPb;H@;d>%oc>Gtz*%IRmz(Z3@ly1VF>)RD&fbca3pWpdq z>J?@7|HS{G{4Vn0@t|&hC?U3ZVCtXBTANG!OO*_?^VQFJ{@hSb3tw}%pu7a8-cWp6 z@%_2x=<*KC$4HDIneu>) z^+}oX%$DB4%9L`g`Sx*nAFx}vfbsKvRq8#ZBE}g%Un=yyuQVg`^JQD=L#2bwfxpC5DsQH zp#8eKhmV>>E`xZT9Af1mA}Y6AJ|>JE6Gpst!lzMaJ9*Vj{jCU*>g{NGn! zy+m&RI{!Z#r~}>+`-kPjw~;y)?Hu_&Kc5<_)5ssf`eD9>x{%zPyc*3mIC*=cv6@5f z4&(WU`!-RplG{MM-@c}*(UEV@Gr#C9^?)#r7sB_u)MDWX7BCm?AMblhRXXwX5o|5g z#~<%&rnVKv^Jzx!=IS7{^CGl|>XdJx4kP3ByoEZ7jNiv=p^hW-`hus=L9?fw9BfAK z7HYN5e0%WzK=5b#TB=dP5#oGZ(5tn2fLsLkGY<4_qq28-dfcD!z1pcCqv`$i9n{Z- z3z&Bhj7RkDpiaX$%=d%m&wIV2Mw9vTo=19hRBw^LgLp)*PU@&GxId4;yLxq2=aWl? z-&I4qitRh`c)O_K!Vzp4wBI)M>Z1NmaqK^IS8oXiJIg`&3aimwHM-&QZ-QH}9%>!5 zXb&cO^-!CW@%-tjCQjjkg+|{TW#N6te;hd^;hoQTZPPmUx5$i zM;|qb;u`eV8}#;9oA@2RC|C$>ihskM9J z@nHL9kUEAO0{sbpuR&^*u;+R0L28aLydR?mjGy)%q_*!RmZ$o1usR$~>mRI6Cgbr9 zRzu0Sy}{~MVNd-uSUp9?{2QV^BV&CvRDG>C-yU3V;dpfQW3{1h2s^u!_xCZ32||19$1|JQ%Lzf^D9(pTs^TCL{)GC!yF z`C9cy)BOE$>PKXpzdug>63u>?0RMNNaq1k3gL3eWNZXm%{zWb$z~3 z*OKpn|LgsYx`jO2;_+|Q-QKq3;coq_v#4Y2)6nj^jG&yR>#@w*Jp})RoIii)6|D{ z`%Y86`-%K>VtJdcs$|~2U)N{4S`97!&-YqSSKHe>#CL`oE*!*~_JZ%JwVtIGP`n)6 z&mQ9YgBsjltY5Up$FvSm{RX`BW8WaPz0KAA=cxWRzt=igU4^FeW1hN07@jYO?S0}u zPyLPJcz%Saw}iu3U#P#v_=l*~2J-E}_bG~D{9@maYENM(kKo9C3)J@S@pu?3sR`fz z-nT@(B#il$?7u>FzR%-9Y%iR@$^I+V*1|>Nc#iq6QYQ*y`U-v5s&oD)zFu92aq5po zsau4DoheYCIeeqkXo_S1bd#Dy@j1|6b@*;lGtpFEY*w$>T;jj^#reU{?_>U3)qgN9 z-al}_f4iy-;`{Hp9_~~-3d8$KM0;?jI!>70XUn41v*dlyzV+)Ht=9ek^Cuauj~{|p z2;=z1G5sfm*T#esxpKxA0+bUQr}#4 z9QkwjKZ^V2tEuGSa6QQAyI*yGgvZa@^N;%;QU?eJv3p{?=7{>8@HGe4zem++!k+&3 zan)}qF8@|1cpkRjaWzmlh>f|*?;jMaXUU(z_(Ao4C)B1ND>RwMKI+T69#>;crzm-pRJdr}z9Rnq=dM~x8q&A98Nd22OC za(mjVzFI@!2-Y9UPpdRvZ8-U(=Zw9ZR$d!p^T4zU+75Cm{Qo1VMK#*8coIE)&M~ zPiDW$TIIP+x)DbpEkf|-~KhU5yIFWO72%nTZVC}ziVr&$yk5a z);5u`{(eK-Nyhs74K0z}XD;vW)X{RuuYQg`ho=0fqp>eu=1+-#9Zj)0xnEtaKAJs+ z{!fX2J>7nrW|m`Evh~`!&~8wCEoxMhmSnnfgas4Kg1eUemvY)(|cFKgs=CX!C_( zeh|dxrM1+K2xEV!WB=CL12XRq<@Rr*)f@eC`)~Abrwv0pas3^%QNq~1+|j>-HVNaR zKlF9~ceJf!-X99;-%-0n=GR02{++b<#_;tAG2S2gw0~!98kzTpKJEXmc1alfKOgq* zqLuqf><`^P>88oTp8iiats=&$ztdf-OXmHREB$(Curc^ye`QDio?0Zud4I*Xe=n`> z*JAx*Jh8Z6A8j?-iT$I#T09y1M}4(l$k^ZNtCf`f5?a zp8iW;EeGRtzQOY2`107EKi$8-_N}ldzu(g)3VZ60_qFN55lq?w-}6X&Uz`6w@j==# zv>4wFPaC9NusJequy&8!8peOOrVY^?-(Y@l$E1C%S!gF89}7<#uKAI_0&h(lp*5iN z^iquB1TBz^&yP*iyvOsn=XyC&n=Bl`uAhPXp=lGfP+`o^TWQ~G zjtOFYPDf>azhJWFOP0Y3SR=nJ;#0ILB>xCjzzxVNM0~2&jJz4<&p$|; zsfuDf8lCO*SG|iviMFM#90)17MiXuC=6i!__dKeOr+B z4!Jpa*y13q2l)+{zqD@K9IZdOF?iVGIoc3%Hnb1dZJVo&ARh(~TRc}AN4_EUFIYQE z-rAS%Pq0@1JHEd#9tZUse4$zUKsba|{D&WZur|u(is>O*hA=)~HEGd&t^fDpcvKnoTw66a%S*g`D|&ID}RIlc!&zEt`tl zyL640mnB-KY3P5zaKBHBB5xDvmulhDG0wmDG;Hxw%{c?t&)qP6ndUF-$&VG<5n)gL z60V&RhVSFT{HA8<;ac~ZVtwLz+95qcn_+XGkkwi++Br(%?{{6Jg_6gE`-H5~R+3+J z^Y-CdZ6o>V5I#R>t+t)){SluZv{s8JV|#I(mWrnL1+UYt3K!A)F(b8qgbSRVV17!? z0Qjo#EU~}To?fr{lX-pJGkv`_h>YjMdTkha2Am&0Y`yjcc|SZ~=_Ij3ogO}G0+HLZ6D8EA?ebpau|Dx((ycRhItU&sP!bQ$Q zBEC^;j&X+XGv25TBjfvwH*2|=o;8R6tJ?CPwBuxaKf!kG78&o4?9j%{=GzZuJU$|Q zhgK>aA>Q9JI(?^hkIdf>Fgks=#sb9tvOgB`^fB51VNd-UtJMy~IG=AaF+El*CGY%# zJ5C!EB(^8Y&y>h`?Q>x)Z`0BfwDC62OHb0~V|vjZ+Z>dtX>-K-#rsKzrl)CDgoB;y zAU~%>rfIb(j^}H-)_~$i;ruO0OV^rH9LJ+FwD#oTP+sbk&(OM()k@*?dBnYRb_rtjDO77k(aVSI-zKBT#R#Pr{ScY7b!t_p{+Z}{@; zh&EvXZm$>gAJ?TH)uM%QzV4(&$24Ujk9&^)xE3tzX-^&3b_;us_qeu)jL&-?*V4&& zKlZqmj~3-IKmE8?D(snGaZ-~P;r{UZsYlX();f^!eY>Z%F=YIIe$jG-J^Ob?s~#$r zr}BPAYlEiom$O<=v}nJcOh2p5CiDN}LVAg|jC?T$%ICZit&EKC*FCTKFUI}j@5e1o zKd-GO^Y`OENWY+sS%Pugzl+*>GVjk#iM*smlX-h;O5|nj5_!f_7*AgJyHhM@TO)72RlFN z>0m2o-_-oj;{T`{c~hH->8XCbtt}#-9L($E+u8~=U2kq{n<(CQ8nk!k-_drGvA(&Z zC6FtG@#XJm8RUt}czf@TRzRK!egZy^W}n0Mo}}H?Zjq0zhxZN6Dbu}+Zx?xA3&-^0|C+Ytffgwo3{M{N_SOTf%L@EIZ;JE(p*Bny-aiX= zu!mX>#@Uz9zU3|x4rY%Q@%rN#kSWc;gj6UO@gGw>jrzs_L#PPFq9v=>@1FFk=gsTRLq z=%w!?pMd{2E6_{NC5L?o?4zlSvw(vKmp z5$BV)o+^y*PxcA)(F0fU^g)b&fAC6BIX#}tzdsn9@v44`Tp!BEq70`#WHqM8@9kF5 zSCdym|8vEBmtK1fZvV2leo6Z0WWK(d0kS@ceEbUBZ_jY+mu%jgq3XBD9pT@K$uRVW zYjJxs-h}yU85Q+0{H?#Y)M^^gjCxkuq zYYqKZn@iGa=%tvR+Q&8ZGU0hnyx!K-|Dibcr)uerNO8PQ>`&Fw%agG`Ra@7{8E`+V z1$;%m3i$vSCj062$)`m84gGD;@=(9DU~lMc$=DxzLw}cy{joRnK4k2V)zRV4gkO<_ z?E#MqBy2x=T;zl8sW)Gw$M#rV{e4`YXdhQwURR$c4E=Gqeivrc)3;+>)Hla6>g#80 z4qMegH`j~&5b@I)jdXvTf6r*DN7`JH)?7b_W+zkl`PxE%M6SPu_rF`{J{!dP#QS$1 zM6}XPo7b#rqt_$1g!DOSZS^)bmt}O&d)pkgs+0aHxyFAmDzWNaeLT4h^xwl)b=3oG zK9pzl9hw

Jj8>J>Yr!%zpX-^1sktt&llDKTB>2>3!IHx-*LJ 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 diff --git a/.task8-sweep/ref/RefClumsyDance.dll b/.task8-sweep/ref/RefClumsyDance.dll deleted file mode 100644 index 7e191a18b59d5ae719c3ae067578e10974215d5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55296 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 VEwCGZJqtSY!Yls2-~W9U_+O;32^atX diff --git a/.task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj b/.task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj deleted file mode 100644 index 94e673d..0000000 --- a/.task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - Exe - net10.0-windows - true - disable - enable - - - - - - diff --git a/.task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs b/.task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs deleted file mode 100644 index 65b748a..0000000 --- a/.task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; - -internal static class Program -{ - private static readonly byte[] PngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 }; - - private static int Main(string[] arguments) - { - try - { - if (arguments.Length == 3 && arguments[0] == "--export-fixtures") - { - ExportFixtureReports(arguments[1], arguments[2]); - return 0; - } - if (arguments.Length == 2 && arguments[0] == "--export-end-to-end") - { - ExportEndToEndReports(arguments[1]); - return 0; - } - Require(arguments.Length == 1 && File.Exists(arguments[0]), "A fixture path is required."); - Verify(arguments[0]); - Console.WriteLine("PNG verification host completed."); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine(exception.ToString()); - return 1; - } - } - - private static void ExportFixtureReports(string fixturePath, string outputDirectory) - { - Require(File.Exists(fixturePath), "Fixture path was not found."); - PrintReports(new PathSmoothingComparisonDemo().ExportFixtureReports(fixturePath, outputDirectory)); - } - - private static void ExportEndToEndReports(string outputDirectory) - { - PrintReports(new PathSmoothingComparisonDemo().ExportEndToEndReports(outputDirectory)); - } - - private static void PrintReports(IReadOnlyList scenarios) - { - for (int scenarioIndex = 0; scenarioIndex < scenarios.Count; scenarioIndex++) - { - PathSmoothingComparisonScenarioResult scenario = scenarios[scenarioIndex]; - if (scenario.Comparison == null) - { - Console.WriteLine(scenario.ScenarioId + " coarse=" + scenario.CoarsePathStatus + " " + scenario.Diagnostic); - continue; - } - PrintEntry(scenario.ScenarioId, scenario.Comparison.RawPathBaseline); - for (int entryIndex = 0; entryIndex < scenario.Comparison.Entries.Count; entryIndex++) - PrintEntry(scenario.ScenarioId, scenario.Comparison.Entries[entryIndex]); - Console.WriteLine(scenario.ScenarioId + " report=" + scenario.Report.Status + " " + scenario.Report.Reason); - } - } - - private static void PrintEntry(string scenarioId, PathSmoothingComparisonEntry entry) - { - Console.WriteLine(string.Format( - CultureInfo.InvariantCulture, - "{0} {1} status={2} length={3:F4} peakKappa={4:F4} clearance={5:F4}", - scenarioId, - entry.IsRawPathBaseline ? "RawPath" : entry.Method.ToString(), - entry.Status, - entry.Metrics.PathLengthMeters, - entry.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, - entry.Metrics.MinimumBodyClearanceMeters)); - } - - private static void Verify(string fixturePath) - { - PathSmoothingComparisonRequest request = CreateComparisonRequest(); - PathSmoothingComparisonResult comparison = new PathSmoothingComparisonService().Compare(request); - Require(!comparison.IsCancelled, "Fixture comparison was cancelled."); - Require(request.SmoothingRequest.CoarsePath.Count > 1, "Fixture coarse path is too short."); - - CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; - CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; - var model = new SmoothingFigureModelBuilder().Build( - comparison, - request.SmoothingRequest.Map, - new Pose2D(first.X, first.Y, first.Heading), - new Pose2D(last.X, last.Y, last.Heading), - "straight", - "Straight"); - - VerifyObstacleRectanglesAreMerged(fixturePath, comparison, new Pose2D(first.X, first.Y, first.Heading), new Pose2D(last.X, last.Y, last.Heading)); - VerifySixFigureDefinitionContract(model); - VerifyPointOnlyRendererContract(model); - - var resolver = new SmoothingFontResolver(); - Require(resolver.TryResolve("SimSun", "Times New Roman", out SmoothingFontResolution fonts, out string fontReason), - "Required report fonts are unavailable: " + fontReason); - byte[] png; - using (fonts) - { - Require(fonts.ChineseFamilyName == "SimSun", "Chinese font must resolve to exact SimSun family."); - Require(fonts.LatinFamilyName == "Times New Roman", "Latin font must resolve to exact Times New Roman family."); - Require(resolver.MeasureMixedText(fonts, "粗路径 κ(s) X (m) −π", 9f).Width > 0f, - "Mixed Chinese/Latin sample must have nonempty measured bounds."); - png = new SmoothingPngRenderer().Render(model, fonts); - } - VerifyPng(png); - - string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-png-" + Guid.NewGuid().ToString("N")); - try - { - var exporter = new SmoothingReportExporter(); - SmoothingReportExportResult report = exporter.Export(new SmoothingReportExportRequest - { - Model = model, - OutputDirectory = outputDirectory, - FileStem = "straight-report", - }); - Require(report.Status == SmoothingReportExportStatus.Success, "Report export failed: " + report.Reason); - Require(report.SvgPaths.Count == 6 && report.PngPaths.Count == 6 && File.Exists(report.CsvPath), - "Successful export must publish six SVGs, six PNGs and one CSV."); - VerifyPublishedSixFigureFiles(report, outputDirectory); - Require(!ContainsTemporaryFiles(outputDirectory), "Successful export must not leave temporary files."); - - string unavailableDirectory = Path.Combine(outputDirectory, "missing-font"); - SmoothingReportExportResult unavailable = exporter.Export(new SmoothingReportExportRequest - { - Model = model, - OutputDirectory = unavailableDirectory, - FileStem = "must-not-write", - ChineseFontFamilyName = "Missing report font", - LatinFontFamilyName = "Times New Roman", - }); - Require(unavailable.Status == SmoothingReportExportStatus.FontUnavailable, - "Missing exact font must report FontUnavailable."); - Require(!Directory.Exists(unavailableDirectory), "Missing-font export must remain atomic and create no output directory."); - } - finally - { - if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); - } - } - - private static PathSmoothingComparisonRequest CreateComparisonRequest() - { - PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f), - ResolutionMm = 50f, - AllowExplicitEmptyMap = true, - }); - Require(mapResult.Succeeded && mapResult.Map != null, "PNG verification map must be created."); - var vehicle = new VehicleParameters - { - LengthMeters = 0.20d, - WidthMeters = 0.20d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 100d, - }; - var coarsePath = new List - { - CreatePoint(0.5d, 0.5d, 0d), - CreatePoint(1.0d, 0.5d, 0.5d), - CreatePoint(1.0d, 1.0d, 1.0d), - CreatePoint(1.5d, 1.0d, 1.5d), - }; - var segments = new List - { - new PathSegment(0, TravelDirection.Forward, 0, coarsePath.Count - 1, false, false), - }; - return new PathSmoothingComparisonRequest(new PathSmoothingRequest( - coarsePath, - segments, - mapResult.Map, - vehicle, - new PathSmoothingConfiguration())); - } - - private static void VerifySixFigureDefinitionContract(SmoothingFigureModel model) - { - SmoothingFigureSet figureSet = new SmoothingFigureSetBuilder().Build(model); - var stems = new List(); - for (int index = 0; index < figureSet.Figures.Count; index++) stems.Add(figureSet.Figures[index].FileStem); - string expected = string.Join(",", new[] - { - "01-coarse-path-overview", - "02-all-paths-comparison", - "03-cubic-bspline-overview", - "04-local-cubic-bezier-overview", - "05-piecewise-quintic-overview", - "06-curvature-comparison", - }); - Require(string.Join(",", stems) == expected, "Six-figure report stems must be stable and ordered."); - Require(!figureSet.Figures[1].ShowsMapContext, "All-path comparison must contain only trajectories, axes and legend."); - Require(figureSet.Figures[2].ShowsMapContext && figureSet.Figures[2].Series[0].Opacity < 1d, - "Individual smoother figures must retain a faded raw-path map reference."); - Require(figureSet.Figures[0].WorldScalePointsPerMeter > 0d && - Math.Abs((figureSet.Figures[0].WorldXMaxMeters - figureSet.Figures[0].WorldXMinMeters) / - (figureSet.Figures[0].WorldYMaxMeters - figureSet.Figures[0].WorldYMinMeters) - - figureSet.Figures[0].PlotWidthPoints / figureSet.Figures[0].PlotHeightPoints) < 0.000001d, - "Overhead figures must preserve equal X/Y scale."); - Require(figureSet.Figures[0].LegendYPoints - - (figureSet.Figures[0].PlotYPoints + figureSet.Figures[0].PlotHeightPoints + 31d) >= 8d, - "Legend must leave vertical clearance below the X-axis unit label."); - } - - private static void VerifyObstacleRectanglesAreMerged(string fixturePath, PathSmoothingComparisonResult comparison, Pose2D start, Pose2D goal) - { - IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); - IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); - int rectangleIndex = -1; - for (int index = 0; index < fixtures.Count; index++) - { - if (fixtures[index].Id == "rectangle-detour") { rectangleIndex = index; break; } - } - Require(rectangleIndex >= 0, "Fixture suite must include rectangle-detour for obstacle rendering verification."); - SmoothingFigureModel model = new SmoothingFigureModelBuilder().Build( - comparison, requests[rectangleIndex].SmoothingRequest.Map, start, goal, "obstacle", "obstacle"); - Require(model.Obstacles.Count > 0, "Rectangle-detour fixture must produce report obstacles."); - for (int firstIndex = 0; firstIndex < model.Obstacles.Count; firstIndex++) - { - SmoothingFigureObstacle first = model.Obstacles[firstIndex]; - for (int secondIndex = firstIndex + 1; secondIndex < model.Obstacles.Count; secondIndex++) - { - SmoothingFigureObstacle second = model.Obstacles[secondIndex]; - bool matchingColumn = Math.Abs(first.X - second.X) < 0.0000001d && Math.Abs(first.Width - second.Width) < 0.0000001d; - bool verticallyAdjacent = Math.Abs((first.Y + first.Height) - second.Y) < 0.0000001d || - Math.Abs((second.Y + second.Height) - first.Y) < 0.0000001d; - Require(!(matchingColumn && verticallyAdjacent), "Adjacent occupied rows must merge into a single obstacle rectangle."); - } - } - } - - private static void VerifyPointOnlyRendererContract(SmoothingFigureModel model) - { - SmoothingFigureDefinition figure = new SmoothingFigureSetBuilder().Build(model).Figures[1]; - string svg = new SmoothingSvgRenderer().Render(figure); - Require(svg.Contains("class=\"trajectory-point\""), "Trajectory samples must render as discrete SVG point markers."); - Require(!svg.Contains("stroke-dasharray") && !svg.Contains("-path\""), "Trajectory SVG output must not use dashed or joined path strokes."); - Require(svg.Contains("X (m)") && svg.Contains("Y (m)"), "Overhead SVG must label metre coordinate axes."); - } - - private static void VerifyPublishedSixFigureFiles(SmoothingReportExportResult report, string outputDirectory) - { - var expectedStems = new[] - { - "01-coarse-path-overview", - "02-all-paths-comparison", - "03-cubic-bspline-overview", - "04-local-cubic-bezier-overview", - "05-piecewise-quintic-overview", - "06-curvature-comparison", - }; - VerifyPublishedPaths(report.SvgPaths, expectedStems, ".svg", outputDirectory); - VerifyPublishedPaths(report.PngPaths, expectedStems, ".png", outputDirectory); - Require(!File.Exists(Path.Combine(outputDirectory, "comparison.svg")) && !File.Exists(Path.Combine(outputDirectory, "comparison.png")), - "Six-figure export must not leave legacy composite comparison images."); - } - - private static void VerifyPublishedPaths(IReadOnlyList paths, string[] expectedStems, string extension, string outputDirectory) - { - var actual = new List(); - for (int index = 0; index < paths.Count; index++) actual.Add(paths[index]); - Require(actual.Count == expectedStems.Length, "Six-figure export must publish six " + extension + " files."); - for (int index = 0; index < expectedStems.Length; index++) - { - string expected = Path.Combine(outputDirectory, expectedStems[index] + extension); - Require(actual[index] == expected && File.Exists(actual[index]), "Published " + extension + " path must match the stable figure stem."); - } - } - - private static CoarsePathPoint CreatePoint(double x, double y, double arcLength) - { - return new CoarsePathPoint( - x, y, 0d, 0d, arcLength, TravelDirection.Forward, 0d, 1d, false, CoarsePathPointSource.Start); - } - - private static bool ContainsTemporaryFiles(string directory) - { - foreach (string ignored in Directory.EnumerateFiles(directory, "*.tmp")) return true; - return false; - } - - private static void VerifyPng(byte[] png) - { - Require(png != null && png.Length > PngSignature.Length, "PNG output is empty."); - byte[] data = png ?? throw new InvalidOperationException("PNG output is empty."); - for (int index = 0; index < PngSignature.Length; index++) - Require(data[index] == PngSignature[index], "PNG signature is invalid."); - - bool sawHeader = false; - bool sawPhysicalResolution = false; - int offset = PngSignature.Length; - while (offset < data.Length) - { - Require(offset + 12 <= data.Length, "PNG chunk header is truncated."); - int length = checked((int)ReadUInt32BigEndian(data, offset)); - int dataStart = offset + 8; - int crcStart = checked(dataStart + length); - Require(crcStart + 4 <= data.Length, "PNG chunk data is truncated."); - uint expectedCrc = ReadUInt32BigEndian(data, crcStart); - uint actualCrc = ComputeCrc32(data, offset + 4, length + 4); - Require(expectedCrc == actualCrc, "PNG chunk CRC is invalid."); - - string type = System.Text.Encoding.ASCII.GetString(data, offset + 4, 4); - if (type == "IHDR") - { - Require(length == 13, "IHDR length must be 13."); - Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.WidthPixels, "PNG width must be 4296."); - Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.HeightPixels, "PNG height must be 3120."); - sawHeader = true; - } - else if (type == "pHYs") - { - Require(length == 9, "pHYs length must be 9."); - Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.PixelsPerMeter, - "PNG horizontal density must be 23622 pixels/meter."); - Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.PixelsPerMeter, - "PNG vertical density must be 23622 pixels/meter."); - Require(data[dataStart + 8] == 1, "PNG pHYs unit must be meter."); - sawPhysicalResolution = true; - } - offset = crcStart + 4; - } - Require(sawHeader && sawPhysicalResolution, "PNG must contain IHDR and pHYs chunks."); - } - - private static uint ReadUInt32BigEndian(byte[] data, int offset) - { - return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | - ((uint)data[offset + 2] << 8) | data[offset + 3]; - } - - private static uint ComputeCrc32(byte[] data, int offset, int length) - { - uint crc = 0xffffffffu; - for (int index = 0; index < length; index++) - { - crc ^= data[offset + index]; - for (int bit = 0; bit < 8; bit++) - crc = (crc & 1u) == 0u ? crc >> 1 : (crc >> 1) ^ 0xedb88320u; - } - return crc ^ 0xffffffffu; - } - - private static void Require(bool condition, string message) - { - if (!condition) throw new InvalidOperationException(message); - } -} diff --git a/.task8-sweep/tests/generate_path_smoothing_fixtures.ps1 b/.task8-sweep/tests/generate_path_smoothing_fixtures.ps1 deleted file mode 100644 index 9f8223a..0000000 --- a/.task8-sweep/tests/generate_path_smoothing_fixtures.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -param( - [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), - [string]$OutputPath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), - [switch]$Overwrite -) - -$ErrorActionPreference = 'Stop' -$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -$type = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.SmoothingFixtureGenerator', $true) -$method = $type.GetMethod('Generate', [Type[]]@([string], [bool])) -if ($null -eq $method) { throw 'SmoothingFixtureGenerator.Generate(string, bool) is missing.' } -$method.Invoke($null, @([IO.Path]::GetFullPath($OutputPath), [bool]$Overwrite)) -Write-Output "Path smoothing fixtures generated: $OutputPath" diff --git a/.task8-sweep/tests/run_path_smoothing_comparison.ps1 b/.task8-sweep/tests/run_path_smoothing_comparison.ps1 deleted file mode 100644 index 2308ccd..0000000 --- a/.task8-sweep/tests/run_path_smoothing_comparison.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -param( - [switch]$FixtureOnly, - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), - [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports') -) - -$ErrorActionPreference = 'Stop' - -$clumsyPilotRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path -$allowedRoot = [IO.Path]::GetFullPath((Join-Path $clumsyPilotRoot 'obj\path_smoothing_reports')) -$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) -$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar -if ($resolvedOutputDirectory -ne $allowedRoot -and -not $resolvedOutputDirectory.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { - throw "Report output must stay below $allowedRoot" -} - -$projectPath = Join-Path $clumsyPilotRoot 'ClumsyPilot.csproj' -$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' -$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path - -& dotnet build $projectPath --no-restore -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -& dotnet run --project $hostProject --no-restore -- --export-fixtures $resolvedFixturePath $resolvedOutputDirectory -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -if (-not $FixtureOnly) { - & dotnet run --project $hostProject --no-restore -- --export-end-to-end $resolvedOutputDirectory - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -} - -Write-Output "Path smoothing reports written below $resolvedOutputDirectory" diff --git a/.task8-sweep/tests/verify_coarse_path_collision.ps1 b/.task8-sweep/tests/verify_coarse_path_collision.ps1 deleted file mode 100644 index b7cd515..0000000 --- a/.task8-sweep/tests/verify_coarse_path_collision.ps1 +++ /dev/null @@ -1,130 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function New-TestMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) - $rectangleType = $assembly.GetType($mapping + 'AxisAlignedRectangleObstacle', $true) - $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) - $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) - $obstacle = [Activator]::CreateInstance($rectangleType, @([single]1500, [single]1550, [single]1200, [single]1800)) - $obstacles = [Array]::CreateInstance($obstacleType, 1) - $obstacles.SetValue($obstacle, 0) - $source = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) - $sources = [Array]::CreateInstance($sourceType, 1) - $sources.SetValue($source, 0) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.ObstacleSources = $sources - $request.AllowExplicitEmptyMap = $false - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Test map must be created.' - Assert-True $result.Map.PlanningReady 'Test map must be ready.' - return $result.Map -} - -function New-DiagonalCellMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) - $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) - $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) - $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) - $obstacle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0)) - $obstacles = [Array]::CreateInstance($obstacleType, 1) - $obstacles.SetValue($obstacle, 0) - $source = [Activator]::CreateInstance($manualType, @('diagonal-cell', [long]1, $true, $obstacles)) - $sources = [Array]::CreateInstance($sourceType, 1) - $sources.SetValue($source, 0) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.ObstacleSources = $sources - $request.AllowExplicitEmptyMap = $false - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Diagonal cell map must be created.' - Assert-True $result.Map.PlanningReady 'Diagonal cell map must be ready.' - Assert-True $result.Map.IsOccupied(30, 30) 'Diagonal cell obstacle must occupy its 50mm cell.' - return $result.Map -} - -$map = New-TestMap -$vehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters -$vehicle.LengthMeters = 0.20 -$vehicle.WidthMeters = 0.20 -$vehicle.SafetyMarginMeters = 0.0 -$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker - -$diagonalCellMap = New-DiagonalCellMap -$diagonalCellVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters -$diagonalCellVehicle.LengthMeters = 0.10 -$diagonalCellVehicle.WidthMeters = 0.10 -$diagonalCellVehicle.SafetyMarginMeters = 0.0 -$diagonalCellPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.299, 1.299, 0.0) -$diagonalCellClearance = 0.0 -$diagonalCellSafe = $checker.IsPoseCollisionFree($diagonalCellPose, $diagonalCellMap, $diagonalCellVehicle, 0.20, [ref]$diagonalCellClearance) -Assert-False $diagonalCellSafe 'Expanded diagonal footprint must not bypass an occupied 50mm cell.' - -$edgePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.35, 1.50, 0.0) -$edgeClearance = 0.0 -$edgeSafe = $checker.IsPoseCollisionFree($edgePose, $map, $vehicle, 0.0, [ref]$edgeClearance) -Assert-False $edgeSafe 'Touching an occupied cell must be a collision.' - -$farPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.50, 0.50, 0.0) -$farClearance = 0.0 -$farSafe = $checker.IsPoseCollisionFree($farPose, $map, $vehicle, 0.0, [ref]$farClearance) -Assert-True $farSafe 'Strict distance field clearance must allow the distant pose.' -Assert-True ($farClearance -gt 0.0) 'Distant pose must report positive body clearance.' - -$from = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.20, 1.50, 0.0) -$to = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.80, 1.50, 0.0) -$sweepClearance = 0.0 -$sweepSafe = $checker.IsSweptMotionCollisionFree($from, $to, $map, $vehicle, 0.50, [ref]$sweepClearance) -Assert-False $sweepSafe 'A swept vehicle must not pass through an occupied cell.' - -$gridCenterPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.475, 1.475, 0.0) -$gridCenterClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($gridCenterPose, $map, $vehicle, 0.0, [ref]$gridCenterClearance) 'A pose at an occupied grid center must collide.' - -$subGridPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.481, 1.463, 0.37) -$subGridClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($subGridPose, $map, $vehicle, 0.0, [ref]$subGridClearance) 'An arbitrary sub-grid heading must collide with the thin obstacle.' - -$diagonalPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.32, 1.50, ([Math]::PI / 4.0)) -$diagonalClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($diagonalPose, $map, $vehicle, 0.0, [ref]$diagonalClearance) 'A 45 degree footprint overlap must collide.' - -$outsidePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.05, 0.05, 0.0) -$outsideClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($outsidePose, $map, $vehicle, 0.0, [ref]$outsideClearance) 'Any footprint corner outside the map must collide.' - -$curveVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters -$curveVehicle.MaximumCurvaturePerMeter = 0.80 -$curveVehicle.MinimumTurningRadiusMeters = 2.0 -$maximumCurvature = 0.0 -$hasMaximumCurvature = [MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.VehicleKinematics]::TryGetMaximumCurvaturePerMeter($curveVehicle, [ref]$maximumCurvature) -Assert-True $hasMaximumCurvature 'Vehicle curvature constraints must be accepted.' -Assert-Near 0.50 $maximumCurvature 'Both curvature constraints must use the conservative minimum.' - -Write-Output 'Coarse path collision checks passed.' diff --git a/.task8-sweep/tests/verify_coarse_path_integration.ps1 b/.task8-sweep/tests/verify_coarse_path_integration.ps1 deleted file mode 100644 index 6051426..0000000 --- a/.task8-sweep/tests/verify_coarse_path_integration.ps1 +++ /dev/null @@ -1,460 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' - -$plannerRoot = Join-Path $PSScriptRoot '..\ParkrobTrajplanner' -$coarsePathReadme = Join-Path $plannerRoot 'CoarsePath\README.md' -$legacyRootReadme = Join-Path $plannerRoot 'README.md' -if (-not (Test-Path -LiteralPath $coarsePathReadme -PathType Leaf)) { - throw 'CoarsePath module README must be located inside the CoarsePath directory.' -} -if (Test-Path -LiteralPath $legacyRootReadme -PathType Leaf) { - throw 'The coarse-path-only README must not remain at the ParkrobTrajplanner root.' -} -$coarsePathReadmeContent = Get-Content -LiteralPath $coarsePathReadme -Raw -if (-not $coarsePathReadmeContent.Contains('## 总预算与取消')) { - throw 'CoarsePath README must document total timeout and cancellation semantics.' -} -if (-not $coarsePathReadmeContent.Contains('PlanningMapBuildStatus')) { - throw 'CoarsePath README must document map-stage termination status handling.' -} - -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods()) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { - $matches = $false - break - } - } - if ($matches) { return $candidate } - } - return $null -} - -function New-EmptyPlanningMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Empty integration map must be created.' - Assert-True $result.Map.PlanningReady 'Explicit empty integration map must be ready.' - return $result.Map -} - -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$plannerType = $assembly.GetType($coarsePath + 'HybridAStarPlanner', $true) -$planner = [Activator]::CreateInstance($plannerType) -$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) -$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) -$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) -$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) -$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) -$plan = Find-Method $plannerType 'Plan' @($requestType, [Threading.CancellationToken]) -Assert-True ($plan -ne $null) 'HybridAStarPlanner must expose Plan(request, cancellationToken).' - -$defaultConfiguration = [Activator]::CreateInstance($configurationType) -Assert-Near 0.15 $defaultConfiguration.GoalPositionToleranceMeters 'Default goal-position tolerance must retain the documented 0.15 m terminal acceptance radius.' - -$request = [Activator]::CreateInstance($requestType) -$request.Map = New-EmptyPlanningMap -$request.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) -$request.Goal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) -$request.Vehicle = [Activator]::CreateInstance($vehicleType) -$request.Vehicle.LengthMeters = 0.20 -$request.Vehicle.WidthMeters = 0.20 -$request.Vehicle.SafetyMarginMeters = 0.0 -$request.Vehicle.MaximumCurvaturePerMeter = 1.0 -$request.Configuration = [Activator]::CreateInstance($configurationType) -$request.Configuration.MaximumExpandedNodes = 10000 -$request.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) -$request.Configuration.GoalPositionToleranceMeters = 0.001 -$request.Configuration.GoalHeadingToleranceRadians = 0.001 -$request.Configuration.AllowReverse = $false -$request.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') -$request.StartVehicleCurvature = [double]0.20 - -$result = $plan.Invoke($planner, @($request, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $result.Status.ToString() 'Planner must publish a validated direct path on an empty map.' -Assert-True ($result.Path.Count -ge 2) 'Validated path must retain its start and goal points.' -Assert-Near 0.0 $result.Path[0].ArcLength 'The first path point must have zero arc length.' -Assert-Near 0.20 $result.Path[0].VehicleCurvature ` - 'The assembled start point must retain the requested physical vehicle curvature.' -Assert-Equal 'Start' $result.Path[0].Source.ToString() 'The first path point must retain the start source.' -Assert-Equal 'GoalTruncation' $result.Path[-1].Source.ToString() 'The terminal truncated point must retain its source.' -Assert-True ($result.Diagnostics.PeakOpenListCount -ge 1) 'Planner diagnostics must retain the actual Open List peak count.' -Assert-True ($result.Diagnostics.StaleOpenListEntryCount -ge 0) 'Planner diagnostics must retain a non-negative stale Open List count.' -Assert-True ($result.Diagnostics.GetType().GetProperty('PathSearchElapsed') -ne $null) 'Planner diagnostics must expose path-search elapsed time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) 'Successful planning must retain non-negative path-search time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) 'Path-search time must not exceed total elapsed time.' - -$nodeLimitedRequest = [Activator]::CreateInstance($requestType) -$nodeLimitedRequest.Map = $request.Map -$nodeLimitedRequest.Start = $request.Start -$nodeLimitedRequest.Goal = $request.Goal -$nodeLimitedRequest.Vehicle = $request.Vehicle -$nodeLimitedRequest.Configuration = [Activator]::CreateInstance($configurationType) -$nodeLimitedRequest.Configuration.MaximumExpandedNodes = 0 -$nodeLimitedRequest.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2) -$nodeLimitedRequest.Configuration.GoalPositionToleranceMeters = 0.001 -$nodeLimitedRequest.Configuration.GoalHeadingToleranceRadians = 0.001 -$nodeLimitedRequest.Configuration.AllowReverse = $false -$nodeLimitedRequest.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') -$nodeLimitedResult = $plan.Invoke($planner, @($nodeLimitedRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchNodeLimitExceeded' $nodeLimitedResult.Status.ToString() 'A zero node limit must fail after planner preflight.' -Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) 'Search-stage node-limit failure must retain path-search time.' -Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -le $nodeLimitedResult.Diagnostics.Elapsed) 'Failed path-search time must not exceed total elapsed time.' -Assert-True (-not [string]::IsNullOrWhiteSpace($nodeLimitedResult.Diagnostics.TerminationReason)) 'Planner diagnostics must retain a node-limit reason.' - -for ($index = 1; $index -lt $result.Path.Count; $index++) { - $previous = $result.Path[$index - 1] - $current = $result.Path[$index] - Assert-True ($current.ArcLength -ge $previous.ArcLength) 'Path arc length must be non-decreasing.' - Assert-True ([Math]::Abs($current.UnwrappedHeading - $previous.UnwrappedHeading) -le ([Math]::PI + 0.000001)) 'Unwrapped heading must remain continuous between adjacent points.' - $samePose = ([Math]::Abs($current.X - $previous.X) -le 0.000001) -and - ([Math]::Abs($current.Y - $previous.Y) -le 0.000001) -and - ([Math]::Abs($current.Heading - $previous.Heading) -le 0.000001) -and - ([Math]::Abs($current.ArcLength - $previous.ArcLength) -le 0.000001) - if ($samePose) { - Assert-True (($current.Direction -ne $previous.Direction) -and $current.IsGearSwitchPoint) 'Adjacent duplicate points are permitted only as a marked gear-switch pair.' - } -} - -Assert-True ($result.Segments.Count -ge 1) 'Validated path must include inclusive direction segments.' -Assert-Equal 0 $result.Segments[0].StartIndex 'The first segment must include the first path point.' -Assert-Equal ($result.Path.Count - 1) $result.Segments[-1].EndIndex 'The final segment must include the final path point.' -for ($index = 0; $index -lt $result.Segments.Count; $index++) { - $segment = $result.Segments[$index] - Assert-Equal $index $segment.SegmentIndex 'Segment indexes must be contiguous.' - Assert-True ($segment.StartIndex -le $segment.EndIndex) 'Each segment must use inclusive ordered indexes.' - if ($index -gt 0) { - $previousSegment = $result.Segments[$index - 1] - Assert-Equal ($previousSegment.EndIndex + 1) $segment.StartIndex 'Segments must cover each path point exactly once.' - } -} - -Write-Output 'Coarse path integration checks passed.' - -# Facade checks intentionally use only ASCII text so the script can also run in legacy PowerShell hosts. -$facade = $coarsePath + 'Facade.' -$serviceType = $assembly.GetType($facade + 'CoarsePathPlanningService', $true) -$jobType = $assembly.GetType($facade + 'CoarsePathPlanningJob', $true) -$jobResultType = $assembly.GetType($facade + 'CoarsePathPlanningJobResult', $true) -$debugOptionsType = $assembly.GetType($facade + 'PlanningDebugOptions', $true) -$debugSinkType = $assembly.GetType($facade + 'IPlanningDebugSink', $true) -$mapRequestType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true) -$boundsType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true) -$servicePlan = Find-Method $serviceType 'Plan' @($jobType, [Threading.CancellationToken]) -Assert-True ($servicePlan -ne $null) 'CoarsePathPlanningService must expose Plan(job, cancellationToken).' -Assert-True ($jobResultType.GetProperty('MapResult') -ne $null) 'Facade result must retain the map result.' -Assert-True ($jobResultType.GetProperty('PlanningResult') -ne $null) 'Facade result must retain the planning result.' -Assert-True ($jobResultType.GetProperty('DebugDiagnostics') -ne $null) 'Facade result must retain debug diagnostics.' - -function New-FacadeJob { - $job = [Activator]::CreateInstance($jobType) - $mapRequest = [Activator]::CreateInstance($mapRequestType) - $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $mapRequest.ResolutionMm = [single]50 - $mapRequest.AllowExplicitEmptyMap = $true - $job.MapRequest = $mapRequest - $job.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) - $job.Goal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) - $job.Vehicle = [Activator]::CreateInstance($vehicleType) - $job.Vehicle.LengthMeters = 0.20 - $job.Vehicle.WidthMeters = 0.20 - $job.Vehicle.SafetyMarginMeters = 0.0 - $job.Vehicle.MaximumCurvaturePerMeter = 1.0 - $job.Configuration = [Activator]::CreateInstance($configurationType) - $job.Configuration.MaximumExpandedNodes = 10000 - $job.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) - $job.Configuration.GoalPositionToleranceMeters = 0.001 - $job.Configuration.GoalHeadingToleranceRadians = 0.001 - $job.Configuration.AllowReverse = $false - $job.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') - return $job -} - -$service = [Activator]::CreateInstance($serviceType) -$firstFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), [Threading.CancellationToken]::None)) -Assert-True $firstFacadeResult.MapResult.Succeeded 'Facade must build the requested map first.' -Assert-Equal 'Success' $firstFacadeResult.PlanningResult.Status.ToString() 'Facade must pass a successful map to the planner.' -Assert-Equal 0 $firstFacadeResult.DebugDiagnostics.Count 'Default debug sink must not add diagnostics.' - -$secondFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), [Threading.CancellationToken]::None)) -Assert-True $secondFacadeResult.MapResult.Succeeded 'Repeated facade request must retain a map result.' -Assert-Equal 'Input' $secondFacadeResult.MapResult.CacheHit.ToString() 'One facade service must retain its map factory cache.' -Assert-Equal $firstFacadeResult.MapResult.Map.InputFingerprint $secondFacadeResult.MapResult.Map.InputFingerprint 'Map fingerprint must remain stable for equal input.' -Assert-Equal $firstFacadeResult.PlanningResult.Status $secondFacadeResult.PlanningResult.Status 'Map cache reuse must not change planning status.' -Assert-Equal $firstFacadeResult.PlanningResult.Path.Count $secondFacadeResult.PlanningResult.Path.Count 'Map cache reuse must not change path point count.' - -$invalidJob = [Activator]::CreateInstance($jobType) -$invalidJob.MapRequest = [Activator]::CreateInstance($mapRequestType) -$mapFailureResult = $servicePlan.Invoke($service, @($invalidJob, [Threading.CancellationToken]::None)) -Assert-False $mapFailureResult.MapResult.Succeeded 'Invalid map input must be returned as a map failure.' -Assert-Equal 'InvalidMap' $mapFailureResult.PlanningResult.Status.ToString() 'Map failure must return an empty non-search planning result.' -Assert-Equal 0 $mapFailureResult.PlanningResult.Diagnostics.ExpandedNodeCount 'Map failure must not start the search.' -Assert-Equal 0 $mapFailureResult.PlanningResult.Path.Count 'Map failure must not publish a path.' -Assert-Equal ([TimeSpan]::Zero) $mapFailureResult.PlanningResult.Diagnostics.PathSearchElapsed 'Map failure must report zero path-search time.' - -$cancelledFacadeSource = New-Object Threading.CancellationTokenSource -$cancelledFacadeSource.Cancel() -$cancelledFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), $cancelledFacadeSource.Token)) -Assert-Equal 'Cancelled' $cancelledFacadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.' -Assert-Equal 'Cancelled' $cancelledFacadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.' -Assert-Equal 0 $cancelledFacadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.' -Assert-Equal 0 $cancelledFacadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.' - -$timedOutFacadeJob = New-FacadeJob -$timedOutFacadeJob.Configuration.SearchTimeout = [TimeSpan]::Zero -$timedOutFacadeResult = $servicePlan.Invoke($service, @($timedOutFacadeJob, [Threading.CancellationToken]::None)) -Assert-Equal 'TimedOut' $timedOutFacadeResult.MapResult.Status.ToString() 'Facade total timeout must stop before publishing a map.' -Assert-Equal 'SearchTimeout' $timedOutFacadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage timeout to planning timeout.' -Assert-Equal 0 $timedOutFacadeResult.PlanningResult.Path.Count 'Timed out facade planning must publish no path.' - -$defaultDebugOptions = [Activator]::CreateInstance($debugOptionsType) -Assert-False $defaultDebugOptions.Enabled 'Debug output must be opt-in.' -Assert-True ($defaultDebugOptions.Sink -ne $null) 'Debug options must default to a no-op sink.' - -$debugSinkSource = @' -using System; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -public sealed class FacadeRecordingDebugSink : IPlanningDebugSink -{ - public int PublishCount { get; private set; } - public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) - { - PublishCount++; - } -} - -public sealed class FacadeThrowingDebugSink : IPlanningDebugSink -{ - public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) - { - throw new InvalidOperationException("debug sink failure"); - } -} -'@ -$runtimeDirectory = [Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() -$debugSinkReferences = @( - (Join-Path $runtimeDirectory 'mscorlib.dll'), - (Join-Path $runtimeDirectory 'System.dll'), - (Join-Path $runtimeDirectory 'System.Core.dll'), - $AssemblyPath, - 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\Facades\netstandard.dll' -) -Add-Type -TypeDefinition $debugSinkSource -ReferencedAssemblies $debugSinkReferences -$recordingSinkType = 'FacadeRecordingDebugSink' -as [type] -$throwingSinkType = 'FacadeThrowingDebugSink' -as [type] - -$disabledDebugOptions = [Activator]::CreateInstance($debugOptionsType) -$recordingSink = [Activator]::CreateInstance($recordingSinkType) -$disabledDebugOptions.Sink = $recordingSink -$disabledJob = New-FacadeJob -$disabledJob.DebugOptions = $disabledDebugOptions -$disabledResult = $servicePlan.Invoke($service, @($disabledJob, [Threading.CancellationToken]::None)) -Assert-Equal 0 $recordingSink.PublishCount 'Disabled debug output must not publish to the sink.' -Assert-Equal $firstFacadeResult.PlanningResult.Status $disabledResult.PlanningResult.Status 'Disabled debug output must not change planning status.' - -$throwingDebugOptions = [Activator]::CreateInstance($debugOptionsType) -$throwingDebugOptions.Enabled = $true -$throwingDebugOptions.Sink = [Activator]::CreateInstance($throwingSinkType) -$throwingJob = New-FacadeJob -$throwingJob.DebugOptions = $throwingDebugOptions -$throwingResult = $servicePlan.Invoke($service, @($throwingJob, [Threading.CancellationToken]::None)) -Assert-Equal $firstFacadeResult.MapResult.Map.InputFingerprint $throwingResult.MapResult.Map.InputFingerprint 'Debug sink errors must not change the map fingerprint.' -Assert-Equal $firstFacadeResult.PlanningResult.Status $throwingResult.PlanningResult.Status 'Debug sink errors must not change planning status.' -Assert-Equal $firstFacadeResult.PlanningResult.Path.Count $throwingResult.PlanningResult.Path.Count 'Debug sink errors must not change the path.' -Assert-Equal 1 $throwingResult.DebugDiagnostics.Count 'Debug sink errors must be retained as diagnostics.' - -Write-Output 'Coarse path facade checks passed.' - -# P1 UI scenario factory contract checks. These run against the same built assembly -# and intentionally fail until the factory is introduced. -$testNamespace = $coarsePath + 'Test.' -$scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false) -$scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false) -Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.' -Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.' - -$factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType) -$factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @( - $scenarioEnumType, [double], [double], [double]) -$factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @( - [double], [double], [double], [double], [double], [double]) -Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).' -Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).' -Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.' - -$scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath') -foreach ($scenarioName in $scenarioNames) { - $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) - $jobA = $factoryCreate.Invoke($null, @($scenario)) - $jobB = $factoryCreate.Invoke($null, @($scenario)) - Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job." - Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call." -} - -foreach ($scenarioName in $scenarioNames) { - $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) - $liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0)) - Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X." - Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y." - Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading." -} - -$rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour') -$liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0)) -Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.' -Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.' -Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.' -Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.' -Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.' -Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.' -Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.' -Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.' -$rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld() -$rectangleObstacle = $rectangleProjection.Obstacles[0] -Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.' -Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.' - -$multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg') -$baselineMulti = $factoryCreate.Invoke($null, @($multiScenario)) -$liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0)) -Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.' -Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.' -Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.' -$baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles -$liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles -Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.' -Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.' - -$noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath') -$liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0)) -Assert-Near 9.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.' -Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.' -Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.' - -$invalidLivePoseRejected = $false -try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) } -catch [ArgumentOutOfRangeException] { - $invalidLivePoseRejected = $true -} -catch [Reflection.TargetInvocationException] { - $invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] -} -Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.' - -$manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0)) -Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.' -Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.' -Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.' -Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.' -Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.' -Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.' - -$slowFeasibleJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 0.0, 1500.0, 2500.0, 90.0)) -$slowFeasibleJob.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(30) -$slowFeasibleJob.Configuration.MaximumExpandedNodes = 1000000 -$slowFeasibleService = [Activator]::CreateInstance($serviceType) -$slowFeasibleResult = $servicePlan.Invoke($slowFeasibleService, @($slowFeasibleJob, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $slowFeasibleResult.PlanningResult.Status.ToString() 'The previously five-second-limited feasible pose must succeed with a caller-selected longer budget.' -Assert-True ($slowFeasibleResult.PlanningResult.Diagnostics.PathSearchElapsed -le $slowFeasibleResult.PlanningResult.Diagnostics.Elapsed) 'Slow feasible path-search time must remain within total elapsed time.' - -$manualObstacleKindType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacleKind', $false) -$manualObstacleType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacle', $false) -Assert-True ($manualObstacleKindType -ne $null) 'Manual obstacle kind enum must exist.' -Assert-True ($manualObstacleType -ne $null) 'Manual obstacle value type must exist.' - -$manualCircle = Find-Method $manualObstacleType 'Circle' @([double], [double], [double]) -$manualRectangle = Find-Method $manualObstacleType 'AxisAlignedRectangle' @([double], [double], [double], [double]) -$manualObstacleFactory = $scenarioFactoryType.GetMethods() | Where-Object { - $_.Name -eq 'CreateManualObstacleDemo' -and $_.GetParameters().Length -eq 8 -} | Select-Object -First 1 -Assert-True ($manualCircle -ne $null) 'Manual obstacle type must create circles from center and radius.' -Assert-True ($manualRectangle -ne $null) 'Manual obstacle type must create rectangles from center and X/Y dimensions.' -Assert-True ($manualObstacleFactory -ne $null) 'Scenario factory must expose CreateManualObstacleDemo with six poses, obstacles and version.' - -$manualObstacles = [Array]::CreateInstance($manualObstacleType, 2) -$manualObstacles.SetValue($manualCircle.Invoke($null, @([double]6500, [double]2000, [double]200)), 0) -$manualObstacles.SetValue($manualRectangle.Invoke($null, @([double]-2000, [double]500, [double]600, [double]400)), 1) -$manualObstacleJob = $manualObstacleFactory.Invoke($null, @( - 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $manualObstacles, [long]77)) -Assert-False $manualObstacleJob.MapRequest.AllowExplicitEmptyMap 'Manual obstacles must disable the explicit-empty-map mode.' -Assert-Equal 1 $manualObstacleJob.MapRequest.ObstacleSources.Count 'Manual obstacles must create one unified source.' -Assert-Equal 'manual-user-input' $manualObstacleJob.MapRequest.ObstacleSources[0].SourceId 'Manual source ID must be stable.' -Assert-Equal 77 $manualObstacleJob.MapRequest.ObstacleSources[0].SourceVersion 'Manual source version must be preserved.' -Assert-True ($manualObstacleJob.MapRequest.Bounds.XMin -le -4300.0) 'Manual map must include the rectangle outline and padding.' -Assert-True ($manualObstacleJob.MapRequest.Bounds.XMax -ge 8700.0) 'Manual map must include the circle outline and padding.' - -$emptyManualObstacles = [Array]::CreateInstance($manualObstacleType, 0) -$emptyManualJob = $manualObstacleFactory.Invoke($null, @( - 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $emptyManualObstacles, [long]0)) -Assert-True $emptyManualJob.MapRequest.AllowExplicitEmptyMap 'Zero manual obstacles must retain explicit empty-map mode.' -Assert-Equal 0 $emptyManualJob.MapRequest.ObstacleSources.Count 'Zero manual obstacles must not create a fake source.' - -$invalidGeometryRejected = $false -try { - $null = $manualCircle.Invoke($null, @([double]1000, [double]1000, [double]0)) -} -catch [ArgumentOutOfRangeException] { $invalidGeometryRejected = $true } -catch [Reflection.TargetInvocationException] { - $invalidGeometryRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] -} -Assert-True $invalidGeometryRejected 'Invalid manual geometry must report argument range.' - -$scenarioService = [Activator]::CreateInstance($serviceType) -foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { - $job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName))) - $result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None)) - Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed." -} - -$reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch'))) -$reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None)) -Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.' - -$noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath'))) -$noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None)) -Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.' -Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.' -Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('Open List')) 'No-path diagnostics must retain the exact search exhaustion reason.' -Assert-True (-not [string]::IsNullOrWhiteSpace($noPathResult.PlanningResult.Diagnostics.TerminationReason)) 'No-path diagnostics must retain a reason.' - -$cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) -$cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) -$cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None)) -$cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None)) -Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.' -Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.' - -Write-Output 'Coarse path P1 scenario checks passed.' diff --git a/.task8-sweep/tests/verify_coarse_path_search.ps1 b/.task8-sweep/tests/verify_coarse_path_search.ps1 deleted file mode 100644 index 3566c91..0000000 --- a/.task8-sweep/tests/verify_coarse_path_search.ps1 +++ /dev/null @@ -1,411 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Null($Actual, [string]$Message) { - if ($null -ne $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch { - return - } - - throw $Message -} - -function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods()) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { - $matches = $false - break - } - } - if ($matches) { return $candidate } - } - return $null -} - -function Find-NonPublicStaticMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Static,NonPublic')) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } - } - if ($matches) { return $candidate } - } - return $null -} - -function New-TestMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Test map must be created.' - Assert-True $result.Map.PlanningReady 'Test map must be ready.' - return $result.Map -} - -function New-CornerBlockedMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) - $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) - $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) - $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]150, [single]0, [single]150)) - $obstacles = [Array]::CreateInstance($obstacleType, 2) - $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]75, [single]25, [single]0)), 0) - $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]25, [single]75, [single]0)), 1) - $sources = [Array]::CreateInstance($sourceType, 1) - $sources.SetValue([Activator]::CreateInstance($manualType, @('corner-blocks', [long]1, $true, $obstacles)), 0) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.ObstacleSources = $sources - $request.AllowExplicitEmptyMap = $false - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Corner-blocked map must be created.' - Assert-True $result.Map.PlanningReady 'Corner-blocked map must be ready.' - return $result.Map -} - -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$search = $coarsePath + 'Search.' -$primitiveType = $assembly.GetType($search + 'MotionPrimitive', $true) -$generatorType = $assembly.GetType($search + 'MotionPrimitiveGenerator', $true) -$goalCheckerType = $assembly.GetType($search + 'GoalToleranceChecker', $true) - -Assert-True ($primitiveType.GetProperty('ActualLengthMeters') -ne $null) 'MotionPrimitive must expose actual length.' -Assert-True ($primitiveType.GetProperty('Points') -ne $null) 'MotionPrimitive must expose integration points.' -Assert-True ($primitiveType.GetProperty('IsGoalTruncation') -ne $null) 'MotionPrimitive must expose goal truncation state.' - -$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) -$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) -$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) -$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) -$directionType = $assembly.GetType($coarsePath + 'TravelDirection', $true) -$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) -$generate = Find-Method $generatorType 'Generate' @($poseType, [double], $directionType, $requestType) -Assert-True ($generate -ne $null) 'MotionPrimitiveGenerator must expose Generate(start, curvature, direction, request).' -$isSatisfied = Find-Method $goalCheckerType 'IsSatisfied' @($poseType, $poseType, $configurationType, $directionType, $goalDirectionType) -Assert-True ($isSatisfied -ne $null) 'GoalToleranceChecker must expose the planned goal check.' - -$map = New-TestMap -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = 0.20 -$vehicle.WidthMeters = 0.20 -$vehicle.SafetyMarginMeters = 0.0 -$vehicle.MaximumCurvaturePerMeter = 1.0 -$config = [Activator]::CreateInstance($configurationType) -$config.PrimitiveLengthMeters = 0.50 -$config.IntegrationStepMeters = 0.05 -$config.MaximumCollisionCheckStepMeters = 0.025 -$config.GoalPositionToleranceMeters = 0.001 -$config.GoalHeadingToleranceRadians = 0.001 -$config.CurvatureLevelCount = 5 -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anyDirection = [Enum]::Parse($goalDirectionType, 'Any') -$forwardOnly = [Enum]::Parse($goalDirectionType, 'Forward') -$start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) -$generator = [Activator]::CreateInstance($generatorType) - -$request = [Activator]::CreateInstance($requestType) -$request.Map = $map -$request.Vehicle = $vehicle -$request.Configuration = $config -$request.Goal = [Activator]::CreateInstance($poseType, @(3.0, 1.0, 0.0)) -$request.GoalDirection = $anyDirection -$straight = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) -Assert-True ($straight -ne $null) 'Straight primitive must be generated.' -Assert-Near 0.50 $straight.ActualLengthMeters 'Straight primitive must use the configured cap.' -Assert-Near 1.50 $straight.End.X 'Straight primitive must use analytic integration.' -Assert-Near 1.00 $straight.End.Y 'Straight primitive must not drift laterally.' -Assert-Near 0.00 $straight.End.Heading 'Straight primitive must keep heading.' -Assert-True ($straight.Points.Count -ge 20) 'Point spacing must honor the collision check step.' -$previous = $start -foreach ($point in $straight.Points) { - $distance = [Math]::Sqrt(($point.X - $previous.X) * ($point.X - $previous.X) + ($point.Y - $previous.Y) * ($point.Y - $previous.Y)) - Assert-True ($distance -le 0.025001) 'Point spacing must not exceed the map-safe collision step.' - $previous = $point -} - -$curve = $generate.Invoke($generator, @($start, 1.0, $forward, $request)) -Assert-True ($curve -ne $null) 'Arc primitive must be generated.' -Assert-Near ([Math]::Sin(0.50) + 1.0) $curve.End.X 'Arc X must use analytic integration.' -Assert-Near (1.0 - [Math]::Cos(0.50) + 1.0) $curve.End.Y 'Arc Y must use analytic integration.' -Assert-Near 0.50 $curve.End.Heading 'Arc heading must use signed distance.' - -$reversePrimitive = $generate.Invoke($generator, @($start, 0.0, $reverse, $request)) -Assert-True ($reversePrimitive -ne $null) 'Reverse primitive must be generated.' -Assert-Near 0.50 $reversePrimitive.End.X 'Reverse straight primitive must move behind the vehicle.' - -$request.Goal = [Activator]::CreateInstance($poseType, @(1.30, 1.0, 0.0)) -$truncated = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) -Assert-True ($truncated -ne $null) 'Goal-truncated primitive must be generated.' -Assert-Near 0.30 $truncated.ActualLengthMeters 'A 0.30m goal must truncate at its first internal point.' -Assert-True $truncated.IsGoalTruncation 'Truncated primitive must record its goal source.' -Assert-Near 1.30 $truncated.End.X 'Truncated primitive must end at the goal point.' - -$request.Goal = $start -$zeroLength = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) -Assert-True ($zeroLength -ne $null) 'Start-at-goal must produce a zero-length candidate.' -Assert-Near 0.0 $zeroLength.ActualLengthMeters 'Start-at-goal candidate must have zero length.' -Assert-True $zeroLength.IsGoalTruncation 'Start-at-goal candidate must have goal truncation source.' - -$wrapPose = [Activator]::CreateInstance($poseType, @(1.0, 1.0, (-[Math]::PI + 0.0005))) -$wrapGoal = [Activator]::CreateInstance($poseType, @(1.0, 1.0, ([Math]::PI - 0.0005))) -$config.GoalHeadingToleranceRadians = 0.002 -Assert-True $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $forward, $forwardOnly)) 'Goal heading tolerance must wrap across pi.' -Assert-False $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $reverse, $forwardOnly)) 'Goal direction constraint must reject reverse entry.' - -$levelsMethod = Find-Method $generatorType 'GetCurvatureLevels' @($vehicleType, $configurationType) -Assert-True ($levelsMethod -ne $null) 'MotionPrimitiveGenerator must expose curvature levels.' -$levels = $levelsMethod.Invoke($generator, @($vehicle, $config)) -Assert-True ($levels.Count -eq 5) 'Five configured curvature levels must be generated.' -Assert-Near -1.0 $levels[0] 'First curvature level must be negative maximum.' -Assert-Near 0.0 $levels[2] 'Middle curvature level must be straight.' -Assert-Near 1.0 $levels[4] 'Last curvature level must be positive maximum.' -$adjacent = Find-Method $generatorType 'AreCurvatureLevelsAdjacent' @([int], [int]) -Assert-True ($adjacent -ne $null) 'MotionPrimitiveGenerator must expose curvature adjacency.' -Assert-True $adjacent.Invoke($null, @(2, 3)) 'Neighboring curvature levels must be allowed.' -Assert-False $adjacent.Invoke($null, @(2, 4)) 'Non-neighboring curvature levels must be rejected.' - -$heapGenericType = $assembly.GetType($search + 'BinaryMinHeap`1', $true) -$heapType = $heapGenericType.MakeGenericType([string]) -$heap = [Activator]::CreateInstance($heapType) -$push = Find-Method $heapType 'Push' @([string], [double], [double], [double]) -$pop = Find-Method $heapType 'Pop' @() -Assert-True ($push -ne $null) 'BinaryMinHeap must expose Push(item, f, h, g).' -Assert-True ($pop -ne $null) 'BinaryMinHeap must expose Pop().' -$push.Invoke($heap, @('node-a', 8.0, 2.0, 1.0)) -$push.Invoke($heap, @('node-b', 8.0, 1.0, 1.0)) -$push.Invoke($heap, @('node-c', 8.0, 1.0, 3.0)) -$push.Invoke($heap, @('node-d', 7.0, 9.0, 0.0)) -$push.Invoke($heap, @('node-e', 8.0, 1.0, 3.0)) -Assert-Equal 'node-d' $pop.Invoke($heap, @()) 'Lower F must win.' -Assert-Equal 'node-c' $pop.Invoke($heap, @()) 'Equal F and H must prefer larger G.' -Assert-Equal 'node-e' $pop.Invoke($heap, @()) 'Equal F, H and G must preserve insertion order.' -Assert-Equal 'node-b' $pop.Invoke($heap, @()) 'Equal F must prefer lower H.' -Assert-Equal 'node-a' $pop.Invoke($heap, @()) 'Remaining item must be returned last.' -$firstDeterministicHeap = [Activator]::CreateInstance($heapType) -$secondDeterministicHeap = [Activator]::CreateInstance($heapType) -foreach ($deterministicHeap in @($firstDeterministicHeap, $secondDeterministicHeap)) { - $push.Invoke($deterministicHeap, @('repeat-a', 5.0, 2.0, 1.0)) - $push.Invoke($deterministicHeap, @('repeat-b', 5.0, 1.0, 1.0)) - $push.Invoke($deterministicHeap, @('repeat-c', 5.0, 1.0, 3.0)) -} -$firstDeterministicOrder = @($pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @())) -join ',' -$secondDeterministicOrder = @($pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @())) -join ',' -Assert-Equal $firstDeterministicOrder $secondDeterministicOrder 'Equivalent heap inputs must have deterministic pop order.' - -$calculatorType = $assembly.GetType($search + 'SearchCostCalculator', $true) -$calculator = [Activator]::CreateInstance($calculatorType) -$calculate = Find-Method $calculatorType 'Calculate' @([double], $directionType, [bool], [double], [double], [int], [double], $configurationType) -Assert-True ($calculate -ne $null) 'SearchCostCalculator must expose the planned equivalent-meter calculation.' -$costConfig = [Activator]::CreateInstance($configurationType) -$cost = $calculate.Invoke($calculator, @(2.0, $reverse, $true, 0.5, 1.0, 2, 0.25, $costConfig)) -Assert-Near 4.55 $cost 'Reverse, gear, curvature and clearance costs must be accumulated.' -$negativeWeightConfig = [Activator]::CreateInstance($configurationType) -$negativeWeightConfig.CurvatureMagnitudeWeight = -0.01 -Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $negativeWeightConfig)) } 'Negative cost weights must be rejected.' -$nonFiniteWeightConfig = [Activator]::CreateInstance($configurationType) -$nonFiniteWeightConfig.ClearanceCostWeight = [double]::NaN -Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $nonFiniteWeightConfig)) } 'Non-finite cost weights must be rejected.' - -$dijkstraType = $assembly.GetType($search + 'GridDijkstraHeuristic', $true) -$dijkstraConstructor = $dijkstraType.GetConstructor(@($map.GetType(), [int], [int])) -Assert-True ($dijkstraConstructor -ne $null) 'GridDijkstraHeuristic must expose the planned grid constructor.' -$getCost = Find-Method $dijkstraType 'GetCost' @([int], [int]) -Assert-True ($getCost -ne $null) 'GridDijkstraHeuristic must expose GetCost(row, col).' -$openHeuristic = $dijkstraConstructor.Invoke(@($map, 2, 2)) -$expectedDiagonalCost = 2.0 * [Math]::Sqrt(2.0) * $map.ResolutionMeters -Assert-Near $expectedDiagonalCost $getCost.Invoke($openHeuristic, @(0, 0)) 'Open-grid diagonal movement must cost sqrt(2) per cell.' -$cornerBlockedMap = New-CornerBlockedMap -$blockedHeuristic = $dijkstraConstructor.Invoke(@($cornerBlockedMap, 1, 1)) -Assert-True ([double]::IsPositiveInfinity($getCost.Invoke($blockedHeuristic, @(0, 0)))) 'Diagonal corner cutting through two occupied orthogonal cells must be forbidden.' - -$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $true) -$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $true) -$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @([Threading.CancellationToken], [TimeSpan]), $null) -Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' -$tryCreateDijkstra = Find-NonPublicStaticMethod $dijkstraType 'TryCreate' @($map.GetType(), [int], [int], $operationBudgetType, - $dijkstraType.MakeByRefType(), $operationStopReasonType.MakeByRefType()) -Assert-True ($tryCreateDijkstra -ne $null) 'GridDijkstraHeuristic must expose an internal budget-aware TryCreate method.' -$expiredBudget = $budgetConstructor.Invoke(@([Threading.CancellationToken]::None, [TimeSpan]::Zero)) -$dijkstraArguments = [object[]]@($map, 2, 2, $expiredBudget, $null, $null) -$dijkstraCreated = $tryCreateDijkstra.Invoke($null, $dijkstraArguments) -Assert-False $dijkstraCreated 'An expired total budget must stop Dijkstra construction.' -Assert-Null $dijkstraArguments[4] 'Stopped Dijkstra construction must not publish a partial heuristic.' -Assert-Equal 'TimedOut' $dijkstraArguments[5].ToString() 'Expired Dijkstra construction must report timeout.' - -$largeBounds = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true), - @([single]0, [single]40000, [single]0, [single]20000)) -$largeMapRequest = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true)) -$largeMapRequest.Bounds = $largeBounds -$largeMapRequest.ResolutionMm = [single]20 -$largeMapRequest.AllowExplicitEmptyMap = $true -$largeMap = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory', $true)).Create($largeMapRequest) -Assert-True $largeMap.Succeeded 'Large empty map for Dijkstra cancellation must be created before cancellation starts.' -$midDijkstraCancellation = New-Object Threading.CancellationTokenSource -$midDijkstraBudget = $budgetConstructor.Invoke(@($midDijkstraCancellation.Token, [TimeSpan]::FromSeconds(2))) -$midDijkstraCancellation.CancelAfter(1) -$midDijkstraTimer = [Diagnostics.Stopwatch]::StartNew() -$midDijkstraArguments = [object[]]@($largeMap.Map, 500, 1000, $midDijkstraBudget, $null, $null) -$midDijkstraCreated = $tryCreateDijkstra.Invoke($null, $midDijkstraArguments) -$midDijkstraTimer.Stop() -Assert-False $midDijkstraCreated 'Cancellation during Dijkstra initialization must stop construction.' -Assert-Null $midDijkstraArguments[4] 'Mid-Dijkstra cancellation must not publish a partial heuristic.' -Assert-Equal 'Cancelled' $midDijkstraArguments[5].ToString() 'Mid-Dijkstra cancellation must retain cancellation status.' -Assert-True ($midDijkstraTimer.Elapsed -lt [TimeSpan]::FromSeconds(2)) 'Dijkstra cancellation must return within the configured response bound.' - -Write-Output 'Coarse path search primitive checks passed.' - -$nodeType = $assembly.GetType($search + 'HybridAStarNode', $true) -$nodeKeyType = $assembly.GetType($search + 'HybridAStarNodeKey', $true) -$searchType = $assembly.GetType($search + 'HybridAStarSearch', $true) -$searchResultType = $assembly.GetType($search + 'HybridAStarSearchResult', $true) -$searchMethod = Find-Method $searchType 'Search' @($requestType, [Threading.CancellationToken]) -Assert-True ($searchMethod -ne $null) 'HybridAStarSearch must expose Search(request, cancellationToken).' -Assert-True ($searchResultType.GetProperty('Status') -ne $null) 'Search result must expose planning status.' -Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) 'Search result must expose its original termination reason.' -Assert-True ($searchResultType.GetProperty('SuccessNodeIndex') -ne $null) 'Search result must expose the successful node index.' -Assert-True ($searchResultType.GetProperty('ReopenedNodeCount') -ne $null) 'Search result must expose reopened node count.' -Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entry count.' -Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak count.' -Assert-True ($nodeType.GetProperty('ParentNodeIndex') -ne $null) 'Search node must expose its parent node index.' -Assert-True ($nodeKeyType.GetProperty('HeadingIndex') -ne $null) 'Search node key must contain a heading index.' -Assert-True ($nodeKeyType.GetProperty('Direction') -ne $null) 'Search node key must contain a travel direction.' -Assert-True ($nodeKeyType.GetProperty('CurvatureLevelIndex') -ne $null) 'Search node key must contain a curvature level index.' - -$goalCandidateProperty = $nodeType.GetProperty('IsGoalCandidate') -Assert-True ($goalCandidateProperty -ne $null) 'Search node must mark a goal-truncated candidate explicitly.' -$nodeConstructor = $nodeType.GetConstructor(@([int], $nodeKeyType, $poseType, [int], $primitiveType, [double], [double], [double], [bool])) -Assert-True ($nodeConstructor -ne $null) 'Search node must accept the explicit goal-candidate marker.' -$admissionMethod = $searchType.GetMethod('ShouldEnqueueSuccessor', [Reflection.BindingFlags]'NonPublic,Static') -Assert-True ($admissionMethod -ne $null) 'Search must expose its private successor admission policy for reflection regression coverage.' - -$candidateStart = [Activator]::CreateInstance($poseType, @(1.000, 1.000, 0.0)) -$candidateGoal = [Activator]::CreateInstance($poseType, @(1.024, 1.000, 0.0)) -$candidatePoints = [Array]::CreateInstance($poseType, 1) -$candidatePoints.SetValue($candidateGoal, 0) -$candidatePrimitive = [Activator]::CreateInstance($primitiveType, @($candidateStart, $forward, 0.0, 0.024, $candidatePoints, [double[]]@(1.0), $true)) -$sharedKey = [Activator]::CreateInstance($nodeKeyType, @(20, 20, 0, $forward, 2)) -$normalNode = $nodeConstructor.Invoke(@(10, $sharedKey, $candidateStart, 0, $null, 0.0, 0.250, 0.0, $false)) -$goalCandidateNode = $nodeConstructor.Invoke(@(11, $sharedKey, $candidateGoal, 10, $candidatePrimitive, 0.0, 0.300, 0.0, $true)) -$dominanceConfiguration = [Activator]::CreateInstance($configurationType) -$dominanceConfiguration.GoalPositionToleranceMeters = 0.001 -$dominanceConfiguration.GoalHeadingToleranceRadians = 0.001 -Assert-True $candidatePrimitive.IsGoalTruncation 'Regression setup must use a goal-truncated motion primitive.' -Assert-True $normalNode.Key.Equals($goalCandidateNode.Key) 'Regression setup must share one discrete state key.' -Assert-False $isSatisfied.Invoke($null, @($normalNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Lower-G normal node must remain outside the continuous goal tolerance.' -Assert-True $isSatisfied.Invoke($null, @($goalCandidateNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Goal candidate must satisfy the continuous goal tolerance.' -Assert-False $admissionMethod.Invoke($null, @($normalNode, 0.250)) 'Equal-cost normal state must be dominated by the best same-key label.' -Assert-True $admissionMethod.Invoke($null, @($goalCandidateNode, 0.250)) 'Goal candidate must enter Open List even when a lower-G normal state has the same key.' - -function New-SearchRequest([object]$Goal, [object]$GoalDirection, [bool]$AllowReverse) { - $searchRequest = [Activator]::CreateInstance($requestType) - $searchRequest.Map = $map - $searchRequest.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) - $searchRequest.Goal = $Goal - $searchRequest.Vehicle = $vehicle - $searchConfiguration = [Activator]::CreateInstance($configurationType) - $searchConfiguration.PrimitiveLengthMeters = 0.50 - $searchConfiguration.IntegrationStepMeters = 0.05 - $searchConfiguration.MaximumCollisionCheckStepMeters = 0.025 - $searchConfiguration.GoalPositionToleranceMeters = 0.001 - $searchConfiguration.GoalHeadingToleranceRadians = 0.001 - $searchConfiguration.CurvatureLevelCount = 5 - $searchConfiguration.MaximumExpandedNodes = 10000 - $searchConfiguration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) - $searchConfiguration.AllowReverse = $AllowReverse - $searchRequest.Configuration = $searchConfiguration - $searchRequest.GoalDirection = $GoalDirection - return $searchRequest -} - -$searcher = [Activator]::CreateInstance($searchType) -$searchGoal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) -$searchRequest = New-SearchRequest $searchGoal $forwardOnly $false -$searchResult = $searchMethod.Invoke($searcher, @($searchRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $searchResult.Status.ToString() 'Empty map forward search must succeed.' -Assert-True ($null -ne $searchResult.SuccessNodeIndex) 'Success must be reported only with a popped goal candidate node.' -Assert-True ($searchResult.ExpandedNodeCount -gt 0) 'A successful search must expand the selected goal candidate.' - -$atGoal = New-SearchRequest $searchRequest.Start $forwardOnly $false -$atGoalResult = $searchMethod.Invoke($searcher, @($atGoal, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $atGoalResult.Status.ToString() 'A zero-length goal candidate must enter and leave the open list successfully.' -Assert-True ($null -ne $atGoalResult.SuccessNodeIndex) 'A zero-length goal candidate must have a node index.' - -$cancelledRequest = New-SearchRequest $searchGoal $forwardOnly $false -$cancellationSource = New-Object Threading.CancellationTokenSource -$cancellationSource.Cancel() -$cancelledResult = $searchMethod.Invoke($searcher, @($cancelledRequest, $cancellationSource.Token)) -Assert-Equal 'Cancelled' $cancelledResult.Status.ToString() 'Cancellation must be observed before node expansion.' -Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) 'Cancelled search must retain a non-empty reason.' - -$limitedRequest = New-SearchRequest $searchGoal $forwardOnly $false -$limitedRequest.Configuration.MaximumExpandedNodes = 0 -$limitedResult = $searchMethod.Invoke($searcher, @($limitedRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchNodeLimitExceeded' $limitedResult.Status.ToString() 'Node limit must be checked before expansion.' -Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) 'Node-limited search must retain a non-empty reason.' - -$timedOutRequest = New-SearchRequest $searchGoal $forwardOnly $false -$timedOutRequest.Configuration.SearchTimeout = [TimeSpan]::Zero -$timedOutResult = $searchMethod.Invoke($searcher, @($timedOutRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchTimeout' $timedOutResult.Status.ToString() 'Timeout must be checked before expansion.' -Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) 'Timed-out search must retain a non-empty reason.' -Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) 'Cancelled and node-limited searches must retain different reasons.' -Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) 'Node-limited and timed-out searches must retain different reasons.' - -$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') | - Where-Object { - $_.Name -eq 'Search' -and - $_.GetParameters().Length -eq 2 -and - $_.GetParameters()[1].ParameterType -eq $operationBudgetType - } | - Select-Object -First 1 -Assert-True ($internalSearchMethod -ne $null) 'Search must retain its internal shared-budget overload.' -$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null)) -Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() 'A missing internal budget must be mapped to InternalError.' -Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) 'Internal search errors must retain the exception type.' - -Write-Output 'Coarse path Hybrid A star search checks passed.' diff --git a/.task8-sweep/tests/verify_coarse_path_ui.ps1 b/.task8-sweep/tests/verify_coarse_path_ui.ps1 deleted file mode 100644 index 58b2d00..0000000 --- a/.task8-sweep/tests/verify_coarse_path_ui.ps1 +++ /dev/null @@ -1,122 +0,0 @@ -$ErrorActionPreference = 'Stop' - -function Assert-True([bool]$condition, [string]$message) { - if (-not $condition) { throw $message } -} - -function Assert-Match([string]$content, [string]$pattern, [string]$message) { - Assert-True ($content -match $pattern) $message -} - -function Assert-NotMatch([string]$content, [string]$pattern, [string]$message) { - Assert-True ($content -notmatch $pattern) $message -} - -$sourcePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\Test\MovementTest.CoarsePathTest.cs' -Assert-True (Test-Path -LiteralPath $sourcePath) 'P1 coarse-path UI movement-test source must exist.' -$source = Get-Content -Raw -Encoding UTF8 $sourcePath - -Assert-Match $source 'class\s+CoarsePathPlanningTest\s*:\s*MovementTest' 'The UI entry must inherit MovementTest.' -Assert-Match $source 'namespace\s+MultiWheelC\s*;' 'MovementTest entries must use the project discovery namespace.' -Assert-Match $source 'CoarsePathPlanningService' 'The UI must keep one shared planning service for map-cache reuse.' -Assert-Match $source 'Task\.Run\s*\(' 'Planning must run in a background Task.' -Assert-Match $source 'CancellationTokenSource' 'The UI must retain a cancellation source for its active task.' -Assert-Match $source 'override\s+void\s+TestStop\s*\(' 'The UI must implement TestStop cancellation.' -Assert-Match $source 'CoarsePathScenarioFactory\.Create\s*\(' 'The UI must expose fixed scenario entries through the factory.' -Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.' -Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.' -Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.' -Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.' -Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.' -Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.' -Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.' -Assert-Match $source 'UI\.GetInput\s*\(' 'The manual demo must accept user input.' -Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' 'The manual UI must read a finite positive timeout.' -Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' 'The manual UI must apply the timeout to the current job.' -Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' 'The manual timeout must convert seconds to TimeSpan.' -Assert-Match $source 'timeoutSeconds\s*<=\s*0' 'The manual timeout must reject zero and negative values.' -Assert-Match $source 'getCartLocation\s*\(' 'The manual demo must read the AMR body-center pose.' -Assert-Match $source 'DrawLegend\s*\(' 'The painter output must include a visual legend.' -Assert-Match $source 'PlanningGridMap' 'The painter output must consume the planning grid result.' -Assert-Match $source '\.IsOccupied\s*\(' 'The painter output must draw occupied grid cells.' -Assert-Match $source 'ResolutionMm' 'The painter output must draw the grid at its true resolution.' -Assert-Match $source 'SnapshotId' 'The painter output must show the map snapshot identity.' -Assert-Match $source 'IsGearSwitchPoint' 'The painter output must highlight gear-switch points.' -Assert-Match $source 'GoalPositionToleranceMeters' 'The painter output must draw goal tolerance.' -Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) 'The status layer and Toast must both show path-search elapsed time.' -Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' 'The failure Toast must include the termination reason.' -Assert-Match $source 'ExpandedNodeCount' 'The status layer must show expanded-node statistics.' -Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' 'The status layer must show the effective turning radius.' -Assert-NotMatch $source 'PlanningMapFactory' 'Movement tests must not build maps directly; use CoarsePathPlanningService.' -Assert-NotMatch $source '\.Result\b' 'The UI must not block on Task.Result.' -Assert-NotMatch $source '\.Wait\s*\(' 'The UI must not block the movement-test thread.' - -$runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner') -Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.' -$runnerSource = $source.Substring($runnerStart) -Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.' -Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.' -Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.' -Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.' -Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.' -Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.' -$inputFailureStart = $runnerSource.IndexOf('internal static void ShowInputFailure') -$inputFailureEnd = $runnerSource.IndexOf('private static void Finish', $inputFailureStart) -Assert-True ($inputFailureStart -ge 0 -and $inputFailureEnd -gt $inputFailureStart) 'Input failure renderer must be isolated before completion handling.' -$inputFailureSource = $runnerSource.Substring($inputFailureStart, $inputFailureEnd - $inputFailureStart) -Assert-Match $inputFailureSource 'Painter\.DrawText\s*\(' 'Invalid AMR input must be shown on the Painter status layer.' - -$readmePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\README.md' -Assert-True (Test-Path -LiteralPath $readmePath) 'CoarsePath README must exist beside its module.' -$readme = Get-Content -Raw -Encoding UTF8 $readmePath -foreach ($requiredText in @( - 'CoarsePathPlanningV1', - 'CoarsePathPlanningTest', - 'CreateManualGoalDemo', - 'getCartLocation', - 'mm', - 'deg', - 'rad', - 'CancellationTokenSource', - 'TestStop', - 'NoFeasiblePath', - 'IsGearSwitchPoint')) { - Assert-True ($readme.Contains($requiredText)) "P1 README must document $requiredText." -} - -$readmeStructure = @( - 'File Structure', - 'Planning Data Flow', - 'Build Status and Stop', - 'Coordinates and Units', - 'Minimal Call Example', - 'Cache and SourceVersion', - 'Detailed Usage Guide', - 'P1 Manual Tests and Visualization', - 'Common Errors', - 'First-Version Limits', - 'CoarsePathPlanningService.Plan(job, cancellationToken)', - 'CoarsePathPlanningJob', - 'PlanningGridMap', - 'SourceVersion', - 'CoarsePathPlanningV1', - 'CancellationTokenSource', - 'NoFeasiblePath', - 'IsGearSwitchPoint', - 'CreateManualObstacleDemo', - 'ManualCoarsePathObstacle', - 'manual-user-input', - '0-20', - 'PathSearchElapsed', - '1.20 m', - 'Reeds-Shepp', - 'AxisAlignedRectangle', - 'Create(CoarsePathTestScenario scenario, double amrXMillimeters', - 'DetectionHeadingRadians', - '../Map/README.md' -) -foreach ($requiredText in $readmeStructure) { - Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText." -} - -Write-Output 'Coarse path P1 UI source checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_algorithm_input.ps1 b/.task8-sweep/tests/verify_path_smoothing_algorithm_input.ps1 deleted file mode 100644 index e2682fa..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_algorithm_input.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { - throw "$Message Expected=$Expected Actual=$Actual" - } -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch { - return - } - - throw $Message -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Get-RequiredProperty($Type, [string]$Name) { - $property = $Type.GetProperty($Name, [Reflection.BindingFlags]'Instance,Public,NonPublic') - Assert-True ($null -ne $property) ("Missing property: " + $Name) - Assert-True (-not $property.CanWrite) ("Snapshot property must be get-only: " + $Name) - return $property -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$algorithms = $root + 'Algorithms.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - -$snapshotType = Get-RequiredType ($algorithms + 'SmoothingOptionsSnapshot') -$inputType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmInput') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$preparedPathType = Get-RequiredType ($root + 'Processing.PreparedPath') -$mapType = Get-RequiredType ($mapping + 'PlanningGridMap') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$pathSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') - -$snapshotConstructor = $snapshotType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @($configurationType), $null) -Assert-True ($null -ne $snapshotConstructor) 'SmoothingOptionsSnapshot must be created from PathSmoothingConfiguration.' - -$inputConstructor = $inputType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @($preparedPathType, $mapType, $vehicleType, [double], [double], $snapshotType), $null) -Assert-True ($null -ne $inputConstructor) 'SmoothingAlgorithmInput must accept the immutable smoothing-options snapshot at its construction boundary.' - -$optionNames = @( - 'CubicBSplineEndpointTangentScale', - 'BezierCornerHeadingThresholdRadians', - 'BezierMaximumWindowLengthMeters', - 'BezierHandleLengthRatio', - 'QuinticKnotSpacingMeters', - 'QuinticMinimumKnotSpacingMeters') -$optionProperties = @{} -foreach ($optionName in $optionNames) { - $optionProperties[$optionName] = Get-RequiredProperty $snapshotType $optionName -} - -function New-Configuration { - return [Activator]::CreateInstance($configurationType) -} - -function New-Snapshot($Configuration) { - return $snapshotConstructor.Invoke(@($Configuration)) -} - -# Request creation takes a configuration copy. Later mutations to either the source configuration -# or a configuration copy returned by the request must not alter the algorithm snapshot. -$configuration = New-Configuration -$configuration.CubicBSpline.EndpointTangentScale = [double]0.20 -$configuration.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.40 -$configuration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.80 -$configuration.LocalCubicBezier.HandleLengthRatio = [double]0.25 -$configuration.PiecewiseQuintic.KnotSpacingMeters = [double]0.60 -$configuration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.15 -$emptyCoarsePath = [Array]::CreateInstance($coarsePointType, 0) -$emptySegments = [Array]::CreateInstance($pathSegmentType, 0) -$request = [Activator]::CreateInstance($requestType, @($emptyCoarsePath, $emptySegments, $null, $null, $configuration)) -$configuration.CubicBSpline.EndpointTangentScale = [double]0.90 -$requestConfiguration = $request.Configuration -Assert-Near 0.20 $requestConfiguration.CubicBSpline.EndpointTangentScale 'Request configuration must remain independent from source-config mutations.' -$snapshot = New-Snapshot $requestConfiguration -$requestConfiguration.CubicBSpline.EndpointTangentScale = [double]0.70 -Assert-Near 0.20 $optionProperties['CubicBSplineEndpointTangentScale'].GetValue($snapshot) 'Algorithm options must remain independent from request-configuration mutations.' -Assert-Near 0.40 $optionProperties['BezierCornerHeadingThresholdRadians'].GetValue($snapshot) 'Snapshot must preserve the request configuration values.' -Assert-Near 0.80 $optionProperties['BezierMaximumWindowLengthMeters'].GetValue($snapshot) 'Snapshot must preserve the request configuration values.' -Assert-Near 0.25 $optionProperties['BezierHandleLengthRatio'].GetValue($snapshot) 'Snapshot must preserve the request configuration values.' -Assert-Near 0.60 $optionProperties['QuinticKnotSpacingMeters'].GetValue($snapshot) 'Snapshot must preserve the request configuration values.' -Assert-Near 0.15 $optionProperties['QuinticMinimumKnotSpacingMeters'].GetValue($snapshot) 'Snapshot must preserve the request configuration values.' - -function Assert-InvalidSnapshot([scriptblock]$Mutate, [string]$Message) { - $invalidConfiguration = New-Configuration - & $Mutate $invalidConfiguration - Assert-Throws { New-Snapshot $invalidConfiguration } $Message -} - -Assert-InvalidSnapshot { param($c) $c.CubicBSpline.EndpointTangentScale = [double]::NaN } 'Non-finite B-spline tangent scale must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.CubicBSpline.EndpointTangentScale = [double]0.0 } 'Non-positive B-spline tangent scale must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.0 } 'A zero Bézier heading threshold must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.LocalCubicBezier.CornerHeadingThresholdRadians = [Math]::PI + 0.0001 } 'A Bézier heading threshold above pi must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.LocalCubicBezier.MaximumWindowLengthMeters = [double]::PositiveInfinity } 'A non-finite Bézier window length must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.LocalCubicBezier.HandleLengthRatio = [double]0.0 } 'A non-positive Bézier handle ratio must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]0.0 } 'A non-positive quintic knot spacing must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]::NaN } 'A non-finite quintic minimum knot spacing must be rejected before retry.' -Assert-InvalidSnapshot { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]0.05; $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.10 } 'Quintic knot spacing below the configured minimum must be rejected before retry.' - -Write-Output 'Path smoothing algorithm-input checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_bezier.ps1 b/.task8-sweep/tests/verify_path_smoothing_bezier.ps1 deleted file mode 100644 index 0debea4..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_bezier.ps1 +++ /dev/null @@ -1,319 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Get-PropertyValue($Instance, [string]$Name) { - $property = $Instance.GetType().GetProperty($Name, [Reflection.BindingFlags]'Instance,Public,NonPublic') - Assert-True ($null -ne $property) ("Missing property: " + $Name) - return $property.GetValue($Instance) -} - -function Assert-PointBitwiseEqual($Expected, $Actual, [string]$Message) { - foreach ($name in @('X', 'Y', 'ArcLength', 'Heading', 'UnwrappedHeading', 'BodyClearance')) { - $expectedBits = [BitConverter]::DoubleToInt64Bits([double]$Expected.$name) - $actualBits = [BitConverter]::DoubleToInt64Bits([double]$Actual.$name) - Assert-Equal $expectedBits $actualBits ($Message + ' ' + $name) - } - Assert-Equal $Expected.IsGearSwitchPoint $Actual.IsGearSwitchPoint ($Message + ' IsGearSwitchPoint') - Assert-Equal $Expected.Source.ToString() $Actual.Source.ToString() ($Message + ' Source') -} - -function New-Point( - [double]$X, - [double]$Y, - [double]$ArcLength, - [double]$Heading, - [double]$BodyClearance = 0.10, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($pointType, @( - $X, $Y, $ArcLength, $Heading, $Heading, $BodyClearance, $IsGearSwitch, $anchor)) -} - -function New-DirectionSegment( - [int]$Index, - $Direction, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch)) -} - -function New-EmptyMap { - $request = [Activator]::CreateInstance($mapRequestType) - $request.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $map = [Activator]::CreateInstance($mapFactoryType).Create($request).Map - Assert-True ($null -ne $map) 'Bézier test must create an explicit empty planning map.' - return $map -} - -function New-AlgorithmInput( - [object[]]$Segments, - [double]$ReserveMeters = 0.02, - [double]$CornerThresholdRadians = ([Math]::PI / 18.0), - [double]$MaximumWindowLengthMeters = 0.60, - [double]$HandleLengthRatio = (1.0 / 3.0)) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]]@(,$typedSegments)) - $vehicle = [Activator]::CreateInstance($vehicleType) - $vehicle.LengthMeters = [double]0.20 - $vehicle.WidthMeters = [double]0.20 - $vehicle.SafetyMarginMeters = [double]0.0 - $vehicle.MaximumCurvaturePerMeter = [double]100.0 - $vehicle.MinimumTurningRadiusMeters = [double]0.01 - $configuration = [Activator]::CreateInstance($configurationType) - $configuration.LocalCubicBezier.CornerHeadingThresholdRadians = $CornerThresholdRadians - $configuration.LocalCubicBezier.MaximumWindowLengthMeters = $MaximumWindowLengthMeters - $configuration.LocalCubicBezier.HandleLengthRatio = $HandleLengthRatio - $options = $optionsConstructor.Invoke(@($configuration)) - return $inputConstructor.Invoke(@($preparedPath, (New-EmptyMap), $vehicle, [double]0.05, $ReserveMeters, $options)) -} - -function Invoke-Candidate( - [object[]]$Segments, - [double]$ReserveMeters = 0.02, - [double]$CornerThresholdRadians = ([Math]::PI / 18.0), - [double]$MaximumWindowLengthMeters = 0.60, - [double]$HandleLengthRatio = (1.0 / 3.0), - [double]$Strength = 1.0) { - return $smoothMethod.Invoke($smoother, @( - (New-AlgorithmInput $Segments $ReserveMeters $CornerThresholdRadians $MaximumWindowLengthMeters $HandleLengthRatio), - $Strength, - [Threading.CancellationToken]::None)) -} - -function Invoke-Smoothing( - [object[]]$Segments, - [double]$ReserveMeters = 0.02, - [double]$CornerThresholdRadians = ([Math]::PI / 18.0), - [double]$MaximumWindowLengthMeters = 0.60, - [double]$HandleLengthRatio = (1.0 / 3.0), - [double]$Strength = 1.0) { - $candidate = Invoke-Candidate $Segments $ReserveMeters $CornerThresholdRadians $MaximumWindowLengthMeters $HandleLengthRatio $Strength - Assert-True (Get-PropertyValue $candidate 'Succeeded') 'Bézier smoothing must produce a candidate for the deterministic fixture.' - return @(Get-PropertyValue $candidate 'Segments') -} - -function Get-InterpolatedRunCount($Points) { - $runCount = 0 - $inRun = $false - foreach ($point in $Points) { - $interpolated = $point.Source.ToString() -eq 'Interpolated' - if ($interpolated -and -not $inRun) { $runCount++ } - $inRun = $interpolated - } - return $runCount -} - -function Get-PointAtArcLength($Points, [double]$ArcLength) { - foreach ($point in $Points) { - if ([BitConverter]::DoubleToInt64Bits([double]$point.ArcLength) -eq - [BitConverter]::DoubleToInt64Bits($ArcLength)) { - return $point - } - } - throw "No output point found at local arc length $ArcLength." -} - -function Get-FirstInterpolatedPoint($Points) { - foreach ($point in $Points) { - if ($point.Source.ToString() -eq 'Interpolated') { return $point } - } - throw 'Expected an interpolated Bézier point.' -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$processing = $root + 'Processing.' -$algorithms = $root + 'Algorithms.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' - -$smootherType = Get-RequiredType ($algorithms + 'LocalCubicBezierSmoother') -$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D') -$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') -$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') -$inputType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmInput') -$optionsType = Get-RequiredType ($algorithms + 'SmoothingOptionsSnapshot') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$candidateStatusType = Get-RequiredType ($algorithms + 'SmoothingCandidateStatus') -$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm' -$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap' -$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest' -$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory' - -$inputConstructor = $inputType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @($preparedPathType, $mapType, $vehicleType, [double], [double], $optionsType), $null) -Assert-True ($null -ne $inputConstructor) 'Bézier tests must construct algorithm input with immutable option values.' -$optionsConstructor = $optionsType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, @($configurationType), $null) -Assert-True ($null -ne $optionsConstructor) 'Bézier tests must create immutable option snapshots.' -$smoother = [Activator]::CreateInstance($smootherType, $true) -$smoothMethod = $smootherType.GetMethod('Smooth', [Reflection.BindingFlags]'Instance,Public') -Assert-True ($null -ne $smoothMethod) 'LocalCubicBezierSmoother must implement the internal smoother contract.' -Assert-Equal 'LocalCubicBezier' $smoother.Method.ToString() 'Bézier smoother must identify its public smoothing method.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anchor = [Enum]::Parse($sourceType, 'Anchor') - -# A straight path must not create a local Bézier window or alter any sample. -$straightSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 0.1 0.0 0.1 0.0), - (New-Point 0.2 0.0 0.2 0.0), - (New-Point 0.3 0.0 0.3 0.0)) -$straightOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $straightSource)))[0].Points -Assert-Equal 0 (Get-InterpolatedRunCount $straightOutput) 'A straight line must create no Bézier replacement window.' -Assert-Equal $straightSource.Count $straightOutput.Count 'A straight line must retain its original sample count.' -for ($index = 0; $index -lt $straightSource.Count; $index++) { - Assert-PointBitwiseEqual $straightSource[$index] $straightOutput[$index] 'Straight samples must remain bitwise unchanged.' -} - -# One corner is one local replacement: only the corner sample is evaluated while the window endpoints stay fixed. -$cornerSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 0.1 0.0 0.1 0.0), - (New-Point 0.2 0.0 0.2 0.0), - (New-Point 0.2 0.1 0.3 ([Math]::PI / 2.0)), - (New-Point 0.2 0.2 0.4 ([Math]::PI / 2.0))) -$cornerOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)))[0].Points -Assert-Equal 1 (Get-InterpolatedRunCount $cornerOutput) 'One corner must produce exactly one contiguous Bézier replacement.' -Assert-Equal $cornerSource.Count $cornerOutput.Count 'One local replacement must preserve the segment sampling topology.' -Assert-True (($cornerOutput[2].X -ne $cornerSource[2].X) -or ($cornerOutput[2].Y -ne $cornerSource[2].Y)) 'The corner sample must be replaced by cubic Bézier geometry.' -Assert-PointBitwiseEqual $cornerSource[1] $cornerOutput[1] 'Bézier entry anchor must remain fixed.' -Assert-PointBitwiseEqual $cornerSource[3] $cornerOutput[3] 'Bézier exit anchor must remain fixed.' -$cornerChordLength = [Math]::Sqrt( - [Math]::Pow($cornerSource[3].X - $cornerSource[1].X, 2.0) + - [Math]::Pow($cornerSource[3].Y - $cornerSource[1].Y, 2.0)) -$cornerHandleLength = $cornerChordLength / 3.0 -$expectedCornerX = - 0.125 * $cornerSource[1].X + - 0.375 * ($cornerSource[1].X + $cornerHandleLength) + - 0.375 * $cornerSource[3].X + - 0.125 * $cornerSource[3].X -$expectedCornerY = - 0.125 * $cornerSource[1].Y + - 0.375 * $cornerSource[1].Y + - 0.375 * ($cornerSource[3].Y - $cornerHandleLength) + - 0.125 * $cornerSource[3].Y -$cornerInterpolated = Get-FirstInterpolatedPoint $cornerOutput -Assert-Near $expectedCornerX $cornerInterpolated.X 0.000000000001 'Bézier control handles must use the local endpoint chord length for X geometry.' -Assert-Near $expectedCornerY $cornerInterpolated.Y 0.000000000001 'Bézier control handles must use the local endpoint chord length for Y geometry.' - -# Adjacent corner windows touch/overlap and must become one merged cubic replacement, not two sequential fits. -$overlappingSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 0.1 0.0 0.1 0.0), - (New-Point 0.2 0.0 0.2 0.0), - (New-Point 0.2 0.1 0.3 ([Math]::PI / 2.0)), - (New-Point 0.3 0.1 0.4 0.0), - (New-Point 0.4 0.1 0.5 0.0)) -$overlappingOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $overlappingSource)))[0].Points -Assert-Equal 1 (Get-InterpolatedRunCount $overlappingOutput) 'Touching local corner windows must merge into exactly one Bézier replacement.' -Assert-PointBitwiseEqual $overlappingSource[1] $overlappingOutput[1] 'Merged Bézier entry anchor must remain fixed.' -Assert-PointBitwiseEqual $overlappingSource[4] $overlappingOutput[4] 'Merged Bézier exit anchor must remain fixed.' -Assert-True (($overlappingOutput[2].X -ne $overlappingSource[2].X) -or ($overlappingOutput[2].Y -ne $overlappingSource[2].Y)) 'Merged window must replace the first interior corner sample.' -Assert-True (($overlappingOutput[3].X -ne $overlappingSource[3].X) -or ($overlappingOutput[3].Y -ne $overlappingSource[3].Y)) 'Merged window must replace the second interior corner sample.' - -# Safe bounded policy: decline an entire connected set when its merged interval exceeds the cap. -# The two candidate windows below are each 0.20 m, but their merged 0.30 m interval must not -# produce one over-length curve or be split into new unrequested joins. -$overCapMergedOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $overlappingSource)) 0.02 ([Math]::PI / 18.0) 0.20)[0].Points -Assert-Equal 0 (Get-InterpolatedRunCount $overCapMergedOutput) 'An oversized connected Bézier window set must be declined instead of emitting an over-cap replacement.' -for ($index = 0; $index -lt $overlappingSource.Count; $index++) { - Assert-PointBitwiseEqual $overlappingSource[$index] $overCapMergedOutput[$index] 'Declining an oversized connected set must preserve its anchors.' -} - -# Samples outside a local window must remain bitwise unchanged rather than be globally re-fit. -$isolatedSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 0.1 0.0 0.1 0.0), - (New-Point 0.2 0.0 0.2 0.0), - (New-Point 0.3 0.0 0.3 0.0), - (New-Point 0.3 0.1 0.4 ([Math]::PI / 2.0)), - (New-Point 0.3 0.2 0.5 ([Math]::PI / 2.0)), - (New-Point 0.3 0.3 0.6 ([Math]::PI / 2.0))) -$isolatedOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $isolatedSource)))[0].Points -foreach ($index in @(0, 1, 2, 5, 6)) { - Assert-PointBitwiseEqual $isolatedSource[$index] (Get-PointAtArcLength $isolatedOutput $isolatedSource[$index].ArcLength) 'Samples outside a Bézier window must remain bitwise unchanged.' -} - -# Direction segments stay independent; segment endpoints and the gear-switch anchor are fixed. -$reverseSource = @( - (New-Point 0.2 0.2 0.0 ([Math]::PI / 2.0) 0.10 $true), - (New-Point 0.2 0.1 0.1 ([Math]::PI / 2.0)), - (New-Point 0.2 0.0 0.2 ([Math]::PI / 2.0))) -$switchOutput = @(Invoke-Smoothing @( - (New-DirectionSegment 0 $forward $cornerSource $false $true), - (New-DirectionSegment 1 $reverse $reverseSource $true $false))) -Assert-Equal 2 $switchOutput.Count 'Bézier smoothing must retain separate forward and reverse direction segments.' -Assert-True $switchOutput[0].EndsAtGearSwitch 'The forward segment must retain its gear-switch boundary flag.' -Assert-True $switchOutput[1].StartsAtGearSwitch 'The reverse segment must retain its gear-switch boundary flag.' -Assert-PointBitwiseEqual $cornerSource[0] $switchOutput[0].Points[0] 'Segment start endpoint must remain fixed.' -Assert-PointBitwiseEqual $cornerSource[$cornerSource.Count - 1] $switchOutput[0].Points[$switchOutput[0].Points.Count - 1] 'Segment end endpoint must remain fixed.' -Assert-PointBitwiseEqual $reverseSource[0] $switchOutput[1].Points[0] 'Gear-switch point must remain fixed.' - -# The immutable options each change only their own local behavior. -$thresholdOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 1.70)[0].Points -Assert-Equal 0 (Get-InterpolatedRunCount $thresholdOutput) 'A non-default heading threshold above the corner angle must suppress only corner detection.' -$windowOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 ([Math]::PI / 18.0) 0.15)[0].Points -Assert-Equal 0 (Get-InterpolatedRunCount $windowOutput) 'A non-default maximum window shorter than the local connection must suppress only that window.' -$shortHandleOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 ([Math]::PI / 18.0) 0.60 0.10)[0].Points -$longHandleOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 ([Math]::PI / 18.0) 0.60 0.60)[0].Points -Assert-Equal 1 (Get-InterpolatedRunCount $shortHandleOutput) 'Changing handle ratio must not change detected window topology.' -Assert-Equal 1 (Get-InterpolatedRunCount $longHandleOutput) 'Changing handle ratio must not change detected window topology.' -$shortHandlePoint = Get-FirstInterpolatedPoint $shortHandleOutput -$longHandlePoint = Get-FirstInterpolatedPoint $longHandleOutput -Assert-True (($shortHandlePoint.X -ne $longHandlePoint.X) -or ($shortHandlePoint.Y -ne $longHandlePoint.Y)) 'A non-default handle ratio must change only the local Bézier geometry.' - -# Parameter-matched local arc-length reference comparison must reject excess displacement as retryable and publish no geometry. -$infeasible = Invoke-Candidate @((New-DirectionSegment 0 $forward $cornerSource)) 0.095 -Assert-Equal 'RetryableInfeasible' (Get-PropertyValue $infeasible 'Status').ToString() 'Exceeded local arc-length displacement must be retryable, not terminal.' -Assert-True (-not (Get-PropertyValue $infeasible 'Succeeded')) 'An infeasible Bézier curve must not be executable.' -Assert-Equal 0 (Get-PropertyValue $infeasible 'Segments').Count 'A retryable Bézier infeasibility must publish no executable geometry.' - -# This nonuniform, offset window evaluates its only interior point at t=0.25 and local s=6. -# A wrong global/index mapping would instead compare to s=5 and accept the 0.50 m clearance; -# the required local-arc reference at s=6 must reject the roughly 0.65 m displacement. -$nonuniformOffsetSource = @( - (New-Point 0.0 0.0 0.0 0.0 0.50), - (New-Point 1.0 0.0 4.0 0.0 0.50), - (New-Point 2.0 0.0 5.0 0.0 0.50), - (New-Point 3.0 0.0 6.0 0.0 0.50), - (New-Point 3.0 1.0 9.0 ([Math]::PI / 2.0) 0.50), - (New-Point 3.0 2.0 20.0 ([Math]::PI / 2.0) 0.50)) -$nonuniformOffsetInfeasible = Invoke-Candidate @((New-DirectionSegment 0 $forward $nonuniformOffsetSource)) 0.0 ([Math]::PI / 18.0) 5.0 -Assert-Equal 'RetryableInfeasible' (Get-PropertyValue $nonuniformOffsetInfeasible 'Status').ToString() 'A nonuniform offset window must use local arc-length mapping for retryable clearance rejection.' -Assert-True (-not (Get-PropertyValue $nonuniformOffsetInfeasible 'Succeeded')) 'The nonuniform local-arc infeasibility must not be executable.' -Assert-Equal 0 (Get-PropertyValue $nonuniformOffsetInfeasible 'Segments').Count 'The nonuniform local-arc infeasibility must publish no geometry.' - -Write-Output 'Path smoothing local cubic Bézier checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_bspline.ps1 b/.task8-sweep/tests/verify_path_smoothing_bspline.ps1 deleted file mode 100644 index a0bd3c0..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_bspline.ps1 +++ /dev/null @@ -1,286 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Get-PropertyValue($Instance, [string]$Name) { - $property = $Instance.GetType().GetProperty($Name, [Reflection.BindingFlags]'Instance,Public,NonPublic') - Assert-True ($null -ne $property) ("Missing property: " + $Name) - return $property.GetValue($Instance) -} - -function New-Point( - [double]$X, - [double]$Y, - [double]$ArcLength, - [double]$Heading, - [double]$BodyClearance, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($pointType, @( - $X, $Y, $ArcLength, $Heading, $Heading, $BodyClearance, $IsGearSwitch, $anchor)) -} - -function New-DirectionSegment( - [int]$Index, - $Direction, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch)) -} - -function New-EmptyMap { - $request = [Activator]::CreateInstance($mapRequestType) - $request.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $map = [Activator]::CreateInstance($mapFactoryType).Create($request).Map - Assert-True ($null -ne $map) 'B-spline test must create an explicit empty planning map.' - return $map -} - -function New-AlgorithmInput( - [object[]]$Segments, - [double]$ReserveMeters, - [double]$EndpointTangentScale = (1.0 / 3.0)) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]]@(,$typedSegments)) - $vehicle = [Activator]::CreateInstance($vehicleType) - $vehicle.LengthMeters = [double]0.20 - $vehicle.WidthMeters = [double]0.20 - $vehicle.SafetyMarginMeters = [double]0.0 - $vehicle.MaximumCurvaturePerMeter = [double]100.0 - $vehicle.MinimumTurningRadiusMeters = [double]0.01 - $configuration = [Activator]::CreateInstance($configurationType) - $configuration.CubicBSpline.EndpointTangentScale = $EndpointTangentScale - $options = $optionsConstructor.Invoke(@($configuration)) - return $inputConstructor.Invoke(@($preparedPath, (New-EmptyMap), $vehicle, [double]0.05, $ReserveMeters, $options)) -} - -function Invoke-Candidate( - [object[]]$Segments, - [double]$ReserveMeters, - [double]$Strength = 1.0, - [double]$EndpointTangentScale = (1.0 / 3.0)) { - return $smoothMethod.Invoke($smoother, @( - (New-AlgorithmInput $Segments $ReserveMeters $EndpointTangentScale), $Strength, [Threading.CancellationToken]::None)) -} - -function Invoke-Smoothing( - [object[]]$Segments, - [double]$ReserveMeters, - [double]$Strength = 1.0, - [double]$EndpointTangentScale = (1.0 / 3.0)) { - $candidate = Invoke-Candidate $Segments $ReserveMeters $Strength $EndpointTangentScale - Assert-True (Get-PropertyValue $candidate 'Succeeded') 'B-spline smoothing must produce a candidate for the deterministic fixture.' - return @(Get-PropertyValue $candidate 'Segments') -} - -function Get-PointDistance($Left, $Right) { - $deltaX = $Left.X - $Right.X - $deltaY = $Left.Y - $Right.Y - return [Math]::Sqrt($deltaX * $deltaX + $deltaY * $deltaY) -} - -function Get-DistanceToSegment($Point, $Left, $Right) { - $deltaX = $Right.X - $Left.X - $deltaY = $Right.Y - $Left.Y - $lengthSquared = $deltaX * $deltaX + $deltaY * $deltaY - if ($lengthSquared -le 0.0) { return Get-PointDistance $Point $Left } - $projection = (($Point.X - $Left.X) * $deltaX + ($Point.Y - $Left.Y) * $deltaY) / $lengthSquared - $projection = [Math]::Max(0.0, [Math]::Min(1.0, $projection)) - $closest = New-Object PSObject -Property @{ - X = $Left.X + $projection * $deltaX - Y = $Left.Y + $projection * $deltaY - } - return Get-PointDistance $Point $closest -} - -function Get-DistanceToPolyline($Point, [object[]]$SourcePoints) { - $minimum = [double]::PositiveInfinity - for ($index = 1; $index -lt $SourcePoints.Count; $index++) { - $minimum = [Math]::Min($minimum, (Get-DistanceToSegment $Point $SourcePoints[$index - 1] $SourcePoints[$index])) - } - return $minimum -} - -function Get-TravelAngle($Left, $Right) { - return [Math]::Atan2($Right.Y - $Left.Y, $Right.X - $Left.X) -} - -function Get-AngleDifference([double]$Left, [double]$Right) { - $difference = $Left - $Right - while ($difference -gt [Math]::PI) { $difference -= 2.0 * [Math]::PI } - while ($difference -lt -[Math]::PI) { $difference += 2.0 * [Math]::PI } - return [Math]::Abs($difference) -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$processing = $root + 'Processing.' -$algorithms = $root + 'Algorithms.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' - -$smootherType = Get-RequiredType ($algorithms + 'CubicBSplineSmoother') -$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D') -$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') -$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') -$inputType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmInput') -$optionsType = Get-RequiredType ($algorithms + 'SmoothingOptionsSnapshot') -$interpolatorType = Get-RequiredType ($processing + 'PathReferenceInterpolator') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm' -$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap' -$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest' -$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory' - -$inputConstructor = $inputType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @($preparedPathType, $mapType, $vehicleType, [double], [double], $optionsType), $null) -Assert-True ($null -ne $inputConstructor) 'Algorithm input must carry immutable smoothing options and the minimum clearance reserve for per-anchor movement limits.' -$optionsConstructor = $optionsType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, @($configurationType), $null) -Assert-True ($null -ne $optionsConstructor) 'B-spline tests must create an immutable options snapshot.' -$interpolateMethod = $interpolatorType.GetMethod('TryInterpolateByArcLength', [Reflection.BindingFlags]'Static,Public,NonPublic') -Assert-True ($null -ne $interpolateMethod) 'PathReferenceInterpolator must expose arc-length interpolation.' -$smoother = [Activator]::CreateInstance($smootherType, $true) -$smoothMethod = $smootherType.GetMethod('Smooth', [Reflection.BindingFlags]'Instance,Public') -Assert-True ($null -ne $smoothMethod) 'CubicBSplineSmoother must implement the internal smoother contract.' -Assert-Equal 'CubicBSpline' $smoother.Method.ToString() 'B-spline smoother must identify its public smoothing method.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anchor = [Enum]::Parse($sourceType, 'Anchor') - -# Arc-length interpolation must bracket by local arc rather than by sample index. -$nonUniformPoints = [Array]::CreateInstance($pointType, 4) -$nonUniformPoints.SetValue((New-Point 0.0 0.0 0.000 0.0 1.0), 0) -$nonUniformPoints.SetValue((New-Point 5.0 0.0 0.050 0.1 0.9), 1) -$nonUniformPoints.SetValue((New-Point 10.0 0.0 0.100 0.2 0.8), 2) -$nonUniformPoints.SetValue((New-Point 20.0 0.0 0.125 0.3 0.7), 3) -$interpolateArguments = [object[]]@($nonUniformPoints, [double]0.1125, $null, $null) -Assert-True $interpolateMethod.Invoke($null, $interpolateArguments) 'Arc-length interpolation must accept a target within the final non-uniform interval.' -$arcReference = $interpolateArguments[2] -Assert-Near 15.0 $arcReference.X 0.000000001 'Target arc length 0.1125 must lie halfway through the final 0.100-0.125 interval, independent of point count.' -Assert-Near 0.1125 $arcReference.ArcLength 0.000000001 'Arc-length interpolation must preserve the requested target arc length.' -Assert-Near 0.25 $arcReference.Heading 0.000000001 'Arc-length interpolation must linearly interpolate heading.' -Assert-Near 0.75 $arcReference.BodyClearance 0.000000001 'Arc-length interpolation must linearly interpolate clearance.' - -# Straight samples are returned exactly, so a straight is never distorted or densified. -$straightSource = @( - (New-Point 0.0 0.0 0.0 0.0 0.03), - (New-Point 1.0 0.0 1.0 0.0 0.03), - (New-Point 2.0 0.0 2.0 0.0 0.03), - (New-Point 3.0 0.0 3.0 0.0 0.03)) -$straightResult = Invoke-Smoothing @((New-DirectionSegment 0 $forward $straightSource)) 0.02 -Assert-Equal 1 $straightResult.Count 'A single direction segment must produce exactly one candidate segment.' -Assert-Equal $straightSource.Count $straightResult[0].Points.Count 'A straight must retain its original samples.' -for ($index = 0; $index -lt $straightSource.Count; $index++) { - Assert-Near $straightSource[$index].X $straightResult[0].Points[$index].X 0.0 'Straight X coordinates must remain exact.' - Assert-Near $straightSource[$index].Y $straightResult[0].Points[$index].Y 0.0 'Straight Y coordinates must remain exact.' -} - -# A five-anchor corner uses the preprocessor's 0.05 m sampling scale. It must retain exact endpoint poses, -# follow endpoint travel tangents, turn continuously, and stay within the per-anchor clearance reserve radius. -$cornerSource = @( - (New-Point 0.0 0.0 0.0 0.0 0.08), - (New-Point 0.05 0.0 0.05 0.0 0.08), - (New-Point 0.10 0.0 0.10 0.0 0.08), - (New-Point 0.10 0.05 0.15 ([Math]::PI / 2.0) 0.08), - (New-Point 0.10 0.10 0.20 ([Math]::PI / 2.0) 0.08)) -$cornerResult = Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 -$cornerPoints = @($cornerResult[0].Points) -Assert-True ($cornerPoints.Count -gt $cornerSource.Count) 'A non-straight B-spline candidate must provide sampled curve geometry.' -$cornerStart = $cornerPoints[0] -$cornerEnd = $cornerPoints[$cornerPoints.Count - 1] -Assert-Near $cornerSource[0].X $cornerStart.X 0.0 'B-spline start X must be exact.' -Assert-Near $cornerSource[0].Y $cornerStart.Y 0.0 'B-spline start Y must be exact.' -Assert-Near $cornerSource[$cornerSource.Count - 1].X $cornerEnd.X 0.0 'B-spline end X must be exact.' -Assert-Near $cornerSource[$cornerSource.Count - 1].Y $cornerEnd.Y 0.0 'B-spline end Y must be exact.' -Assert-Near 0.0 (Get-AngleDifference (Get-TravelAngle $cornerPoints[0] $cornerPoints[1]) 0.0) 0.02 'B-spline start travel tangent must follow the supplied forward heading.' -Assert-Near 0.0 (Get-AngleDifference (Get-TravelAngle $cornerPoints[$cornerPoints.Count - 2] $cornerPoints[$cornerPoints.Count - 1]) ([Math]::PI / 2.0)) 0.02 'B-spline end travel tangent must follow the supplied forward heading.' -for ($index = 2; $index -lt $cornerPoints.Count; $index++) { - $previousAngle = Get-TravelAngle $cornerPoints[$index - 2] $cornerPoints[$index - 1] - $currentAngle = Get-TravelAngle $cornerPoints[$index - 1] $cornerPoints[$index] - Assert-True ((Get-AngleDifference $previousAngle $currentAngle) -lt 0.08) 'B-spline corner samples must turn without a tangent discontinuity.' -} - -# Endpoint tangent scale is an immutable option and must influence the clamped B-spline endpoint handle. -$shortHandlePoints = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 1.0 0.20)[0].Points -$longHandlePoints = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $cornerSource)) 0.02 1.0 0.60)[0].Points -Assert-True (($longHandlePoints[2].X - $shortHandlePoints[2].X) -gt 0.0001) 'A custom endpoint tangent scale must change the B-spline start handle and early curve samples.' - -# Every evaluated point may deviate only by BodyClearance - reserve, never by raw BodyClearance. -$allowedRadius = 0.06 -foreach ($point in $cornerPoints) { - Assert-True ((Get-DistanceToPolyline $point $cornerSource) -le ($allowedRadius + 0.000000001)) 'Every B-spline displacement must stay inside the per-anchor clearance reserve radius.' -} - -# A 0.035 m reserve radius must reject this corner by its parameter-matched source reference, -# even though the candidate's nearest-polyline distance is smaller than 0.035 m. -$parameterMatchedReserveCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $cornerSource)) 0.045 -Assert-True (-not (Get-PropertyValue $parameterMatchedReserveCandidate 'Succeeded')) 'A medium reserve must reject B-spline geometry that exceeds its parameter-matched movement radius.' - -# A tight reserve may not publish an evaluated B-spline that leaves its 0.005 m movement radius. -$tightReserveCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $cornerSource)) 0.075 - -# A tight endpoint circle that cannot meet the heading=0.2 rad tangent ray must fail, not silently rotate the handle. -$misalignedHeadingSource = @( - (New-Point 0.0 0.0 0.0 0.2 0.08), - (New-Point 0.05 0.0 0.05 0.0 0.08), - (New-Point 0.10 0.0 0.10 0.0 0.08), - (New-Point 0.10 0.05 0.15 ([Math]::PI / 2.0) 0.08), - (New-Point 0.10 0.10 0.20 ([Math]::PI / 2.0) 0.08)) -$misalignedHeadingCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $misalignedHeadingSource)) 0.075 -$requiredFailures = New-Object System.Collections.Generic.List[string] -if (Get-PropertyValue $tightReserveCandidate 'Succeeded') { - [void]$requiredFailures.Add('A tight reserve published an evaluated candidate outside its permitted movement radius.') -} -if (Get-PropertyValue $misalignedHeadingCandidate 'Succeeded') { - [void]$requiredFailures.Add('A tight reserve silently accepted an endpoint handle that cannot follow the supplied travel tangent.') -} -Assert-Equal 0 $requiredFailures.Count ([string]::Join(' ', $requiredFailures)) - -# Adjacent direction segments retain their duplicated switch pose and independent topology; no fit may cross the switch. -$reverseSource = @( - (New-Point 0.10 0.10 0.0 ([Math]::PI / 2.0) 0.08 $true), - (New-Point 0.10 0.05 0.05 ([Math]::PI / 2.0) 0.08), - (New-Point 0.10 0.0 0.10 ([Math]::PI / 2.0) 0.08)) -$switchResult = Invoke-Smoothing @( - (New-DirectionSegment 0 $forward $cornerSource $false $true), - (New-DirectionSegment 1 $reverse $reverseSource $true $false)) 0.02 -Assert-Equal 2 $switchResult.Count 'B-spline smoothing must preserve each direction segment boundary.' -Assert-True $switchResult[0].EndsAtGearSwitch 'The forward segment must retain its gear-switch boundary flag.' -Assert-True $switchResult[1].StartsAtGearSwitch 'The reverse segment must retain its gear-switch boundary flag.' -$switchLeft = $switchResult[0].Points[$switchResult[0].Points.Count - 1] -$switchRight = $switchResult[1].Points[0] -Assert-Near $switchLeft.X $switchRight.X 0.0 'B-spline smoothing must retain the duplicated gear-switch X pose.' -Assert-Near $switchLeft.Y $switchRight.Y 0.0 'B-spline smoothing must retain the duplicated gear-switch Y pose.' - -Write-Output 'Path smoothing cubic B-spline checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_comparison.ps1 b/.task8-sweep/tests/verify_path_smoothing_comparison.ps1 deleted file mode 100644 index a331e62..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_comparison.ps1 +++ /dev/null @@ -1,230 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function New-Map { - $mapRequest = [Activator]::CreateInstance($mapRequestType) - $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $mapRequest.ResolutionMm = [single]50 - $mapRequest.AllowExplicitEmptyMap = $true - $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map - Assert-True ($null -ne $map) 'Comparison test must create a planning map.' - return $map -} - -function New-Vehicle { - $vehicle = [Activator]::CreateInstance($vehicleType) - $vehicle.LengthMeters = [double]0.20 - $vehicle.WidthMeters = [double]0.20 - $vehicle.SafetyMarginMeters = [double]0.0 - $vehicle.MaximumCurvaturePerMeter = [double]100.0 - return $vehicle -} - -function New-CoarsePoint([double]$X, [double]$Y, [double]$ArcLength) { - return [Activator]::CreateInstance($coarsePointType, @( - $X, $Y, [double]0.0, [double]0.0, $ArcLength, $forward, - [double]0.0, [double]1.0, $false, $coarseAnchor)) -} - -function New-SmoothingRequest { - $points = [Array]::CreateInstance($coarsePointType, 2) - $points.SetValue((New-CoarsePoint 0.5 0.5 0.0), 0) - $points.SetValue((New-CoarsePoint 1.5 0.5 1.0), 1) - $segments = [Array]::CreateInstance($coarseSegmentType, 1) - $segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 1, $false, $false)), 0) - $configuration = [Activator]::CreateInstance($configurationType) - return [Activator]::CreateInstance($smoothingRequestType, @($points, $segments, (New-Map), (New-Vehicle), $configuration)) -} - -function New-CornerSmoothingRequest { - $points = [Array]::CreateInstance($coarsePointType, 4) - $points.SetValue((New-CoarsePoint 0.5 0.5 0.0), 0) - $points.SetValue((New-CoarsePoint 1.0 0.5 0.5), 1) - $points.SetValue((New-CoarsePoint 1.0 1.0 1.0), 2) - $points.SetValue((New-CoarsePoint 1.5 1.0 1.5), 3) - $segments = [Array]::CreateInstance($coarseSegmentType, 1) - $segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 3, $false, $false)), 0) - $configuration = [Activator]::CreateInstance($configurationType) - return [Activator]::CreateInstance($smoothingRequestType, @($points, $segments, (New-Map), (New-Vehicle), $configuration)) -} - -function New-Metrics( - [double]$VariationEnergy, - [double]$PeakCurvature, - [double]$MinimumClearance, - [double]$LengthChangePercent) { - return [Activator]::CreateInstance($metricsType, @( - $true, [double]1.0, $PeakCurvature, [double]0.0, [double]0.0, $VariationEnergy, - $MinimumClearance, $LengthChangePercent, [double]0.0, [double]0.0, [double]0.0)) -} - -function New-Timing([double]$MedianMilliseconds) { - [double[]]$samples = @($MedianMilliseconds, $MedianMilliseconds, $MedianMilliseconds, $MedianMilliseconds, $MedianMilliseconds) - return [Activator]::CreateInstance($timingType, @($samples, $true, '')) -} - -function New-Entry( - $Method, - [double]$VariationEnergy, - [double]$PeakCurvature, - [double]$MinimumClearance, - [double]$LengthChangePercent, - [double]$MedianMilliseconds) { - [object[]]$arguments = @( - $Method, $successStatus, (New-Metrics $VariationEnergy $PeakCurvature $MinimumClearance $LengthChangePercent), - (New-Timing $MedianMilliseconds), ('synthetic-' + $Method.ToString()), '') - return [Activator]::CreateInstance($entryType, $arguments) -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$comparison = $root + 'Comparison.' -$facade = $root + 'Facade.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - -$comparisonServiceType = Get-RequiredType ($facade + 'PathSmoothingComparisonService') -$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') -$comparisonResultType = Get-RequiredType ($comparison + 'PathSmoothingComparisonResult') -$entryType = Get-RequiredType ($comparison + 'PathSmoothingComparisonEntry') -$timingType = Get-RequiredType ($comparison + 'SmoothingTimingSummary') -$rankerType = Get-RequiredType ($comparison + 'SmoothingMethodRanker') -$smoothingRequestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$metricsType = Get-RequiredType ($root + 'PathQualityMetrics') -$methodType = Get-RequiredType ($root + 'SmoothingMethod') -$statusType = Get-RequiredType ($root + 'PathSmoothingStatus') -$smoothingResultType = Get-RequiredType ($root + 'PathSmoothingResult') -$diagnosticsType = Get-RequiredType ($root + 'PathSmoothingDiagnostics') -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm') -$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest') -$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory') - -$forward = [Enum]::Parse($directionType, 'Forward') -$coarseAnchor = [Enum]::Parse($coarsePointSourceType, 'Start') -$cubicBSpline = [Enum]::Parse($methodType, 'CubicBSpline') -$localCubicBezier = [Enum]::Parse($methodType, 'LocalCubicBezier') -$piecewiseQuintic = [Enum]::Parse($methodType, 'PiecewiseQuintic') -$successStatus = [Enum]::Parse($statusType, 'Success') - -Assert-True $comparisonServiceType.IsPublic 'Comparison service must be public.' -$compareMethod = $comparisonServiceType.GetMethod('Compare', [Type[]]@($comparisonRequestType, [Threading.CancellationToken])) -Assert-True ($null -ne $compareMethod) 'Comparison service must expose Compare(PathSmoothingComparisonRequest, CancellationToken).' -Assert-Equal $comparisonResultType $compareMethod.ReturnType 'Compare must return PathSmoothingComparisonResult.' - -$methods = [Array]::CreateInstance($methodType, 3) -$methods.SetValue($cubicBSpline, 0) -$methods.SetValue($localCubicBezier, 1) -$methods.SetValue($piecewiseQuintic, 2) -$comparisonRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-SmoothingRequest), $methods)) -$comparisonService = [Activator]::CreateInstance($comparisonServiceType) -$result = $compareMethod.Invoke($comparisonService, @($comparisonRequest, [Threading.CancellationToken]::None)) - -Assert-True ($null -ne $result.RawPathBaseline) 'Comparison must publish a separately analyzed raw-path baseline.' -Assert-True $result.RawPathBaseline.IsRawPathBaseline 'Raw baseline must be explicitly marked and excluded from candidates.' -Assert-Equal 3 $result.Entries.Count 'Comparison must contain exactly one entry for every requested method.' -Assert-True ($null -ne $result.RecommendedMethod) 'A comparison with feasible methods must select a recommendation.' -foreach ($entry in $result.Entries) { - Assert-True (-not $entry.IsRawPathBaseline) 'Candidate entries must not be marked as the raw baseline.' - Assert-Equal 5 $entry.Timing.MeasuredElapsedMilliseconds.Count 'Warm-up must be excluded and exactly five measurements retained.' - Assert-True $entry.Timing.IsDeterministic 'Repeated deterministic smoothing geometry must remain eligible for recommendation.' - Assert-True (-not [string]::IsNullOrWhiteSpace($entry.StableGeometryDigest)) 'Every measured candidate must expose a stable geometry digest.' -} - -$fromMeasurementsMethod = $timingType.GetMethod('FromMeasurements') -Assert-True ($null -ne $fromMeasurementsMethod) 'Timing summary must analyze the five measured outputs for deterministic geometry.' -$failureFactory = $smoothingResultType.GetMethod('Failure') -$invalidInputStatus = [Enum]::Parse($statusType, 'InvalidInput') -$infeasibleStatus = [Enum]::Parse($statusType, 'Infeasible') -$inconsistentResults = [Array]::CreateInstance($smoothingResultType, 5) -for ($index = 0; $index -lt 5; $index++) { - $status = if ($index -eq 4) { $infeasibleStatus } else { $invalidInputStatus } - [object[]]$failureArguments = New-Object object[] 2 - $failureArguments[0] = $status - $failureArguments[1] = [Activator]::CreateInstance($diagnosticsType) - $inconsistentResults.SetValue($failureFactory.Invoke($null, $failureArguments), $index) -} -[object[]]$timingArguments = New-Object object[] 2 -$timingArguments[0] = [double[]]@(1.0, 2.0, 3.0, 4.0, 5.0) -$timingArguments[1] = $inconsistentResults -$nonDeterministicTiming = $fromMeasurementsMethod.Invoke($null, $timingArguments) -Assert-True (-not $nonDeterministicTiming.IsDeterministic) 'A status, point-count, segment-count, or digest mismatch must be non-deterministic.' -Assert-True (-not [string]::IsNullOrWhiteSpace($nonDeterministicTiming.Diagnostic)) 'Non-deterministic measurements must publish a stable diagnostic.' - -# All published comparison metrics must be normalized against the separately analyzed raw baseline. -$cornerMethods = [Array]::CreateInstance($methodType, 1) -$cornerMethods.SetValue($cubicBSpline, 0) -$cornerRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-CornerSmoothingRequest), $cornerMethods)) -$cornerResult = $compareMethod.Invoke($comparisonService, @($cornerRequest, [Threading.CancellationToken]::None)) -$cornerEntry = $cornerResult.Entries[0] -Assert-Equal 'Success' $cornerEntry.Status.ToString() 'The unconstrained empty-map corner fixture must produce a B-spline comparison candidate.' -$expectedLengthChange = (($cornerEntry.Path[$cornerEntry.Path.Count - 1].ArcLength - $cornerResult.RawPathBaseline.Metrics.PathLengthMeters) / - $cornerResult.RawPathBaseline.Metrics.PathLengthMeters) * 100.0 -Assert-Near $expectedLengthChange $cornerEntry.Metrics.LengthChangePercent 0.000001 'Candidate length change must be normalized relative to the raw baseline.' - -# The ranker must apply every public tie-break in order. Each pair ties all prior criteria. -$entryListType = [Collections.Generic.IReadOnlyList``1].MakeGenericType(@($entryType)) -$rankMethod = $rankerType.GetMethod('Rank', [Type[]]@($entryListType)) -Assert-True ($null -ne $rankMethod) 'SmoothingMethodRanker must expose Rank(IReadOnlyList).' - -function Assert-Rank($ExpectedMethod, [object[]]$Entries, [string]$Message) { - $typedEntries = [Array]::CreateInstance($entryType, $Entries.Count) - for ($index = 0; $index -lt $Entries.Count; $index++) { $typedEntries.SetValue($Entries[$index], $index) } - [object[]]$invokeArguments = New-Object object[] 1 - $invokeArguments[0] = $typedEntries - $actual = $rankMethod.Invoke($null, $invokeArguments) - Assert-Equal $ExpectedMethod.ToString() $actual.ToString() $Message -} - -Assert-Rank $cubicBSpline @( - (New-Entry $cubicBSpline 1.0 0.5 0.8 5.0 10.0), - (New-Entry $localCubicBezier 2.0 0.1 1.0 1.0 1.0)) 'Variation-energy tie-break must take priority over later criteria.' -Assert-Rank $cubicBSpline @( - (New-Entry $cubicBSpline 1.0 0.2 0.8 5.0 10.0), - (New-Entry $localCubicBezier 1.0 0.3 1.0 1.0 1.0)) 'Peak-curvature tie-break must follow variation energy.' -Assert-Rank $cubicBSpline @( - (New-Entry $cubicBSpline 1.0 0.2 0.9 5.0 10.0), - (New-Entry $localCubicBezier 1.0 0.2 0.8 1.0 1.0)) 'Clearance-loss tie-break must follow peak curvature.' -Assert-Rank $cubicBSpline @( - (New-Entry $cubicBSpline 1.0 0.2 0.9 2.0 10.0), - (New-Entry $localCubicBezier 1.0 0.2 0.9 3.0 1.0)) 'Length-change tie-break must follow clearance loss.' -Assert-Rank $cubicBSpline @( - (New-Entry $cubicBSpline 1.0 0.2 0.9 2.0 5.0), - (New-Entry $localCubicBezier 1.0 0.2 0.9 2.0 6.0)) 'Median elapsed tie-break must be last.' - -$cancelSource = [Threading.CancellationTokenSource]::new() -try { - $cancelSource.Cancel() - $cancelled = $compareMethod.Invoke($comparisonService, @($comparisonRequest, $cancelSource.Token)) - Assert-True $cancelled.IsCancelled 'Cancellation must stop comparison before subsequent methods start.' - Assert-Equal $null $cancelled.RecommendedMethod 'Cancelled comparison must not make a recommendation.' -} -finally { - $cancelSource.Dispose() -} - -Write-Output 'Path smoothing comparison checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_contracts.ps1 b/.task8-sweep/tests/verify_path_smoothing_contracts.ps1 deleted file mode 100644 index 75726c4..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_contracts.ps1 +++ /dev/null @@ -1,381 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' - -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$coarsePathRoot = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$mappingRoot = 'MultiWheelC.TrajectoryPlanning.Mapping.' - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001d) { - throw "$Message Expected=$Expected Actual=$Actual" - } -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - $threw = $false - try { & $Action } - catch { $threw = $true } - if (-not $threw) { throw $Message } -} - -function Assert-ReadOnlyCollection($Collection, [string]$Message) { - $list = [System.Collections.IList]$Collection - Assert-True ($null -ne $list) "$Message The collection must implement IList." - Assert-True $list.IsReadOnly "$Message The collection must report IsReadOnly." - Assert-Throws { $list.Add($null) } "$Message The collection must reject Add." -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$methodType = Get-RequiredType ($root + 'SmoothingMethod') -$statusType = Get-RequiredType ($root + 'PathSmoothingStatus') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$pointType = Get-RequiredType ($root + 'SmoothedPathPoint') -$segmentType = Get-RequiredType ($root + 'SmoothedPathSegment') -$bsplineOptionsType = Get-RequiredType ($root + 'CubicBSplineOptions') -$bezierOptionsType = Get-RequiredType ($root + 'LocalCubicBezierOptions') -$quinticOptionsType = Get-RequiredType ($root + 'PiecewiseQuinticOptions') -$localOptionsType = Get-RequiredType ($root + 'LocalG2QuinticOptions') -$regionStatusType = Get-RequiredType ($root + 'PathSmoothingRegionStatus') -$regionFailureType = Get-RequiredType ($root + 'PathSmoothingRegionFailureReason') -$regionReportType = Get-RequiredType ($root + 'PathSmoothingRegionReport') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$metricsType = Get-RequiredType ($root + 'PathQualityMetrics') -$diagnosticsType = Get-RequiredType ($root + 'PathSmoothingDiagnostics') -$resultType = Get-RequiredType ($root + 'PathSmoothingResult') -$directionType = Get-RequiredType ($coarsePathRoot + 'TravelDirection') -$coarsePointType = Get-RequiredType ($coarsePathRoot + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePathRoot + 'PathSegment') -$mapType = Get-RequiredType ($mappingRoot + 'PlanningGridMap') -$vehicleType = Get-RequiredType ($coarsePathRoot + 'VehicleParameters') - -Assert-Equal $true $methodType.IsEnum 'SmoothingMethod must be a public enum.' -Assert-Equal $true $statusType.IsEnum 'PathSmoothingStatus must be a public enum.' -Assert-Equal $true $sourceType.IsEnum 'SmoothedPathPointSource must be a public enum.' -Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic,LocalG2Quintic' ([string]::Join(',', [Enum]::GetNames($methodType))) 'The Local G2 method must be appended without reordering legacy methods.' -Assert-Equal 'Success,FallbackToCoarsePath,InvalidInput,Infeasible,Failed,Cancelled,Complete,PartialImprovement,NotNeeded,Unchanged' ([string]::Join(',', [Enum]::GetNames($statusType))) 'Local G2 statuses must be appended without reordering legacy statuses.' -Assert-Equal 'Anchor,Interpolated,GearSwitch,CoarsePathFallback,LocalG2Transition' ([string]::Join(',', [Enum]::GetNames($sourceType))) 'Local G2 point source must be appended without reordering legacy sources.' -Assert-Equal 'Improved,RetainedOriginal' ([string]::Join(',', [Enum]::GetNames($regionStatusType))) 'Local G2 region statuses must be stable.' -Assert-Equal 'None,WindowUnavailable,CandidateGenerationFailed,Collision,InsufficientClearance,CurvatureExceeded,CurvatureOvershoot,DeviationExceeded,InsufficientImprovement,VariationCostRegression,GlobalValidationRollback' ([string]::Join(',', [Enum]::GetNames($regionFailureType))) 'Local G2 region failure reasons must be stable.' - -$configuration = [Activator]::CreateInstance($configurationType) -Assert-Near 0.025 $configuration.OutputSpacingMeters 'Default output spacing must be 0.025 m.' -Assert-Near 0.025 $configuration.MaximumCollisionCheckStepMeters 'Default collision step must be 0.025 m.' -Assert-Near 0.02 $configuration.MinimumClearanceReserveMeters 'Default clearance reserve must be 0.02 m.' -Assert-Near 1.0 $configuration.SmoothingStrength 'Default smoothing strength must be 1.0.' -Assert-Equal $true $configuration.AllowFallbackToCoarsePath 'Fallback must be enabled by default.' -Assert-Equal 4 $configuration.RetryStrengthScales.Count 'Retry schedule must contain four entries.' -Assert-Near 1.0 $configuration.RetryStrengthScales[0] 'First retry scale must be 1.0.' -Assert-Near 0.75 $configuration.RetryStrengthScales[1] 'Second retry scale must be 0.75.' -Assert-Near 0.50 $configuration.RetryStrengthScales[2] 'Third retry scale must be 0.50.' -Assert-Near 0.25 $configuration.RetryStrengthScales[3] 'Last retry scale must be 0.25.' -for ($index = 1; $index -lt $configuration.RetryStrengthScales.Count; $index++) { - Assert-True ($configuration.RetryStrengthScales[$index] -lt $configuration.RetryStrengthScales[$index - 1]) 'Retry schedule must be strictly decreasing.' -} -Assert-ReadOnlyCollection $configuration.RetryStrengthScales 'Retry schedule must be immutable.' -Assert-Near (1.0 / 3.0) ([Activator]::CreateInstance($bsplineOptionsType)).EndpointTangentScale 'B-spline endpoint tangent default must be one third.' -$bezier = [Activator]::CreateInstance($bezierOptionsType) -Assert-Near ([Math]::PI / 18.0) $bezier.CornerHeadingThresholdRadians 'Bezier corner threshold must be 10 degrees.' -Assert-Near 0.60 $bezier.MaximumWindowLengthMeters 'Bezier window default must be 0.60 m.' -Assert-Near (1.0 / 3.0) $bezier.HandleLengthRatio 'Bezier handle default must be one third.' -$quintic = [Activator]::CreateInstance($quinticOptionsType) -Assert-Near 0.50 $quintic.KnotSpacingMeters 'Quintic knot spacing must be 0.50 m.' -Assert-Near 0.10 $quintic.MinimumKnotSpacingMeters 'Quintic minimum knot spacing must be 0.10 m.' -$local = [Activator]::CreateInstance($localOptionsType) -Assert-Near 0.20 $local.MinimumWindowLengthMeters 'Minimum Local G2 window must be 0.20 m.' -Assert-Near 0.50 $local.PreferredWindowLengthMeters 'Preferred Local G2 window must be 0.50 m.' -Assert-Near 0.80 $local.MaximumWindowLengthMeters 'Maximum Local G2 window must be 0.80 m.' -Assert-Near 0.10 $local.MaximumDeviationMeters 'Maximum Local G2 deviation must be 0.10 m.' -Assert-Near 0.001 $local.AbsoluteCurvatureJumpFloorPerMeter 'Absolute jump floor must be 0.001 1/m.' -Assert-Near 0.05 $local.CurvatureJumpRatioOfMaximum 'Relative jump threshold must be 5 percent.' -Assert-Near 0.20 $local.MinimumPeakGradientImprovementRatio 'Peak improvement must be 20 percent.' -Assert-Near 0.02 $local.MaximumVariationCostRegressionRatio 'Variation cost tolerance must be 2 percent.' -Assert-Equal 12 $local.MaximumCandidatesPerRegion 'At most twelve candidates are allowed.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$anchor = [Enum]::Parse($sourceType, 'Anchor') -$point = [Activator]::CreateInstance($pointType, @( - [double]1.25, [double]-2.50, [double]0.30, [double]6.58, [double]4.75, - $forward, [double]0.12, [double]0.12, [double]0.44, $false, $anchor)) -Assert-Near 1.25 $point.X 'Smoothed point X must be stored in m.' -Assert-Near -2.50 $point.Y 'Smoothed point Y must be stored in m.' -Assert-Near 0.30 $point.Heading 'Smoothed point heading must be stored in rad.' -Assert-Near 6.58 $point.UnwrappedHeading 'Smoothed point unwrapped heading must be stored in rad.' -Assert-Near 4.75 $point.ArcLength 'Smoothed point arc length must be stored in m.' -Assert-Equal 'Forward' $point.Direction.ToString() 'Smoothed point direction must be preserved.' -Assert-Near 0.12 $point.GeometricCurvature 'Smoothed point geometric curvature must be stored in 1/m.' -Assert-Near 0.12 $point.VehicleCurvature 'Smoothed point vehicle curvature must be stored in 1/m.' -Assert-Near 0.44 $point.BodyClearance 'Smoothed point clearance must be stored in m.' -Assert-Equal $false $point.IsGearSwitchPoint 'Smoothed point gear-switch marker must be preserved.' -Assert-Equal 'Anchor' $point.Source.ToString() 'Smoothed point source must be preserved.' -Assert-True ($pointType.GetProperty('VehicleCurvatureDerivative') -ne $null) 'Smoothed points must expose d-kappa/d-s.' -$pointWithDerivative = [Activator]::CreateInstance($pointType, @( - [double]1.25, [double]-2.50, [double]0.30, [double]6.58, [double]4.75, - $forward, [double]0.12, [double]0.12, [double]0.37, [double]0.44, $false, $anchor)) -Assert-Near 0.37 $pointWithDerivative.VehicleCurvatureDerivative 'Smoothed point curvature derivative must be stored in 1/m^2.' - -$segmentA = [Activator]::CreateInstance($segmentType, @(0, $forward, 0, 2, $false, $true)) -$reverse = [Enum]::Parse($directionType, 'Reverse') -$segmentB = [Activator]::CreateInstance($segmentType, @(1, $reverse, 3, 5, $true, $false)) -Assert-Equal 0 $segmentA.SegmentIndex 'First smoothing segment index must be retained.' -Assert-Equal 'Forward' $segmentA.Direction.ToString() 'First smoothing segment direction must be retained.' -Assert-Equal 2 $segmentA.EndIndex 'First smoothing segment end index must be retained.' -Assert-Equal $true $segmentA.EndsAtGearSwitch 'First smoothing segment switch flag must be retained.' -Assert-Equal 1 $segmentB.SegmentIndex 'Second smoothing segment index must be retained.' -Assert-Equal 'Reverse' $segmentB.Direction.ToString() 'Second smoothing segment direction must be retained.' -Assert-Equal $true $segmentB.StartsAtGearSwitch 'Second smoothing segment switch flag must be retained.' - -$metrics = [Activator]::CreateInstance($metricsType) -Assert-Equal $false $metrics.IsFeasible 'Default metrics must be infeasible until analysis accepts a candidate.' -Assert-Near 0.0 $metrics.PathLengthMeters 'Default metrics must be zero-valued.' -Assert-Near 0.0 $metrics.MinimumBodyClearanceMeters 'Default metrics must be zero-valued.' -Assert-Near 0.0 $metrics.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 'Default derivative metric must be zero-valued.' -Assert-Near 0.0 $metrics.CurvatureVariationCost 'Curvature variation cost compatibility alias must be available.' -$metricsWithDerivative = [Activator]::CreateInstance($metricsType, @( - $true, - [double]1.0, [double]0.50, [double]0.75, [double]0.0, [double]0.0, - [double]0.0, [double]0.25, [double]0.0, [double]0.0, [double]0.0, [double]0.0)) -Assert-Near 0.75 $metricsWithDerivative.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 'Derivative-aware metrics constructor must retain the peak derivative.' -$diagnostics = [Activator]::CreateInstance($diagnosticsType) -Assert-True ($diagnostics.Metrics -ne $null) 'Default diagnostics must provide quality metrics.' -Assert-Equal 0 $diagnostics.RetryCount 'Default diagnostics must have no retries.' -Assert-Near 0.0 $diagnostics.AcceptedStrength 'Default diagnostics must have zero accepted strength.' -$feasibleMetrics = [Activator]::CreateInstance($metricsType, @( - $true, - [double]1.0, [double]0.0, [double]0.0, [double]0.0, [double]0.0, - [double]0.5, [double]0.0, [double]0.0, [double]0.0, [double]0.0)) -$feasibleDiagnostics = [Activator]::CreateInstance($diagnosticsType, @( - $feasibleMetrics, [TimeSpan]::Zero, 0, [double]1.0, 'test feasible diagnostics')) - -$pointArray = [Array]::CreateInstance($pointType, 1) -$pointArray.SetValue($point, 0) -$segmentArray = [Array]::CreateInstance($segmentType, 2) -$segmentArray.SetValue($segmentA, 0) -$segmentArray.SetValue($segmentB, 1) -$method = [Enum]::Parse($methodType, 'CubicBSpline') -$successMethod = $resultType.GetMethod('Success') -Assert-True ($null -ne $successMethod) 'PathSmoothingResult must expose Success.' -Assert-Throws { $successMethod.Invoke($null, @($method, $pointArray, $segmentArray, $diagnostics)) } 'Success factory must reject diagnostics that are not feasible.' -Assert-Throws { $successMethod.Invoke($null, @($method, $pointArray, $segmentArray, $null)) } 'Success factory must reject null diagnostics.' -Assert-Throws { $successMethod.Invoke($null, @([Enum]::ToObject($methodType, 99), $pointArray, $segmentArray, $feasibleDiagnostics)) } 'Success factory must reject undefined smoothing methods.' -$success = $successMethod.Invoke($null, @($method, $pointArray, $segmentArray, $feasibleDiagnostics)) -Assert-Equal 'Success' $success.Status.ToString() 'Success factory must publish Success status.' -Assert-Equal 'CubicBSpline' $success.Method.ToString() 'Success factory must retain the selected method.' -Assert-Equal 1 $success.Path.Count 'Success factory must publish the provided path.' -Assert-Equal 2 $success.Segments.Count 'Success factory must publish the provided segments.' -Assert-ReadOnlyCollection $success.Path 'Success path must be immutable.' -Assert-ReadOnlyCollection $success.Segments 'Success segments must be immutable.' -$pointArray.SetValue($null, 0) -$segmentArray.SetValue($null, 0) -Assert-True ($null -ne $success.Path[0]) 'Success factory must copy path collections.' -Assert-True ($null -ne $success.Segments[0]) 'Success factory must copy segment collections.' - -$fallbackMethod = $resultType.GetMethod('Fallback') -Assert-True ($null -ne $fallbackMethod) 'PathSmoothingResult must expose Fallback.' -$fallbackPath = [Array]::CreateInstance($pointType, 1) -$fallbackPath.SetValue($point, 0) -$fallbackSegments = [Array]::CreateInstance($segmentType, 1) -$fallbackSegments.SetValue($segmentA, 0) -Assert-Throws { $fallbackMethod.Invoke($null, @($method, $fallbackPath, $fallbackSegments, $diagnostics)) } 'Fallback factory must reject diagnostics that are not feasible.' -Assert-Throws { $fallbackMethod.Invoke($null, @($method, $fallbackPath, $fallbackSegments, $null)) } 'Fallback factory must reject null diagnostics.' -Assert-Throws { $fallbackMethod.Invoke($null, @([Enum]::ToObject($methodType, 99), $fallbackPath, $fallbackSegments, $feasibleDiagnostics)) } 'Fallback factory must reject undefined smoothing methods.' -$fallback = $fallbackMethod.Invoke($null, @($method, $fallbackPath, $fallbackSegments, $feasibleDiagnostics)) -Assert-Equal 'FallbackToCoarsePath' $fallback.Status.ToString() 'Fallback factory must publish an explicit fallback status.' -Assert-Equal 1 $fallback.Path.Count 'Fallback factory must publish a validated fallback path.' -Assert-Equal 0 $fallback.RegionReports.Count 'Legacy fallback results must publish empty immutable region reports.' - -$failureMethod = $resultType.GetMethod('Failure') -Assert-True ($null -ne $failureMethod) 'PathSmoothingResult must expose Failure.' -$failed = $failureMethod.Invoke( - $null, - @([Enum]::Parse($statusType, 'InvalidInput'), - [Activator]::CreateInstance($diagnosticsType))) -Assert-Equal 'InvalidInput' $failed.Status.ToString() 'Failure factory must retain failure status.' -Assert-Equal 0 $failed.Path.Count 'Failure must publish no path.' -Assert-Equal 0 $failed.Segments.Count 'Failure must publish no segments.' -Assert-ReadOnlyCollection $failed.Path 'Failure path must be immutable.' -Assert-ReadOnlyCollection $failed.Segments 'Failure segments must be immutable.' -Assert-Throws { $failureMethod.Invoke($null, @([Enum]::Parse($statusType, 'Success'), $diagnostics)) } 'Failure factory must reject Success.' -Assert-Throws { $failureMethod.Invoke($null, @([Enum]::Parse($statusType, 'FallbackToCoarsePath'), $diagnostics)) } 'Failure factory must reject fallback status.' -Assert-Throws { $failureMethod.Invoke($null, @([Enum]::ToObject($statusType, 99), $diagnostics)) } 'Failure factory must reject undefined statuses.' -Assert-Throws { $successMethod.Invoke($null, @($method, [Array]::CreateInstance($pointType, 0), $fallbackSegments, $diagnostics)) } 'Success factory must reject an empty path.' -Assert-Throws { $successMethod.Invoke($null, @($method, $fallbackPath, [Array]::CreateInstance($segmentType, 0), $diagnostics)) } 'Success factory must reject empty segments.' -Assert-Equal 0 $success.RegionReports.Count 'Legacy success results must publish empty immutable region reports.' -Assert-ReadOnlyCollection $success.RegionReports 'Legacy success region reports must be immutable.' - -$curvatureJumps = [System.Collections.Generic.List[double]]::new() -$curvatureJumps.Add([double]0.20) -$report = [Activator]::CreateInstance($regionReportType, @( - 0, [double]0.0, [double]0.5, $curvatureJumps, - [double]0.5, [double]0.5, [double]0.25, [double]0.25, - 1, 0, - [Enum]::Parse($regionStatusType, 'Improved'), [Enum]::Parse($regionFailureType, 'None'), - [double]1.0, [double]0.5, [double]2.0, [double]1.0, - [double]0.05, [double]0.10, [double]0.80)) -Assert-ReadOnlyCollection $report.CurvatureJumpsPerMeter 'Region report curvature jumps must be immutable.' -$curvatureJumps[0] = [double]9.99 -Assert-Near 0.20 $report.CurvatureJumpsPerMeter[0] 'Region report must copy curvature jumps.' -$retainedReport = [Activator]::CreateInstance($regionReportType, @( - 0, [double]0.0, [double]0.5, $curvatureJumps, - [double]0.5, [double]0.5, [double]0.25, [double]0.25, - 1, 7, - [Enum]::Parse($regionStatusType, 'RetainedOriginal'), [Enum]::Parse($regionFailureType, 'InsufficientImprovement'), - [double]1.0, [double]1.0, [double]2.0, [double]2.0, - [double]0.0, [double]0.10, [double]0.80)) -Assert-Equal -1 $retainedReport.SelectedCandidateIndex 'A region without a selected candidate must publish -1.' -$publishLocalG2Method = $resultType.GetMethod('PublishLocalG2') -Assert-True ($null -ne $publishLocalG2Method) 'PathSmoothingResult must expose PublishLocalG2.' -$reports = [Array]::CreateInstance($regionReportType, 1) -$reports.SetValue($report, 0) -$localG2Method = [Enum]::Parse($methodType, 'LocalG2Quintic') -$complete = [Enum]::Parse($statusType, 'Complete') -$localG2Result = $publishLocalG2Method.Invoke($null, @($complete, $fallbackPath, $fallbackSegments, $feasibleDiagnostics, $reports)) -Assert-Equal 'Complete' $localG2Result.Status.ToString() 'PublishLocalG2 must retain Local G2 publication status.' -Assert-Equal 'LocalG2Quintic' $localG2Result.Method.ToString() 'PublishLocalG2 must publish the Local G2 method.' -Assert-Equal 1 $localG2Result.RegionReports.Count 'PublishLocalG2 must publish region reports.' -Assert-ReadOnlyCollection $localG2Result.RegionReports 'Local G2 result region reports must be immutable.' -$reports.SetValue($null, 0) -Assert-True ($null -ne $localG2Result.RegionReports[0]) 'PublishLocalG2 must copy region reports.' -Assert-Throws { $publishLocalG2Method.Invoke($null, @([Enum]::Parse($statusType, 'Success'), $fallbackPath, $fallbackSegments, $feasibleDiagnostics, $reports)) } 'PublishLocalG2 must reject legacy statuses.' -Assert-Throws { $publishLocalG2Method.Invoke($null, @($complete, [Array]::CreateInstance($pointType, 0), $fallbackSegments, $feasibleDiagnostics, $reports)) } 'PublishLocalG2 must reject an empty path.' - -$requestConstructor = $requestType.GetConstructor(@( - [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarsePointType), - [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarseSegmentType), - $mapType, - $vehicleType, - $configurationType)) -Assert-True ($null -ne $requestConstructor) 'PathSmoothingRequest must expose the public five-argument constructor.' -$boundsType = Get-RequiredType ($mappingRoot + 'MapBoundsMm') -$mapRequestType = Get-RequiredType ($mappingRoot + 'PlanningMapRequest') -$mapFactoryType = Get-RequiredType ($mappingRoot + 'PlanningMapFactory') -$mapRequest = [Activator]::CreateInstance($mapRequestType) -$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]1000, [single]0, [single]1000)) -$mapRequest.ResolutionMm = [single]50 -$mapRequest.AllowExplicitEmptyMap = $true -$map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map -Assert-True ($null -ne $map) 'Contract test must create an explicit empty planning map.' -$requestCoarsePath = [Array]::CreateInstance($coarsePointType, 1) -$requestCoarsePath.SetValue([Activator]::CreateInstance($coarsePointType, @( - [double]0.0, [double]0.0, [double]0.0, [double]0.0, [double]0.0, - $forward, [double]0.0, [double]1.0, $false, - [Enum]::Parse((Get-RequiredType ($coarsePathRoot + 'CoarsePathPointSource')), 'Start'))), 0) -$requestSegments = [Array]::CreateInstance($coarseSegmentType, 1) -$requestSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 0, $false, $false)), 0) -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = [double]0.80 -$vehicle.WidthMeters = [double]0.60 -$vehicle.SafetyMarginMeters = [double]0.05 -$vehicle.MaximumCurvaturePerMeter = [double]0.8333333333333334 -$requestConfiguration = [Activator]::CreateInstance($configurationType) -$request = $requestConstructor.Invoke(@($requestCoarsePath, $requestSegments, $map, $vehicle, $requestConfiguration)) -Assert-ReadOnlyCollection $request.CoarsePath 'Request coarse path must be immutable.' -Assert-ReadOnlyCollection $request.Segments 'Request segments must be immutable.' -$requestCoarsePath.SetValue($null, 0) -$requestSegments.SetValue($null, 0) -$vehicle.LengthMeters = [double]9.99 -$vehicle.WidthMeters = [double]9.99 -$vehicle.SafetyMarginMeters = [double]9.99 -$vehicle.MaximumCurvaturePerMeter = [double]0.1 -$vehicle.MinimumTurningRadiusMeters = [double]9.99 -$requestConfiguration.Method = [Enum]::Parse($methodType, 'PiecewiseQuintic') -$requestConfiguration.OutputSpacingMeters = [double]0.99 -$requestConfiguration.MaximumCollisionCheckStepMeters = [double]0.99 -$requestConfiguration.MinimumClearanceReserveMeters = [double]0.99 -$requestConfiguration.SmoothingStrength = [double]0.99 -$requestConfiguration.AllowFallbackToCoarsePath = $false -$requestConfiguration.CubicBSpline.EndpointTangentScale = [double]0.99 -$requestConfiguration.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.99 -$requestConfiguration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.99 -$requestConfiguration.LocalCubicBezier.HandleLengthRatio = [double]0.99 -$requestConfiguration.PiecewiseQuintic.KnotSpacingMeters = [double]0.99 -$requestConfiguration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.99 -$requestConfiguration.LocalG2Quintic.MinimumWindowLengthMeters = [double]0.99 -$requestConfiguration.LocalG2Quintic.PreferredWindowLengthMeters = [double]0.99 -$requestConfiguration.LocalG2Quintic.MaximumWindowLengthMeters = [double]0.99 -$requestConfiguration.LocalG2Quintic.MaximumDeviationMeters = [double]0.99 -$requestConfiguration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = [double]0.99 -$requestConfiguration.LocalG2Quintic.CurvatureJumpRatioOfMaximum = [double]0.99 -$requestConfiguration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = [double]0.99 -$requestConfiguration.LocalG2Quintic.MaximumVariationCostRegressionRatio = [double]0.99 -$requestConfiguration.LocalG2Quintic.MaximumCandidatesPerRegion = 99 -Assert-True ($null -ne $request.CoarsePath[0]) 'Request must copy the coarse-path collection.' -Assert-True ($null -ne $request.Segments[0]) 'Request must copy the segment collection.' -Assert-Near 0.80 $request.Vehicle.LengthMeters 'Request must snapshot vehicle parameters.' -Assert-Near 0.60 $request.Vehicle.WidthMeters 'Request must snapshot vehicle width.' -Assert-Near 0.05 $request.Vehicle.SafetyMarginMeters 'Request must snapshot vehicle safety margin.' -Assert-Near (1.0 / 1.20) $request.Vehicle.MaximumCurvaturePerMeter 'Request must snapshot nullable vehicle curvature.' -Assert-True ($null -eq $request.Vehicle.MinimumTurningRadiusMeters) 'Request must snapshot nullable vehicle turning radius.' -Assert-Equal 'CubicBSpline' $request.Configuration.Method.ToString() 'Request must snapshot smoothing method.' -Assert-Near 0.025 $request.Configuration.OutputSpacingMeters 'Request must snapshot common configuration.' -Assert-Near 0.025 $request.Configuration.MaximumCollisionCheckStepMeters 'Request must snapshot collision configuration.' -Assert-Near 0.02 $request.Configuration.MinimumClearanceReserveMeters 'Request must snapshot clearance configuration.' -Assert-Near 1.0 $request.Configuration.SmoothingStrength 'Request must snapshot smoothing strength.' -Assert-Equal $true $request.Configuration.AllowFallbackToCoarsePath 'Request must snapshot fallback configuration.' -Assert-Near (1.0 / 3.0) $request.Configuration.CubicBSpline.EndpointTangentScale 'Request must snapshot B-spline options.' -Assert-Near ([Math]::PI / 18.0) $request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians 'Request must snapshot Bezier threshold.' -Assert-Near 0.60 $request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters 'Request must snapshot Bezier window length.' -Assert-Near (1.0 / 3.0) $request.Configuration.LocalCubicBezier.HandleLengthRatio 'Request must snapshot Bezier options.' -Assert-Near 0.50 $request.Configuration.PiecewiseQuintic.KnotSpacingMeters 'Request must snapshot quintic options.' -Assert-Near 0.10 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters 'Request must snapshot quintic minimum spacing.' -Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumWindowLengthMeters 'Request must snapshot Local G2 minimum window.' -Assert-Near 0.50 $request.Configuration.LocalG2Quintic.PreferredWindowLengthMeters 'Request must snapshot Local G2 preferred window.' -Assert-Near 0.80 $request.Configuration.LocalG2Quintic.MaximumWindowLengthMeters 'Request must snapshot Local G2 maximum window.' -Assert-Near 0.10 $request.Configuration.LocalG2Quintic.MaximumDeviationMeters 'Request must snapshot Local G2 maximum deviation.' -Assert-Near 0.001 $request.Configuration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter 'Request must snapshot Local G2 absolute jump floor.' -Assert-Near 0.05 $request.Configuration.LocalG2Quintic.CurvatureJumpRatioOfMaximum 'Request must snapshot Local G2 relative jump threshold.' -Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio 'Request must snapshot Local G2 peak improvement threshold.' -Assert-Near 0.02 $request.Configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio 'Request must snapshot Local G2 variation tolerance.' -Assert-Equal 12 $request.Configuration.LocalG2Quintic.MaximumCandidatesPerRegion 'Request must snapshot Local G2 candidate count.' -$request.Vehicle.WidthMeters = [double]9.99 -$request.Vehicle.SafetyMarginMeters = [double]9.99 -$request.Vehicle.MinimumTurningRadiusMeters = [double]9.99 -$request.Configuration.MaximumCollisionCheckStepMeters = [double]0.99 -$request.Configuration.MinimumClearanceReserveMeters = [double]0.99 -$request.Configuration.SmoothingStrength = [double]0.99 -$request.Configuration.AllowFallbackToCoarsePath = $false -$request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.99 -$request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.99 -$request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.99 -$request.Configuration.LocalG2Quintic.MinimumWindowLengthMeters = [double]0.99 -$request.Configuration.LocalG2Quintic.PreferredWindowLengthMeters = [double]0.99 -$request.Configuration.LocalG2Quintic.MaximumWindowLengthMeters = [double]0.99 -$request.Configuration.LocalG2Quintic.MaximumDeviationMeters = [double]0.99 -$request.Configuration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = [double]0.99 -$request.Configuration.LocalG2Quintic.CurvatureJumpRatioOfMaximum = [double]0.99 -$request.Configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = [double]0.99 -$request.Configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio = [double]0.99 -$request.Configuration.LocalG2Quintic.MaximumCandidatesPerRegion = 99 -Assert-Near 0.60 $request.Vehicle.WidthMeters 'Request vehicle getter must not expose mutable state.' -Assert-Near 0.05 $request.Vehicle.SafetyMarginMeters 'Request vehicle getter must not expose mutable state.' -Assert-True ($null -eq $request.Vehicle.MinimumTurningRadiusMeters) 'Request vehicle getter must not expose mutable nullable state.' -Assert-Near 0.025 $request.Configuration.MaximumCollisionCheckStepMeters 'Request configuration getter must not expose mutable state.' -Assert-Near 0.02 $request.Configuration.MinimumClearanceReserveMeters 'Request configuration getter must not expose mutable state.' -Assert-Near 1.0 $request.Configuration.SmoothingStrength 'Request configuration getter must not expose mutable state.' -Assert-Equal $true $request.Configuration.AllowFallbackToCoarsePath 'Request configuration getter must not expose mutable state.' -Assert-Near ([Math]::PI / 18.0) $request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians 'Request configuration getter must not expose mutable Bezier options.' -Assert-Near 0.60 $request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters 'Request configuration getter must not expose mutable Bezier options.' -Assert-Near 0.10 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters 'Request configuration getter must not expose mutable quintic options.' -Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumWindowLengthMeters 'Request configuration getter must not expose mutable Local G2 minimum window.' -Assert-Near 0.50 $request.Configuration.LocalG2Quintic.PreferredWindowLengthMeters 'Request configuration getter must not expose mutable Local G2 preferred window.' -Assert-Near 0.80 $request.Configuration.LocalG2Quintic.MaximumWindowLengthMeters 'Request configuration getter must not expose mutable Local G2 maximum window.' -Assert-Near 0.10 $request.Configuration.LocalG2Quintic.MaximumDeviationMeters 'Request configuration getter must not expose mutable Local G2 maximum deviation.' -Assert-Near 0.001 $request.Configuration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter 'Request configuration getter must not expose mutable Local G2 absolute jump floor.' -Assert-Near 0.05 $request.Configuration.LocalG2Quintic.CurvatureJumpRatioOfMaximum 'Request configuration getter must not expose mutable Local G2 relative jump threshold.' -Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio 'Request configuration getter must not expose mutable Local G2 peak improvement threshold.' -Assert-Near 0.02 $request.Configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio 'Request configuration getter must not expose mutable Local G2 variation tolerance.' -Assert-Equal 12 $request.Configuration.LocalG2Quintic.MaximumCandidatesPerRegion 'Request configuration getter must not expose mutable Local G2 candidate count.' - -Write-Output 'Path smoothing contract checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_documentation.ps1 b/.task8-sweep/tests/verify_path_smoothing_documentation.ps1 deleted file mode 100644 index b8538e1..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_documentation.ps1 +++ /dev/null @@ -1,65 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$root = Join-Path $PSScriptRoot '..' -$readmePath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\README.md' -$demoPath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\Test\PathSmoothingComparisonDemo.cs' -$runnerPath = Join-Path $PSScriptRoot 'run_path_smoothing_comparison.ps1' - -function Assert-True($actual, [string]$message) { - if (-not $actual) { throw $message } -} - -Assert-True (Test-Path -LiteralPath $readmePath) 'Path smoothing README is missing.' -Assert-True (Test-Path -LiteralPath $demoPath) 'Path smoothing comparison demo is missing.' -Assert-True (Test-Path -LiteralPath $runnerPath) 'Path smoothing comparison batch runner is missing.' - -$readme = Get-Content -Raw -Encoding UTF8 $readmePath -foreach ($section in @( - '## Units and coordinates', - '## Facade usage', - '## Status handling and fallback', - '## Fixture freshness', - '## IEEE colors, fonts, and Windows PNG', - '## SQP boundary', - '## Output files')) { - Assert-True $readme.Contains($section) "README must contain section: $section" -} -foreach ($requiredText in @( - 'meters', - 'radians', - 'PathSmoothingService', - 'FallbackToCoarsePath', - 'SimSun', - 'Times New Roman', - '600 dpi', - 'discrete samples', - '01-coarse-path-overview', - '02-all-paths-comparison', - '03-cubic-bspline-overview', - '04-local-cubic-bezier-overview', - '05-piecewise-quintic-overview', - '06-curvature-comparison', - 'PDF/EPS')) { - Assert-True $readme.Contains($requiredText) "README must document: $requiredText" -} - -$demo = Get-Content -Raw -Encoding UTF8 $demoPath -foreach ($requiredText in @( - 'PathSmoothingComparisonDemo', - 'PathSmoothingService', - 'PathSmoothingComparisonService', - 'SmoothingReportExporter', - 'SmoothingScenarioFactory', - 'PathSmoothingStatus.Success', - 'PathSmoothingStatus.FallbackToCoarsePath')) { - Assert-True $demo.Contains($requiredText) "Demo must use: $requiredText" -} - -$runner = Get-Content -Raw -Encoding UTF8 $runnerPath -Assert-True ($runner -match '\[switch\]\$FixtureOnly') 'Batch runner must offer -FixtureOnly.' -Assert-True $runner.Contains('obj\path_smoothing_reports') 'Batch runner must write reports below ClumsyPilot/obj/path_smoothing_reports.' -Assert-True $runner.Contains('--export-fixtures') 'Batch runner must export all eight fast fixtures by default.' -Assert-True $runner.Contains('if (-not $FixtureOnly)') 'Batch runner must make end-to-end cases optional.' -Assert-True ($runner -notmatch 'ParkrobTrajplanner\\PathSmoothing\\.*\.(svg|png|csv)') 'Batch runner must never write report files below source directories.' - -Write-Output 'Path smoothing documentation checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_fixtures.ps1 b/.task8-sweep/tests/verify_path_smoothing_fixtures.ps1 deleted file mode 100644 index ec883c4..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_fixtures.ps1 +++ /dev/null @@ -1,119 +0,0 @@ -param( - [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json')) - -$ErrorActionPreference = 'Stop' -$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} -function Assert-ThrowsMatching([scriptblock]$Action, [string]$ExpectedPattern, [string]$Message) { - try { - & $Action - } - catch { - $exception = $_.Exception - while ($null -ne $exception.InnerException) { $exception = $exception.InnerException } - if ($exception.Message -match $ExpectedPattern) { return } - throw "$Message ExpectedPattern=$ExpectedPattern Actual=$($exception.Message)" - } - throw "$Message Expected an exception." -} -function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.' -$comparison = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison.' -$loaderType = Get-RequiredType ($root + 'SmoothingScenarioFixtureLoader') -$factoryType = Get-RequiredType ($root + 'SmoothingScenarioFactory') -$fixtureType = Get-RequiredType ($root + 'SmoothingScenarioFixture') -$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') -$preprocessorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing.PathSmoothingPreprocessor' - -$loadMethod = $loaderType.GetMethod('LoadAndVerify', [Type[]]@([string])) -Assert-True ($null -ne $loadMethod) 'Fixture loader must expose LoadAndVerify(string).' -$fixtureRequestsMethod = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) -Assert-True ($null -ne $fixtureRequestsMethod) 'Scenario factory must expose CreateFixtureRequests(string).' -$generateMethod = $assembly.GetType($root + 'SmoothingFixtureGenerator', $true).GetMethod('Generate', [Type[]]@([string], [bool])) -Assert-True ($null -ne $generateMethod) 'Fixture generator must expose Generate(string, bool).' -$tryPrepareMethod = $preprocessorType.GetMethod('TryPrepare') -Assert-True ($null -ne $tryPrepareMethod) 'Fixture paths must be checked through PathSmoothingPreprocessor.TryPrepare.' - -$fixtures = $loadMethod.Invoke($null, @((Resolve-Path $FixturePath).Path)) -$fixtureDocument = Get-Content -Raw -Encoding UTF8 $FixturePath | ConvertFrom-Json -$expected = @( - 'straight', 'single-turn', 's-bend', 'large-heading-change', - 'rectangle-detour', 'multi-obstacle-detour', - 'narrow-corridor', 'forward-reverse-switch') -Assert-Equal $expected.Count $fixtures.Count 'Fixture loader must return exactly eight fast fixtures.' - -$actualIds = @($fixtures | ForEach-Object { $_.Id }) -Assert-Equal ($expected -join ',') ($actualIds -join ',') 'Fixture IDs must be stable and in documented order.' -Assert-Equal $actualIds.Count (@($actualIds | Select-Object -Unique).Count) 'Fixture IDs must be unique.' -foreach ($fixture in $fixtures) { - Assert-True ($fixture -is $fixtureType) 'Loader must return immutable smoothing fixture values.' - Assert-True ($fixture.FixtureVersion -gt 0) "Fixture $($fixture.Id) must carry a positive version." - Assert-True $fixture.IsConfigurationFingerprintCurrent "Fixture $($fixture.Id) must match its stored configuration fingerprint." - Assert-True ($fixture.ConfigurationFingerprint -match '^sha256:[0-9a-f]{64}$') "Fixture $($fixture.Id) must expose a lowercase SHA-256 fingerprint." - Assert-True ($fixture.Path.Count -gt 1) "Fixture $($fixture.Id) must include a coarse path." - Assert-True ($fixture.Segments.Count -gt 0) "Fixture $($fixture.Id) must include direction segments." - foreach ($segment in $fixture.Segments) { - Assert-True ($segment.StartIndex -ge 0 -and $segment.EndIndex -lt $fixture.Path.Count -and $segment.EndIndex -ge $segment.StartIndex) "Fixture $($fixture.Id) must retain valid segment coverage." - } -} -foreach ($record in $fixtureDocument.scenarios) { - Assert-True ($null -ne $record.planningConfiguration) "Fixture $($record.id) must retain the planning configuration that produced its coarse path." -} - -$requests = $fixtureRequestsMethod.Invoke($null, @((Resolve-Path $FixturePath).Path)) -Assert-Equal 8 $requests.Count 'Fast fixtures must create exactly eight comparison requests without Hybrid A*.' -foreach ($request in $requests) { - Assert-True ($request -is $comparisonRequestType) 'Fast fixture factory must create comparison requests.' - $prepareArguments = [object[]]@($request.SmoothingRequest, $null, $null) - $prepared = $tryPrepareMethod.Invoke([Activator]::CreateInstance($preprocessorType), $prepareArguments) - Assert-True $prepared "Fixture path must satisfy the PathSmoothingPreprocessor input contract. Reason=$($prepareArguments[2])" -} - -$loaderSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\SmoothingScenarioFixtureLoader.cs') -Assert-True (-not $loaderSource.Contains('HybridAStarPlanner')) 'Fixture-only loading must not reference HybridAStarPlanner.' -$generatorSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\SmoothingFixtureGenerator.cs') -Assert-True $generatorSource.Contains('CoarsePathPlanningService') 'Fixture generation must snapshot actual successful coarse-path planning outputs.' -Assert-True $generatorSource.Contains('CoarsePathScenarioFactory') 'Fixture generation must use the established coarse-path scenarios where available.' - -function Write-CorruptedFixture([scriptblock]$Mutate) { - $temporaryPath = [IO.Path]::GetTempFileName() - $document = Get-Content -Raw -Encoding UTF8 $FixturePath | ConvertFrom-Json - & $Mutate $document - [IO.File]::WriteAllText($temporaryPath, ($document | ConvertTo-Json -Depth 16), [Text.UTF8Encoding]::new($false)) - return $temporaryPath -} - -$coverageFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].endIndex = 1 } -$directionFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].direction = 1 } -$switchFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].endsAtGearSwitch = $true } -try { - Assert-ThrowsMatching { $loadMethod.Invoke($null, @($coverageFixture)) } 'Fixture path contract' 'Loader must reject segment gaps before accepting the fingerprint.' - Assert-ThrowsMatching { $loadMethod.Invoke($null, @($directionFixture)) } 'Fixture path contract' 'Loader must reject segment direction mismatches before accepting the fingerprint.' - Assert-ThrowsMatching { $loadMethod.Invoke($null, @($switchFixture)) } 'Fixture path contract' 'Loader must reject illegal gear-switch topology before accepting the fingerprint.' -} -finally { - foreach ($temporaryPath in @($coverageFixture, $directionFixture, $switchFixture)) { - if ($temporaryPath -and [IO.File]::Exists($temporaryPath)) { [IO.File]::Delete($temporaryPath) } - } -} - -$generationPath = [IO.Path]::GetTempFileName() -try { - Assert-ThrowsMatching { $generateMethod.Invoke($null, @($generationPath, $false)) } '.+' 'Fixture generation must refuse to overwrite an existing target.' - $generateMethod.Invoke($null, @($generationPath, $true)) - $generatedFixtures = $loadMethod.Invoke($null, @($generationPath)) - Assert-Equal 8 $generatedFixtures.Count 'Generated fixture data must remain loadable and retain all scenarios.' -} -finally { - if ([IO.File]::Exists($generationPath)) { [IO.File]::Delete($generationPath) } -} - -Write-Output 'Path smoothing fixture checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_geometry.ps1 b/.task8-sweep/tests/verify_path_smoothing_geometry.ps1 deleted file mode 100644 index 8aee66e..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_geometry.ps1 +++ /dev/null @@ -1,642 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch { - return - } - throw $Message -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function New-GeometryPoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - [double]$Heading, - [double]$UnwrappedHeading, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($pointType, @( - $X, $Y, $ArcLength, $Heading, $UnwrappedHeading, - [double]1.0, $IsGearSwitch, $anchor)) -} - -function New-DirectionSegment( - [int]$Index, - $Direction, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch)) -} - -function New-DirectionSegmentWithStartCurvature( - [int]$Index, - $Direction, - [double]$StartVehicleCurvature, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch, $StartVehicleCurvature)) -} - -function New-CoarsePoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - $Direction, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($coarsePointType, @( - $X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction, - [double]0.0, [double]1.0, $IsGearSwitch, $coarseAnchor)) -} - -function Invoke-Analysis([object[]]$Segments, [double]$Spacing = 0.05) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - - $arguments = [object[]]@($typedSegments, $Spacing, $null, $null) - $accepted = $analyzeMethod.Invoke($analyzer, $arguments) - $description = [string]::Join(',', @($Segments | ForEach-Object { - "index=$($_.SegmentIndex);direction=$($_.Direction);points=$($_.Points.Count)" - })) - Assert-True $accepted ("Geometry analysis must accept the analytic candidate. Reason=" + $arguments[3] + '; Segments=' + $description) - Assert-True ($null -ne $arguments[2]) 'Successful geometry analysis must return PathGeometryAnalysis.' - return $arguments[2] -} - -function Assert-AnalysisRejected([object[]]$Segments, [string]$Message) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($segmentIndex = 0; $segmentIndex -lt $Segments.Count; $segmentIndex++) { - $typedSegments.SetValue($Segments[$segmentIndex], $segmentIndex) - } - - $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null) - $accepted = $analyzeMethod.Invoke($analyzer, $arguments) - Assert-True (-not $accepted) ($Message + '; Reason=' + $arguments[3]) -} - -function Invoke-RejectedAnalysis([object[]]$Segments, [string]$Message) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - - $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null) - $accepted = $analyzeMethod.Invoke($analyzer, $arguments) - Assert-False $accepted ($Message + '; Reason=' + $arguments[3]) -} - -function New-CoarsePathPoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - $Direction, - [bool]$IsGearSwitch = $false, - [string]$SourceName = 'MotionPrimitive', - [double]$Heading = 0.0, - [double]$VehicleCurvature = 0.0) { - return [Activator]::CreateInstance($coarsePointType, @( - $X, $Y, $Heading, $Heading, $ArcLength, - $Direction, $VehicleCurvature, [double]1.0, $IsGearSwitch, - [Enum]::Parse($coarsePointSourceType, $SourceName))) -} - -function New-EmptyGeometryMap { - $mapRequest = [Activator]::CreateInstance($mapRequestType) - $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $mapRequest.ResolutionMm = [single]50 - $mapRequest.AllowExplicitEmptyMap = $true - $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map - Assert-True ($null -ne $map) 'Geometry test must create an explicit empty planning map.' - return $map -} - -function Invoke-GeometryValidation($Analysis, [object[]]$Segments, $Vehicle) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - - $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]](, $typedSegments)) - $arguments = [object[]]@( - $Analysis.Path, $Analysis.Segments, $preparedPath, (New-EmptyGeometryMap), $Vehicle, [double]0.05, - $null, [double]0.0, $null) - $accepted = $validateMethod.Invoke($validator, $arguments) - return [pscustomobject]@{ Accepted = $accepted; Reason = $arguments[8] } -} - -function New-CircularDirectionSegment( - $Direction, - [double]$Curvature, - [int]$Intervals, - [double]$ChordLength) { - $radius = 1.0 / $Curvature - $headingStep = 2.0 * [Math]::Asin($Curvature * $ChordLength / 2.0) - $points = New-Object System.Collections.Generic.List[object] - for ($index = 0; $index -le $Intervals; $index++) { - $theta = $headingStep * $index - $heading = if ($Direction.ToString() -eq 'Forward') { $theta } else { $theta + [Math]::PI } - [void]$points.Add((New-GeometryPoint ` - (1.0 + $radius * [Math]::Sin($theta)) ` - (1.0 + $radius * (1.0 - [Math]::Cos($theta))) ` - ($index * $ChordLength) $heading $heading)) - } - return New-DirectionSegment 0 $Direction $points.ToArray() -} - -function Get-OldPolylineCurvatureMaximum($Analysis) { - $maximum = 0.0 - $path = $Analysis.Path - for ($index = 0; $index -lt $path.Count; $index++) { - if ($path.Count -eq 1) { - $curvature = 0.0 - } - elseif ($index -eq 0) { - $curvature = ($path[1].UnwrappedHeading - $path[0].UnwrappedHeading) / - ($path[1].ArcLength - $path[0].ArcLength) - } - elseif ($index -eq $path.Count - 1) { - $curvature = ($path[$index].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) / - ($path[$index].ArcLength - $path[$index - 1].ArcLength) - } - else { - $curvature = ($path[$index + 1].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) / - ($path[$index + 1].ArcLength - $path[$index - 1].ArcLength) - } - $maximum = [Math]::Max($maximum, [Math]::Abs($curvature)) - } - return $maximum -} - -function Get-QuinticAnalyticCurvatureMaximum( - [double]$C2, - [double]$C3, - [double]$C4, - [double]$C5, - [int]$ReferenceSamples = 20000) { - $maximum = 0.0 - for ($index = 0; $index -lt $ReferenceSamples; $index++) { - $t = $index / [double]($ReferenceSamples - 1) - $firstDerivative = 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + - 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t - $secondDerivative = 2.0 * $C2 + 6.0 * $C3 * $t + - 12.0 * $C4 * $t * $t + 20.0 * $C5 * $t * $t * $t - $curvature = [Math]::Abs($secondDerivative / [Math]::Pow(1.0 + $firstDerivative * $firstDerivative, 1.5)) - $maximum = [Math]::Max($maximum, $curvature) - } - return $maximum -} - -function New-QuinticDirectionSegment( - [double]$C2, - [double]$C3, - [double]$C4, - [double]$C5, - [double]$DistributionPower, - [int]$Samples = 400) { - $points = New-Object System.Collections.Generic.List[object] - $arcLength = 0.0 - $previousX = 0.0 - $previousY = 0.0 - for ($index = 0; $index -lt $Samples; $index++) { - $t = [Math]::Pow($index / [double]($Samples - 1), $DistributionPower) - $x = 1.0 + $t - $y = $C2 * $t * $t + $C3 * $t * $t * $t + $C4 * $t * $t * $t * $t + $C5 * $t * $t * $t * $t * $t - if ($index -gt 0) { - $arcLength += [Math]::Sqrt(($x - $previousX) * ($x - $previousX) + ($y - $previousY) * ($y - $previousY)) - } - $heading = [Math]::Atan2( - 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t, - 1.0) - [void]$points.Add((New-GeometryPoint $x $y $arcLength $heading $heading)) - $previousX = $x - $previousY = $y - } - return New-DirectionSegment 0 $forward $points.ToArray() -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$processing = $root + 'Processing.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' - -$analyzerType = Get-RequiredType ($processing + 'PathGeometryAnalyzer') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D') -$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') -$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') -$analysisType = Get-RequiredType ($processing + 'PathGeometryAnalysis') -$preprocessorType = Get-RequiredType ($processing + 'PathSmoothingPreprocessor') -$resamplerType = Get-RequiredType ($processing + 'ArcLengthResampler') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') -$coarseSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap' -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') -$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') -$smoothingRequestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$smoothingConfigurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm' -$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest' -$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory' -$validatorType = Get-RequiredType ($root + 'Validation.SmoothedPathValidator') -$rawBaselineBuilderType = Get-RequiredType ($processing + 'RawPathBaselineBuilder') - -Assert-True ($null -ne $preparedPathType) 'PreparedPath must be discoverable for smoothing algorithms.' -Assert-True ($null -ne $preprocessorType) 'PathSmoothingPreprocessor must be discoverable for request preparation.' -Assert-True ($null -ne $resamplerType) 'ArcLengthResampler must be discoverable for deterministic resampling.' -Assert-True ($null -ne $analysisType.GetProperty('MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter')) ` - 'PathGeometryAnalysis must expose peak d-kappa/d-s.' - -$analyzer = [Activator]::CreateInstance($analyzerType) -$analyzeMethod = $analyzerType.GetMethod('TryAnalyze') -Assert-True ($null -ne $analyzeMethod) 'PathGeometryAnalyzer must expose TryAnalyze.' -Assert-Equal 4 $analyzeMethod.GetParameters().Length 'TryAnalyze must accept segments, spacing, analysis, and reason.' -$validator = [Activator]::CreateInstance($validatorType) -$validateMethod = $validatorType.GetMethod('TryValidate') -Assert-True ($null -ne $validateMethod) 'SmoothedPathValidator must expose TryValidate.' -Assert-Equal 9 $validateMethod.GetParameters().Length 'SmoothedPathValidator.TryValidate must retain its public contract.' -$rawBaselineMethod = $rawBaselineBuilderType.GetMethod( - 'TryCreate', - [Reflection.BindingFlags]'Static,NonPublic') -Assert-True ($null -ne $rawBaselineMethod) 'RawPathBaselineBuilder must expose its internal TryCreate path.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anchor = [Enum]::Parse($sourceType, 'Anchor') -$coarseAnchor = [Enum]::Parse($coarseSourceType, 'Start') - -# The chord-corrected estimator must retain an exact circular curvature limit for both travel directions. -$maximumAllowedCurvature = 5.0 / 6.0 -$circleVehicle = [Activator]::CreateInstance($vehicleType) -$circleVehicle.LengthMeters = 0.20 -$circleVehicle.WidthMeters = 0.20 -$circleVehicle.SafetyMarginMeters = 0.0 -$circleVehicle.MaximumCurvaturePerMeter = $maximumAllowedCurvature -foreach ($direction in @($forward, $reverse)) { - $exactLimitCircle = New-CircularDirectionSegment $direction $maximumAllowedCurvature 20 0.05 - $exactLimitAnalysis = Invoke-Analysis @($exactLimitCircle) - foreach ($point in $exactLimitAnalysis.Path) { - Assert-True ([Math]::Abs($point.VehicleCurvature) -le $maximumAllowedCurvature + 1.0e-9) ` - ('An exact-limit ' + $direction + ' circle must not exceed the curvature limit.') - } - $validation = Invoke-GeometryValidation $exactLimitAnalysis @($exactLimitCircle) $circleVehicle - Assert-True $validation.Accepted ('The validator must accept an analyzed exact-limit ' + $direction + ' circle. Reason=' + $validation.Reason) -} - -# An analyzed over-limit circle must remain detectable by the unchanged validator threshold. -$overLimitCircle = New-CircularDirectionSegment $forward ($maximumAllowedCurvature + 0.01) 20 0.05 -$overLimitAnalysis = Invoke-Analysis @($overLimitCircle) -Assert-True ($overLimitAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter -gt $maximumAllowedCurvature + 1.0e-6) ` - 'An over-limit circle must exceed the vehicle curvature limit by more than the validator tolerance.' -$overLimitValidation = Invoke-GeometryValidation $overLimitAnalysis @($overLimitCircle) $circleVehicle -Assert-False $overLimitValidation.Accepted 'The validator must reject an analyzed over-limit circle.' - -# Trusted raw fallback must apply the exact vehicle-curvature gate before reconstruction. -$trustedLimit = 0.80 -$trustedOverLimit = $trustedLimit + 0.0000005 -$analyzedCurvature = $trustedLimit + 0.01 -$chordLength = 0.05 -$headingStep = 2.0 * [Math]::Asin($analyzedCurvature * $chordLength / 2.0) -$radius = 1.0 / $analyzedCurvature -$trustedPoints = [Array]::CreateInstance($coarsePointType, 21) -for ($index = 0; $index -le 20; $index++) { - $theta = $headingStep * $index - $trustedPoints.SetValue((New-CoarsePathPoint ` - (1.0 + $radius * [Math]::Sin($theta)) ` - (1.0 + $radius * (1.0 - [Math]::Cos($theta))) ` - ($index * $chordLength) ` - $forward ` - $false ` - $(if ($index -eq 0) { 'Start' } else { 'MotionPrimitive' }) ` - $theta ` - $trustedOverLimit), $index) -} -$trustedSegments = [Array]::CreateInstance($coarseSegmentType, 1) -$trustedSegments.SetValue( - [Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 20, $false, $false)), - 0) -$trustedVehicle = [Activator]::CreateInstance($vehicleType) -$trustedVehicle.LengthMeters = 0.20 -$trustedVehicle.WidthMeters = 0.20 -$trustedVehicle.SafetyMarginMeters = 0.0 -$trustedVehicle.MaximumCurvaturePerMeter = $trustedLimit -$trustedConfiguration = [Activator]::CreateInstance($smoothingConfigurationType) -$trustedRequest = [Activator]::CreateInstance($smoothingRequestType, @( - $trustedPoints, - $trustedSegments, - (New-EmptyGeometryMap), - $trustedVehicle, - $trustedConfiguration)) -$trustedPreprocessor = [Activator]::CreateInstance($preprocessorType) -$trustedPrepareMethod = $preprocessorType.GetMethod('TryPrepare') -$trustedPrepareArguments = [object[]]@($trustedRequest, $null, $null) -Assert-True $trustedPrepareMethod.Invoke($trustedPreprocessor, $trustedPrepareArguments) ` - ('Trusted raw fallback regression must prepare successfully. Reason=' + $trustedPrepareArguments[2]) -$trustedBaselineArguments = [object[]]@( - $trustedRequest, - $trustedPrepareArguments[1], - $analyzer, - $trustedConfiguration.OutputSpacingMeters, - $validator, - $trustedConfiguration.MaximumCollisionCheckStepMeters, - $null, - $null) -Assert-False $rawBaselineMethod.Invoke($null, $trustedBaselineArguments) ` - 'Trusted raw fallback must not publish a coarse curvature above the exact vehicle maximum.' -Assert-Equal $overLimitValidation.Reason $trustedBaselineArguments[7] ` - 'Rejecting trusted raw fallback must preserve the original analyzed-path validation failure.' - -# The chord-corrected estimator must not under-estimate these smooth references more than the former polyline estimator. -foreach ($quinticCase in @( - [pscustomobject]@{ Name = 'SBend'; C2 = 0.0; C3 = 0.30; C4 = -0.45; C5 = 0.18; Power = 1.0 }, - [pscustomobject]@{ Name = 'EndpointPeak'; C2 = 0.18; C3 = -0.12; C4 = 0.0; C5 = 0.0; Power = 1.0 }, - [pscustomobject]@{ Name = 'NonUniformFinalInterval'; C2 = -0.12; C3 = 0.36; C4 = -0.30; C5 = 0.08; Power = 1.7 })) { - $quintic = New-QuinticDirectionSegment $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 $quinticCase.Power - $quinticAnalysis = Invoke-Analysis @($quintic) - $analyticMaximum = Get-QuinticAnalyticCurvatureMaximum $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 - $newDeficit = [Math]::Max(0.0, $analyticMaximum - $quinticAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter) - $oldDeficit = [Math]::Max(0.0, $analyticMaximum - (Get-OldPolylineCurvatureMaximum $quinticAnalysis)) - Assert-True ($newDeficit -le $oldDeficit + 1.0e-6) ` - ($quinticCase.Name + ' must not have greater one-sided curvature under-estimation than the old polyline estimator.') -} - -# A half-turn over a single chord has no unambiguous geometric curvature estimate. -$ambiguousTurn = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 ([Math]::PI) ([Math]::PI))) -Assert-AnalysisRejected @($ambiguousTurn) 'A two-point heading turn of π must be rejected as ambiguous.' - -# Forward straight: resampling is exactly 0.05 m, preserves the exact endpoint, and has zero curvature. -$straight = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -$straightAnalysis = Invoke-Analysis @($straight) -Assert-Equal 21 $straightAnalysis.Path.Count 'A one-metre straight must produce twenty 0.05 m intervals plus the initial point.' -Assert-Near 0.0 $straightAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 0.000000001 ` - 'A straight must have zero peak d-kappa/d-s.' -foreach ($point in $straightAnalysis.Path) { - Assert-Near 0.0 $point.VehicleCurvatureDerivative 0.000000001 ` - 'A straight point must carry zero d-kappa/d-s.' -} -for ($index = 1; $index -lt $straightAnalysis.Path.Count; $index++) { - $left = $straightAnalysis.Path[$index - 1] - $right = $straightAnalysis.Path[$index] - $distance = [Math]::Sqrt(($right.X - $left.X) * ($right.X - $left.X) + ($right.Y - $left.Y) * ($right.Y - $left.Y)) - Assert-Near 0.05 $distance 0.000000001 'Straight resampling intervals must be exactly 0.05 m.' - Assert-Near 0.0 $right.GeometricCurvature 0.000000001 'A forward straight must have zero geometric curvature.' - Assert-Near 0.0 $right.VehicleCurvature 0.000000001 'A forward straight must have zero vehicle curvature.' -} -$straightEnd = $straightAnalysis.Path[$straightAnalysis.Path.Count - 1] -Assert-Near 1.0 $straightEnd.X 0.0 'Resampling must retain the exact final X coordinate.' -Assert-Near 0.0 $straightEnd.Y 0.0 'Resampling must retain the exact final Y coordinate.' - -# Raw coarse anchors carry the vehicle pose, which may differ slightly from the chord tangent of a finite integration step. -$poseAnchoredCurve = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.2 1.0 0.4 0.4)) -$poseAnchoredAnalysis = Invoke-Analysis @($poseAnchoredCurve) -Assert-Near 0.0 $poseAnchoredAnalysis.Path[0].Heading 0.000000000001 'Geometry analysis must retain the first coarse-anchor heading rather than replace it with a chord tangent.' -$poseAnchoredEnd = $poseAnchoredAnalysis.Path[$poseAnchoredAnalysis.Path.Count - 1] -Assert-Near 0.4 $poseAnchoredEnd.Heading 0.000000000001 'Geometry analysis must retain the final coarse-anchor heading rather than replace it with a chord tangent.' - -# A forward R=2 quarter circle has positive +0.5 1/m vehicle curvature. -$forwardArcPoints = New-Object System.Collections.Generic.List[object] -for ($index = 0; $index -le 32; $index++) { - $theta = ([Math]::PI / 2.0) * $index / 32.0 - $x = 2.0 * [Math]::Sin($theta) - $y = 2.0 * (1.0 - [Math]::Cos($theta)) - $arcLength = 2.0 * $theta - [void]$forwardArcPoints.Add((New-GeometryPoint $x $y $arcLength $theta $theta)) -} -$forwardArc = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() -$forwardArcAnalysis = Invoke-Analysis @($forwardArc) -$forwardArcMidpoint = $forwardArcAnalysis.Path[[int]($forwardArcAnalysis.Path.Count / 2)] -Assert-Near 0.5 $forwardArcMidpoint.GeometricCurvature 0.01 'An R=2 quarter circle must have geometric curvature +0.5 1/m.' -Assert-Near 0.5 $forwardArcMidpoint.VehicleCurvature 0.01 'A forward R=2 quarter circle must have vehicle curvature +0.5 1/m.' - -# The same spatial R=2 circle in reverse retains geometric curvature but negates vehicle curvature. -$reverseArc = New-DirectionSegment 0 $reverse $forwardArcPoints.ToArray() -$reverseArcAnalysis = Invoke-Analysis @($reverseArc) -$reverseArcMidpoint = $reverseArcAnalysis.Path[[int]($reverseArcAnalysis.Path.Count / 2)] -Assert-Near 0.5 $reverseArcMidpoint.GeometricCurvature 0.01 'Reverse travel must not change geometric curvature.' -Assert-Near -0.5 $reverseArcMidpoint.VehicleCurvature 0.01 'A reverse R=2 quarter circle must have vehicle curvature -0.5 1/m.' - -# Gear-switch poses are intentionally duplicated: they keep equal arc length and never enter a derivative denominator. -$forwardBeforeSwitch = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true)) $false $true -$reverseAfterSwitch = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true), - (New-GeometryPoint 0.0 0.0 2.0 0.0 0.0)) $true $false -$switchAnalysis = Invoke-Analysis @($forwardBeforeSwitch, $reverseAfterSwitch) -$firstSegment = $switchAnalysis.Segments[0] -$secondSegment = $switchAnalysis.Segments[1] -$switchLeft = $switchAnalysis.Path[$firstSegment.EndIndex] -$switchRight = $switchAnalysis.Path[$secondSegment.StartIndex] -Assert-Near $switchLeft.X $switchRight.X 0.0 'Gear-switch endpoints must retain duplicate X coordinates.' -Assert-Near $switchLeft.Y $switchRight.Y 0.0 'Gear-switch endpoints must retain duplicate Y coordinates.' -Assert-Near $switchLeft.ArcLength $switchRight.ArcLength 0.0 'Gear-switch endpoints must retain duplicate arc length.' -Assert-Equal 'Forward' $switchLeft.Direction.ToString() 'The first gear-switch pose must retain its forward segment direction.' -Assert-Equal 'Reverse' $switchRight.Direction.ToString() 'The second gear-switch pose must retain its reverse segment direction.' -Assert-True (-not [double]::IsNaN($switchLeft.GeometricCurvature)) 'No derivative may cross the gear-switch duplicate point.' -Assert-True (-not [double]::IsNaN($switchRight.GeometricCurvature)) 'No reverse derivative may cross the gear-switch duplicate point.' -Assert-True (-not [double]::IsNaN($switchLeft.VehicleCurvatureDerivative)) ` - 'The forward side of a gear switch must have a finite one-sided derivative.' -Assert-True (-not [double]::IsNaN($switchRight.VehicleCurvatureDerivative)) ` - 'The reverse side of a gear switch must have a finite one-sided derivative.' - -# Curvature at a curved segment's end and the following straight reverse segment's start must remain independently differentiated. -$forwardArcToSwitch = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() $false $true -$reverseStraightAfterArc = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 2.0 2.0 0.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0) $true), - (New-GeometryPoint 2.0 1.0 1.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0))) $true $false -$curveSwitchAnalysis = Invoke-Analysis @($forwardArcToSwitch, $reverseStraightAfterArc) -$reverseStraightStart = $curveSwitchAnalysis.Path[$curveSwitchAnalysis.Segments[1].StartIndex] -Assert-Near 0.0 $reverseStraightStart.GeometricCurvature 0.000000001 'A gear-switch must not use the preceding curve to differentiate a reverse straight segment.' -Assert-Near 0.0 $reverseStraightStart.VehicleCurvatureDerivative 0.000000001 ` - 'No curvature derivative may cross from the preceding forward arc into a reverse straight.' - -$physicalStart = New-DirectionSegmentWithStartCurvature 0 $forward 0.20 @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -$physicalStartAnalysis = Invoke-Analysis @($physicalStart) -Assert-Near 0.20 $physicalStartAnalysis.Path[0].VehicleCurvature 0.000000001 ` - 'The unified analyzer must retain a real start steering-curvature boundary state.' -Assert-True ($physicalStartAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter -gt 0.0) ` - 'A real start-curvature mismatch must remain visible to the quality analyzer.' - -# A boundary that claims a gear switch must be a duplicated pose with opposite direction; discontinuities are rejected. -$invalidSwitch = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 1.2 0.0 1.0 0.0 0.0 $true), - (New-GeometryPoint 0.2 0.0 2.0 0.0 0.0)) $true $false -Assert-AnalysisRejected @($forwardBeforeSwitch, $invalidSwitch) 'A discontinuous gear-switch boundary must be rejected.' -$trailingGearSwitch = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) $false $true -Assert-AnalysisRejected @($trailingGearSwitch) 'The final direction segment must not advertise a non-existent trailing gear switch.' - -# The public preprocessor receives a raw coarse path and resets every prepared direction segment to local arc length zero. -$coarsePoints = [Array]::CreateInstance($coarsePointType, 4) -$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 0.0 $forward), 0) -$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $forward), 1) -$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $reverse $true), 2) -$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 2.0 $reverse), 3) -$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 1, $false, $true)), 0) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 2, 3, $true, $false)), 1) -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = 1.0 -$vehicle.WidthMeters = 0.5 -$vehicle.SafetyMarginMeters = 0.0 -$vehicle.MaximumCurvaturePerMeter = 1.0 -$configuration = [Activator]::CreateInstance($configurationType) -$uninitializedMap = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($mapType) -$request = [Activator]::CreateInstance($requestType, @($coarsePoints, $coarseSegments, $uninitializedMap, $vehicle, $configuration)) -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareMethod = $preprocessorType.GetMethod('TryPrepare') -$prepareArguments = [object[]]@($request, $null, $null) -$prepared = $prepareMethod.Invoke($preprocessor, $prepareArguments) -Assert-True $prepared ('Preprocessor must accept a legal forward/reverse raw coarse path. Reason=' + $prepareArguments[2]) -Assert-Near 0.0 $prepareArguments[1].Segments[1].Points[0].ArcLength 0.0 'Every prepared direction segment must begin at local arc length zero.' -Assert-True $prepareArguments[1].Segments[1].Points[0].IsGearSwitchPoint 'The reverse prepared segment must retain its gear-switch point.' - -# Finite coordinates can still overflow distance arithmetic; public resampling must reject them instead of emitting NaN/Infinity. -$overflowSegment = New-DirectionSegment 0 $forward @( - (New-GeometryPoint -1.0e308 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0e308 0.0 1.0 0.0 0.0)) -$resampler = [Activator]::CreateInstance($resamplerType) -$segmentResampleMethod = @($resamplerType.GetMethods() | Where-Object { - $_.Name -eq 'TryResample' -and $_.GetParameters()[0].ParameterType -eq $segmentType -})[0] -$resampleArguments = [object[]]@($overflowSegment, [double]0.05, $null, $null) -$resampled = $segmentResampleMethod.Invoke($resampler, $resampleArguments) -Assert-True (-not $resampled) ('Resampling must reject a distance overflow. Reason=' + $resampleArguments[3]) - -# A prepared path must reject null direction segments rather than silently dropping them during flattening. -$nullSegmentArray = [Array]::CreateInstance($segmentType, 1) -$nullSegmentRejected = $false -try { [void][Activator]::CreateInstance($preparedPathType, @($nullSegmentArray)) } catch { $nullSegmentRejected = $true } -Assert-True $nullSegmentRejected 'PreparedPath must reject a null direction segment.' - -# Unwrapped heading must not jump by 2π when tangents cross the -π/π branch cut. -$crossing = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)), - (New-GeometryPoint -1.0 ([Math]::Tan(10.0 * [Math]::PI / 180.0)) 1.015 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)), - (New-GeometryPoint -2.0 0.0 2.03 (-170.0 * [Math]::PI / 180.0) (-170.0 * [Math]::PI / 180.0))) -$crossingAnalysis = Invoke-Analysis @($crossing) -for ($index = 1; $index -lt $crossingAnalysis.Path.Count; $index++) { - $difference = [Math]::Abs($crossingAnalysis.Path[$index].UnwrappedHeading - $crossingAnalysis.Path[$index - 1].UnwrappedHeading) - Assert-True ($difference -lt [Math]::PI) 'Unwrapped headings must remain continuous across the ±π branch cut.' -} - -# The request preprocessor must reset arc length independently for every direction segment, -# while retaining the duplicated pose that represents a legal forward-to-reverse gear switch. -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareMethod = $preprocessorType.GetMethod('TryPrepare') -Assert-True ($null -ne $prepareMethod) 'PathSmoothingPreprocessor must expose TryPrepare.' -$coarsePath = [Array]::CreateInstance($coarsePointType, 5) -$coarsePath.SetValue((New-CoarsePathPoint 0.0 0.0 0.0 $forward $false 'Start'), 0) -$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 1.0 $forward), 1) -$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $forward), 2) -$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $reverse $true), 3) -$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 3.0 $reverse), 4) -$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 2, $false, $true)), 0) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 3, 4, $true, $false)), 1) -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = [double]0.80 -$vehicle.WidthMeters = [double]0.60 -$vehicle.SafetyMarginMeters = [double]0.05 -$vehicle.MaximumCurvaturePerMeter = [double]0.80 -$configuration = [Activator]::CreateInstance($smoothingConfigurationType) -$smoothingRequest = [Activator]::CreateInstance($smoothingRequestType, @( - $coarsePath, $coarseSegments, (New-EmptyGeometryMap), $vehicle, $configuration)) -$prepareArguments = [object[]]@($smoothingRequest, $null, $null) -Assert-True $prepareMethod.Invoke($preprocessor, $prepareArguments) ('Preprocessor must accept legal forward/reverse topology. Reason=' + $prepareArguments[2]) -$preparedPath = $prepareArguments[1] -Assert-Equal 2 $preparedPath.Segments.Count 'Preprocessor must preserve both direction segments.' -Assert-Near 0.0 $preparedPath.Segments[1].Points[0].ArcLength 0.0 'The reverse segment must restart local arc length at zero.' -Assert-True $preparedPath.Segments[1].Points[0].IsGearSwitchPoint 'The duplicate reverse gear-switch point must be retained.' - -# Segments may meet only at a paired, coincident forward/reverse gear switch. -$illegalGearJump = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 1.25 0.0 1.0 0.0 0.0 $true), - (New-GeometryPoint 0.25 0.0 2.0 0.0 0.0)) $true $false -Invoke-RejectedAnalysis @($forwardBeforeSwitch, $illegalGearJump) 'A gear-switch boundary whose poses differ must be rejected.' -$illegalNormalBoundary = New-DirectionSegment 1 $forward @( - (New-GeometryPoint 2.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 3.0 0.0 1.0 0.0 0.0)) $false $false -Invoke-RejectedAnalysis @($straight, $illegalNormalBoundary) 'A non-gear segment boundary must be rejected.' - -# Finite endpoint coordinates can still overflow while computing their separation; reject before interpolation. -$resampler = [Activator]::CreateInstance($resamplerType) -$resamplePointsMethod = $resamplerType.GetMethods() | Where-Object { - $_.Name -eq 'TryResample' -and $_.GetParameters().Length -eq 4 -and - $_.GetParameters()[0].ParameterType -eq [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($pointType) -} | Select-Object -First 1 -Assert-True ($null -ne $resamplePointsMethod) 'ArcLengthResampler must expose point-list TryResample.' -$hugePoints = [Array]::CreateInstance($pointType, 2) -$hugeCoordinate = [double]::MaxValue / 2.0 -$hugePoints.SetValue((New-GeometryPoint (-$hugeCoordinate) 0.0 0.0 0.0 0.0), 0) -$hugePoints.SetValue((New-GeometryPoint $hugeCoordinate 0.0 1.0 0.0 0.0), 1) -$resampleArguments = [object[]]@($hugePoints, [double]0.05, $null, $null) -Assert-False $resamplePointsMethod.Invoke($resampler, $resampleArguments) 'Resampling must reject an infinite geometric distance caused by finite coordinates.' - -# PreparedPath is an all-or-nothing immutable topology snapshot: null direction segments are invalid. -$nullPreparedSegments = [Array]::CreateInstance($segmentType, 1) -Assert-Throws { [Activator]::CreateInstance($preparedPathType, @($nullPreparedSegments)) } 'PreparedPath must reject null direction segments.' - -# Segment indices are deliberately dense and equal to their position in the candidate array. -$sparseSegment = New-DirectionSegment 2 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -Invoke-RejectedAnalysis @($sparseSegment) 'Prepared direction-segment indices must match their dense array position.' - -Write-Output 'Path smoothing geometry checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_integration.ps1 b/.task8-sweep/tests/verify_path_smoothing_integration.ps1 deleted file mode 100644 index e6895b0..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_integration.ps1 +++ /dev/null @@ -1,140 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} -function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } - -function Assert-CoarsePathGeometry($ExpectedPath, $ActualPath, $Map, [string]$ScenarioName) { - Assert-Equal $ExpectedPath.Count $ActualPath.Count "Raw comparison request $ScenarioName must preserve every coarse-path point." - for ($index = 0; $index -lt $ExpectedPath.Count; $index++) { - $expected = $ExpectedPath[$index] - $actual = $ActualPath[$index] - Assert-Near $expected.X $actual.X "Raw comparison request $ScenarioName point $index must preserve X." - Assert-Near $expected.Y $actual.Y "Raw comparison request $ScenarioName point $index must preserve Y." - Assert-Near $expected.Heading $actual.Heading "Raw comparison request $ScenarioName point $index must preserve heading." - Assert-Near $expected.UnwrappedHeading $actual.UnwrappedHeading "Raw comparison request $ScenarioName point $index must preserve unwrapped heading." - Assert-Near $expected.ArcLength $actual.ArcLength "Raw comparison request $ScenarioName point $index must preserve arc length." - Assert-Equal $expected.Direction $actual.Direction "Raw comparison request $ScenarioName point $index must preserve travel direction." - Assert-Near $expected.VehicleCurvature $actual.VehicleCurvature "Raw comparison request $ScenarioName point $index must preserve vehicle curvature." - $expectedClearance = $expected.BodyClearance - if ([double]::IsPositiveInfinity($expectedClearance)) { - $widthMeters = ($Map.Bounds.XMax - $Map.Bounds.XMin) / 1000.0 - $heightMeters = ($Map.Bounds.YMax - $Map.Bounds.YMin) / 1000.0 - $expectedClearance = [Math]::Sqrt($widthMeters * $widthMeters + $heightMeters * $heightMeters) - } - Assert-Near $expectedClearance $actual.BodyClearance "Raw comparison request $ScenarioName point $index must preserve or normalize clearance for smoothing." - Assert-Equal $expected.IsGearSwitchPoint $actual.IsGearSwitchPoint "Raw comparison request $ScenarioName point $index must preserve gear-switch flag." - Assert-Equal $expected.Source $actual.Source "Raw comparison request $ScenarioName point $index must preserve point source." - } -} - -function Assert-SegmentTopology($ExpectedSegments, $ActualPath, $ActualSegments, [string]$Description) { - Assert-Equal $ExpectedSegments.Count $ActualSegments.Count "$Description must preserve segment count." - $expectedStartIndex = 0 - for ($index = 0; $index -lt $ActualSegments.Count; $index++) { - $expected = $ExpectedSegments[$index] - $actual = $ActualSegments[$index] - Assert-Equal $index $actual.SegmentIndex "$Description segment $index must retain its stable index." - Assert-Equal $expected.Direction $actual.Direction "$Description segment $index must preserve travel direction." - Assert-Equal $expected.StartsAtGearSwitch $actual.StartsAtGearSwitch "$Description segment $index must preserve start gear-switch topology." - Assert-Equal $expected.EndsAtGearSwitch $actual.EndsAtGearSwitch "$Description segment $index must preserve end gear-switch topology." - Assert-Equal $expectedStartIndex $actual.StartIndex "$Description segment $index must start directly after the prior segment." - Assert-True ($actual.EndIndex -ge $actual.StartIndex -and $actual.EndIndex -lt $ActualPath.Count) "$Description segment $index must cover valid path indices." - Assert-Equal $actual.StartsAtGearSwitch $ActualPath[$actual.StartIndex].IsGearSwitchPoint "$Description segment $index start flag must match its path point." - for ($pointIndex = $actual.StartIndex; $pointIndex -le $actual.EndIndex; $pointIndex++) { - Assert-Equal $actual.Direction $ActualPath[$pointIndex].Direction "$Description segment $index may not contain mixed directions." - } - $hasNext = $index + 1 -lt $ActualSegments.Count - $expectedEndSwitch = $hasNext -and $ActualPath[$actual.EndIndex + 1].IsGearSwitchPoint - Assert-Equal $expectedEndSwitch $actual.EndsAtGearSwitch "$Description segment $index end flag must match the next gear switch." - $expectedStartIndex = $actual.EndIndex + 1 - } - Assert-Equal $ActualPath.Count $expectedStartIndex "$Description segments must cover every path point." -} - -function Assert-GearSwitchGeometry($ExpectedPath, $ActualPath, [string]$Description) { - $expectedSwitches = @($ExpectedPath | Where-Object { $_.IsGearSwitchPoint }) - $actualSwitches = @($ActualPath | Where-Object { $_.IsGearSwitchPoint }) - Assert-Equal $expectedSwitches.Count $actualSwitches.Count "$Description must preserve gear-switch count." - for ($index = 0; $index -lt $expectedSwitches.Count; $index++) { - Assert-Near $expectedSwitches[$index].X $actualSwitches[$index].X "$Description gear switch $index must preserve X." - Assert-Near $expectedSwitches[$index].Y $actualSwitches[$index].Y "$Description gear switch $index must preserve Y." - Assert-Near $expectedSwitches[$index].Heading $actualSwitches[$index].Heading "$Description gear switch $index must preserve heading." - Assert-Equal $expectedSwitches[$index].Direction $actualSwitches[$index].Direction "$Description gear switch $index must preserve direction." - } -} - -function Assert-RawBaselinePath($ExpectedPath, $ActualPath, [string]$ScenarioName) { - Assert-Equal $ExpectedPath.Count $ActualPath.Count "Raw baseline $ScenarioName must preserve every coarse-path point." - for ($index = 0; $index -lt $ExpectedPath.Count; $index++) { - $expected = $ExpectedPath[$index] - $actual = $ActualPath[$index] - Assert-Near $expected.X $actual.X "Raw baseline $ScenarioName point $index must preserve X." - Assert-Near $expected.Y $actual.Y "Raw baseline $ScenarioName point $index must preserve Y." - Assert-Near $expected.Heading $actual.Heading "Raw baseline $ScenarioName point $index must preserve heading." - Assert-Near $expected.UnwrappedHeading $actual.UnwrappedHeading "Raw baseline $ScenarioName point $index must preserve unwrapped heading." - Assert-Near $expected.ArcLength $actual.ArcLength "Raw baseline $ScenarioName point $index must preserve arc length." - Assert-Equal $expected.Direction $actual.Direction "Raw baseline $ScenarioName point $index must preserve travel direction." - Assert-Near $expected.VehicleCurvature $actual.VehicleCurvature "Raw baseline $ScenarioName point $index must preserve vehicle curvature." - Assert-Equal $expected.IsGearSwitchPoint $actual.IsGearSwitchPoint "Raw baseline $ScenarioName point $index must preserve gear-switch topology." - } -} - -$coarse = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$coarseFacade = $coarse + 'Facade.' -$coarseTest = $coarse + 'Test.' -$smoothingTest = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.' -$smoothingFacade = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade.' -$comparison = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison.' - -$coarseServiceType = Get-RequiredType ($coarseFacade + 'CoarsePathPlanningService') -$coarseJobType = Get-RequiredType ($coarseFacade + 'CoarsePathPlanningJob') -$coarseResultType = Get-RequiredType ($coarseFacade + 'CoarsePathPlanningJobResult') -$scenarioType = Get-RequiredType ($coarseTest + 'CoarsePathTestScenario') -$coarseFactoryType = Get-RequiredType ($coarseTest + 'CoarsePathScenarioFactory') -$scenarioFactoryType = Get-RequiredType ($smoothingTest + 'SmoothingScenarioFactory') -$comparisonServiceType = Get-RequiredType ($smoothingFacade + 'PathSmoothingComparisonService') -$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') - -$coarseCreate = $coarseFactoryType.GetMethod('Create', [Type[]]@($scenarioType)) -$coarsePlan = $coarseServiceType.GetMethod('Plan', [Type[]]@($coarseJobType, [Threading.CancellationToken])) -$createComparisonRequest = $scenarioFactoryType.GetMethod('CreateEndToEndRequest', [Type[]]@($coarseJobType, $coarseResultType)) -$compare = $comparisonServiceType.GetMethod('Compare', [Type[]]@($comparisonRequestType, [Threading.CancellationToken])) -Assert-True ($null -ne $createComparisonRequest) 'Smoothing scenario factory must convert a successful coarse planning job into a comparison request.' - -$planner = [Activator]::CreateInstance($coarseServiceType) -$comparisonService = [Activator]::CreateInstance($comparisonServiceType) -foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { - $scenario = [Enum]::Parse($scenarioType, $scenarioName) - $job = $coarseCreate.Invoke($null, @($scenario)) - $planningTimer = [Diagnostics.Stopwatch]::StartNew() - $planned = $coarsePlan.Invoke($planner, @($job, [Threading.CancellationToken]::None)) - $planningTimer.Stop() - Assert-Equal 'Success' $planned.PlanningResult.Status.ToString() "Coarse scenario $scenarioName must succeed before smoothing comparison." - $request = $createComparisonRequest.Invoke($null, @($job, $planned)) - Assert-CoarsePathGeometry $planned.PlanningResult.Path $request.SmoothingRequest.CoarsePath $planned.MapResult.Map $scenarioName - $comparisonTimer = [Diagnostics.Stopwatch]::StartNew() - $comparisonResult = $compare.Invoke($comparisonService, @($request, [Threading.CancellationToken]::None)) - $comparisonTimer.Stop() - Assert-True (-not $comparisonResult.IsCancelled) "Comparison $scenarioName must not be cancelled." - Assert-Equal 'Success' $comparisonResult.RawPathBaseline.Status.ToString() "Raw baseline $scenarioName must remain a feasible, verified copy of the coarse path." - Assert-RawBaselinePath $planned.PlanningResult.Path $comparisonResult.RawPathBaseline.Path $scenarioName - Assert-SegmentTopology $planned.PlanningResult.Segments $comparisonResult.RawPathBaseline.Path $comparisonResult.RawPathBaseline.Segments "Raw baseline $scenarioName" - Assert-GearSwitchGeometry $planned.PlanningResult.Path $comparisonResult.RawPathBaseline.Path "Raw baseline $scenarioName" - foreach ($entry in $comparisonResult.Entries | Where-Object { $_.Status.ToString() -eq 'Success' }) { - Assert-SegmentTopology $comparisonResult.RawPathBaseline.Segments $entry.Path $entry.Segments "Successful $($entry.Method) $scenarioName" - Assert-GearSwitchGeometry $comparisonResult.RawPathBaseline.Path $entry.Path "Successful $($entry.Method) $scenarioName" - } - $successfulEntryCount = @($comparisonResult.Entries | Where-Object { $_.Status.ToString() -eq 'Success' }).Count - Write-Output ("$scenarioName diagnostics: planning=$([Math]::Round($planningTimer.Elapsed.TotalSeconds, 3))s; comparison=$([Math]::Round($comparisonTimer.Elapsed.TotalSeconds, 3))s; successfulMethods=$successfulEntryCount") -} - -Write-Output 'Path smoothing end-to-end integration checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 deleted file mode 100644 index e0c5720..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 +++ /dev/null @@ -1,252 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Get-InternalProperty($Instance, [string]$Name) { - return $Instance.GetType().GetProperty( - $Name, - [Reflection.BindingFlags]'Public,NonPublic,Instance').GetValue($Instance) -} - -function Get-InternalMethod($Type, [string]$Name) { - return @($Type.GetMethods([Reflection.BindingFlags]'Public,NonPublic,Instance,Static') | - Where-Object Name -eq $Name)[0] -} - -function New-InternalInstance($Type) { - return [Activator]::CreateInstance( - $Type, - [Reflection.BindingFlags]'Instance,NonPublic,Public', - $null, - @(), - $null) -} - -$builderType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2CandidateBuilder' -$hooksType = $builderType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $hooksType) 'LocalG2CandidateBuilder must expose narrowly scoped deterministic TestHooks.' -$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $executeMethod) 'TestHooks must execute deterministic candidate scenarios.' - -function Invoke-Scenario([string]$Scenario) { - return $executeMethod.Invoke($null, @($Scenario)) -} - -$isolated = Invoke-Scenario 'Isolated' -Assert-True ($isolated.CandidateCount -gt 0 -and $isolated.CandidateCount -le 12) ` - 'An isolated event must produce a bounded non-empty candidate set.' -Assert-Near 0.0 $isolated.StartPositionError 1e-9 'Window start position must match.' -Assert-Near 0.0 $isolated.EndPositionError 1e-9 'Window end position must match.' -Assert-Near 0.0 $isolated.StartCurvatureError 1e-8 'Window start curvature must match.' -Assert-Near 0.0 $isolated.EndCurvatureError 1e-8 'Window end curvature must match.' -Assert-True $isolated.ContainsLocalG2Source 'Generated samples must identify their source.' - -$cluster = Invoke-Scenario 'Cluster' -Assert-Equal 1 $cluster.OutputRegionCount 'Overlapping events must be built as one region.' -Assert-True $cluster.InternalConnectionsAreG2 'Internal anchors must share tangent and curvature.' - -$reverse = Invoke-Scenario 'Reverse' -Assert-Equal 'Reverse' $reverse.Direction 'Reverse candidates must preserve segment direction.' -Assert-True $reverse.VehicleAndGeometricCurvatureSignsAreOpposite ` - 'Reverse candidates must convert vehicle curvature to geometric curvature exactly once.' - -$spliced = Invoke-Scenario 'Spliced' -Assert-True $spliced.NoDuplicateNonGearPoints 'Splicing must remove duplicate boundary samples.' -Assert-True $spliced.EndpointsUnchanged 'Splicing must preserve the complete segment endpoints.' - -$startBoundary = Invoke-Scenario 'StartBoundary' -Assert-True ($startBoundary.CandidateCount -gt 0) 'A start-boundary event must build a one-sided candidate.' -Assert-Near 0.0 $startBoundary.StartCurvatureError 1e-8 ` - 'A start-boundary candidate must use the preserved physical start vehicle curvature.' - -$interiorDerivative = Invoke-Scenario 'InteriorStationaryCurve' -Assert-True $interiorDerivative.Rejected ` - 'A curve with a stationary interior derivative must be rejected even when its endpoint chord is short.' - -$constantVelocity = Invoke-Scenario 'ConstantVelocityCurve' -Assert-True $constantVelocity.Accepted ` - 'A nonstationary constant-velocity curve with collinear derivative controls must be accepted.' - -$exactSplice = Invoke-Scenario 'ExactSpliceEndpoints' -Assert-True $exactSplice.EndpointsAreExact ` - 'Splicing must write interpolated exact endpoints instead of accepting approximate candidate endpoints.' - -$gearBoundary = Invoke-Scenario 'GearBoundary' -Assert-True $gearBoundary.GearBoundaryMarkerPreserved ` - 'A window touching a gear-switch segment boundary must preserve its point-level gear marker.' - -$twoRegions = Invoke-Scenario 'TwoRegionWorkOrder' -Assert-True $twoRegions.WorkOrderDescending ` - 'Same-segment regions must be processed from larger original local arc to smaller local arc.' -Assert-True $twoRegions.FrontArcPreservedAfterBackReplacement ` - 'Replacing the back region must preserve the front region original arc coordinates.' -Assert-True $twoRegions.BothReplacementsRetained ` - 'Back-then-front replacement must retain the exact LocalG2Transition interiors at (0.75, 0.20) and (2.25, 0.20).' -Assert-True $twoRegions.ForwardOrderRejected ` - 'The regression fixture must prove that front-first invalidates the original back absolute arc.' -Assert-True $twoRegions.DeterministicWorkOrder ` - 'Work ordering must repeat exactly without mutating report order.' -Assert-True $twoRegions.DeterministicReplacementGeometry ` - 'Repeated back-then-front replacement must preserve count and every point X, Y, source, and direction.' -Assert-True $twoRegions.InvalidWorkOrderRejected ` - 'Work ordering must reject null regions and cross-segment event contents.' - -$ordering = Invoke-Scenario 'MultiSegmentWorkOrder' -Assert-True $ordering.SegmentOrderAscending ` - 'Region work ordering must process segment indices in ascending order.' -Assert-True $ordering.EqualArcUsesReportOrder ` - 'Equal first-local-arc regions must retain their original report-index order.' - -$evaluatorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2CandidateEvaluator' -$evaluatorHooksType = $evaluatorType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $evaluatorHooksType) 'LocalG2CandidateEvaluator must expose narrowly scoped deterministic TestHooks.' -$evaluateMethod = $evaluatorHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $evaluateMethod) 'TestHooks must execute deterministic candidate quality-gate scenarios.' - -function Invoke-EvaluationScenario([string]$Scenario) { - return $evaluateMethod.Invoke($null, @($Scenario)) -} - -$tooFar = Invoke-EvaluationScenario 'TooFar' -Assert-Equal 'DeviationExceeded' $tooFar.FailureReason ` - 'A collision-free candidate more than 0.10 m from the raw window must be rejected.' - -$overshoot = Invoke-EvaluationScenario 'Overshoot' -Assert-Equal 'CurvatureOvershoot' $overshoot.FailureReason ` - 'A candidate outside the raw regional curvature range must be rejected.' - -$noOp = Invoke-EvaluationScenario 'NoOp' -Assert-Equal 'InsufficientImprovement' $noOp.FailureReason ` - 'A safe no-op must not be accepted.' - -$oscillating = Invoke-EvaluationScenario 'Oscillating' -Assert-Equal 'VariationCostRegression' $oscillating.FailureReason ` - 'Repeated curvature oscillation must fail the variation-cost gate.' - -$lowClearance = Invoke-EvaluationScenario 'LowClearance' -Assert-Equal 'InsufficientClearance' $lowClearance.FailureReason ` - 'A path with less than 0.02 m checked clearance must be rejected.' - -$improved = Invoke-EvaluationScenario 'Improved' -Assert-Equal 'Accepted' $improved.Status ` - 'A safe candidate with at least 20 percent peak improvement must be accepted.' - -$smallestDeviation = Invoke-EvaluationScenario 'SmallestDeviation' -$best = Invoke-EvaluationScenario 'Best' -Assert-Equal $smallestDeviation.CandidateIndex $best.CandidateIndex ` - 'Among sufficient candidates, minimum deviation must win before extra smoothness.' - -# Exercise the real builder → evaluator seam that a hand-built TestHook candidate does not cover. -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$factoryType = Get-RequiredType ($root + 'Test.SmoothingScenarioFactory') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$methodType = Get-RequiredType ($root + 'SmoothingMethod') -$preprocessorType = Get-RequiredType ($root + 'Processing.PathSmoothingPreprocessor') -$optionsType = Get-RequiredType ($root + 'LocalG2.LocalG2OptionsSnapshot') -$detectorType = Get-RequiredType ($root + 'LocalG2.CurvatureTransitionDetector') -$plannerType = Get-RequiredType ($root + 'LocalG2.LocalG2WindowPlanner') -$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json' -$fixtures = (Get-InternalMethod $factoryType 'CreateFixtureRequests').Invoke($null, @((Resolve-Path $fixturePath).Path)) -$baseRequest = $fixtures[1].SmoothingRequest # single-turn -$configuration = [Activator]::CreateInstance($configurationType) -$configuration.Method = [Enum]::Parse($methodType, 'LocalG2Quintic') -$singleTurnRequest = [Activator]::CreateInstance($requestType, @( - $baseRequest.CoarsePath, $baseRequest.Segments, $baseRequest.Map, $baseRequest.Vehicle, $configuration)) -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareArgs = [object[]]@($singleTurnRequest, $null, $null) -Assert-True ((Get-InternalMethod $preprocessorType 'TryPrepare').Invoke($preprocessor, $prepareArgs)) ` - 'SingleTurn must prepare before evaluating Local G2 candidates.' -$preparedPath = $prepareArgs[1] -$options = [Activator]::CreateInstance($optionsType, [Reflection.BindingFlags]'Instance,NonPublic,Public', $null, @($configuration), $null) -$detector = New-InternalInstance $detectorType -$detectArgs = [object[]]@($singleTurnRequest, [double]$singleTurnRequest.Vehicle.MaximumCurvaturePerMeter, $options, $null, $null) -Assert-True ((Get-InternalMethod $detectorType 'TryDetect').Invoke($detector, $detectArgs)) ` - 'SingleTurn must detect its Local G2 transition.' -$planner = New-InternalInstance $plannerType -$planArgs = [object[]]@($preparedPath, $detectArgs[3], $options, $null, $null) -Assert-True ((Get-InternalMethod $plannerType 'TryPlan').Invoke($planner, $planArgs)) ` - 'SingleTurn must plan a Local G2 region.' -$region = $null -foreach ($plannedRegion in $planArgs[3]) { $region = $plannedRegion; break } -$preparedSegment = (Get-InternalProperty $preparedPath 'Segments')[(Get-InternalProperty $region 'SegmentIndex')] -$realBuilder = New-InternalInstance $builderType -$realCandidates = (Get-InternalMethod $builderType 'Build').Invoke($realBuilder, @( - $preparedSegment, $region, [double]$configuration.OutputSpacingMeters, $options, [Threading.CancellationToken]::None)) -Assert-True ($realCandidates.Count -gt 0) 'SingleTurn must build real Local G2 candidates.' -$realEvaluator = New-InternalInstance $evaluatorType -$duplicateFailures = @() -$acceptedCandidates = @() -$extractWindow = $evaluatorType.GetMethod('TryExtractWindow', [Reflection.BindingFlags]'NonPublic,Static') -$analyzeWindow = $evaluatorType.GetMethod('TryAnalyzeWindow', [Reflection.BindingFlags]'NonPublic,Instance') -foreach ($realCandidate in $realCandidates) { - $evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @( - $preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options, [Threading.CancellationToken]::None)) - $failureReason = Get-InternalProperty $evaluation 'FailureReason' - if ($failureReason -eq 2) { - $candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex' - $duplicateFailures += $candidateIndex - } - if (Get-InternalProperty $evaluation 'Accepted') { - $acceptedCandidates += (Get-InternalProperty $realCandidate 'CandidateIndex') - } - $windowArgs = [object[]]@($preparedSegment, - (Get-InternalProperty $realCandidate 'StartArcLengthMeters'), - (Get-InternalProperty $realCandidate 'EndArcLengthMeters'), $null, $null) - $null = $extractWindow.Invoke($null, $windowArgs) - $rawWindowArgs = [object[]]::new(5) - $rawWindowArgs[0] = $preparedSegment - $rawWindowArgs[1] = $windowArgs[3] - $rawWindowArgs[2] = [double]$configuration.OutputSpacingMeters - $candidateWindowArgs = [object[]]::new(5) - $candidateWindowArgs[0] = $preparedSegment - $candidateWindowArgs[1] = $realCandidate.GetType().GetProperty( - 'RegionPoints', [Reflection.BindingFlags]'Public,NonPublic,Instance').GetValue($realCandidate) - $candidateWindowArgs[2] = [double]$configuration.OutputSpacingMeters - $null = $analyzeWindow.Invoke($realEvaluator, $rawWindowArgs) - $null = $analyzeWindow.Invoke($realEvaluator, $candidateWindowArgs) - $rawPath = Get-InternalProperty $rawWindowArgs[3] 'Path' - $rawMinimum = [double]::PositiveInfinity - $rawMaximum = [double]::NegativeInfinity - foreach ($point in $rawPath) { - $rawMinimum = [Math]::Min($rawMinimum, $point.VehicleCurvature) - $rawMaximum = [Math]::Max($rawMaximum, $point.VehicleCurvature) - } - $rawPeak = Get-InternalProperty $rawWindowArgs[3] 'MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter' - $candidatePeak = Get-InternalProperty $candidateWindowArgs[3] 'MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter' - $candidateMaximum = Get-InternalProperty $candidateWindowArgs[3] 'MaximumAbsoluteVehicleCurvaturePerMeter' - $candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex' - $fraction = @(0.00, 0.10, 0.20)[[Math]::Floor($candidateIndex / 4)] - $scale = @(1.00, 1.20, 1.40, 1.60)[$candidateIndex % 4] - Write-Output ('SWEEP candidate={0}; alpha={1:F2}; internalScale={2:F2}; window={3:R}..{4:R}; result={5}; rawK=[{6:R},{7:R}]; maxK={8:R}; rawPeak={9:R}; peak={10:R}' -f - $candidateIndex, $fraction, $scale, - (Get-InternalProperty $realCandidate 'StartArcLengthMeters'), (Get-InternalProperty $realCandidate 'EndArcLengthMeters'), - $failureReason, $rawMinimum, $rawMaximum, $candidateMaximum, $rawPeak, $candidatePeak) -} -Assert-Equal 0 $duplicateFailures.Count ` - 'SingleTurn builder candidates must not fail evaluator window analysis due to duplicate or degenerate points.' -Assert-True ($acceptedCandidates.Count -gt 0) ` - 'SingleTurn must produce at least one builder candidate accepted by the unchanged evaluator gates.' - -Write-Output 'Path smoothing Local G2 candidate checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 deleted file mode 100644 index 2187dc4..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Assert-Rejected($Result, [string]$Message) { - if ($Result.Accepted) { throw $Message } - Assert-True (-not [string]::IsNullOrWhiteSpace($Result.Reason)) 'Rejected curves must provide a stable reason.' -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch [ArgumentOutOfRangeException] { - return - } - catch [System.Reflection.TargetInvocationException] { - Assert-True ($_.Exception.InnerException -is [ArgumentOutOfRangeException]) $Message - return - } - throw $Message -} - -$curveType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.QuinticHermiteCurve2D', $true) -$tryCreate = $curveType.GetMethod('TryCreate', [Reflection.BindingFlags]'Static,NonPublic') -$evaluate = $curveType.GetMethod('Evaluate', [Reflection.BindingFlags]'Instance,NonPublic') -Assert-True ($null -ne $tryCreate) 'QuinticHermiteCurve2D must expose its internal TryCreate factory.' -Assert-True ($null -ne $evaluate) 'QuinticHermiteCurve2D must expose its internal Evaluate method.' - -function Invoke-Curve( - [double]$X0, [double]$Y0, [double]$Dx0, [double]$Dy0, [double]$Ddx0, [double]$Ddy0, - [double]$X1, [double]$Y1, [double]$Dx1, [double]$Dy1, [double]$Ddx1, [double]$Ddy1) { - $arguments = [object[]]@($X0, $Y0, $Dx0, $Dy0, $Ddx0, $Ddy0, $X1, $Y1, $Dx1, $Dy1, $Ddx1, $Ddy1, $null, $null) - $accepted = $tryCreate.Invoke($null, $arguments) - Assert-True $accepted "Quintic Hermite curve creation must succeed. Reason=$($arguments[13])" - return $arguments[12] -} - -function Invoke-InvalidCurve([string]$CaseName) { - $arguments = switch ($CaseName) { - 'ZeroFirstDerivative' { [object[]]@(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 0.0, 0.0, $null, $null) } - 'NonFiniteInput' { [object[]]@(0.0, 0.0, [double]::NaN, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 0.0, 0.0, $null, $null) } - default { throw "Unknown invalid curve case: $CaseName" } - } - return [PSCustomObject]@{ - Accepted = $tryCreate.Invoke($null, $arguments) - Reason = [string]$arguments[13] - } -} - -function Invoke-Evaluate($Curve, [double]$U) { - $arguments = [object[]]@($U, $null, $null, $null, $null, $null, $null) - $evaluate.Invoke($Curve, $arguments) - return [PSCustomObject]@{ - X = [double]$arguments[1]; Y = [double]$arguments[2] - Dx = [double]$arguments[3]; Dy = [double]$arguments[4] - Ddx = [double]$arguments[5]; Ddy = [double]$arguments[6] - } -} - -function Get-Curvature($Sample) { - return ($Sample.Dx * $Sample.Ddy - $Sample.Dy * $Sample.Ddx) / - [Math]::Pow($Sample.Dx * $Sample.Dx + $Sample.Dy * $Sample.Dy, 1.5) -} - -function Assert-Boundary($Expected, $Actual, [string]$Name) { - Assert-Near $Expected.X $Actual.X 1e-10 "$Name X must match." - Assert-Near $Expected.Y $Actual.Y 1e-10 "$Name Y must match." - Assert-Near $Expected.Dx $Actual.Dx 1e-10 "$Name dX must match." - Assert-Near $Expected.Dy $Actual.Dy 1e-10 "$Name dY must match." - Assert-Near $Expected.Ddx $Actual.Ddx 1e-10 "$Name ddX must match." - Assert-Near $Expected.Ddy $Actual.Ddy 1e-10 "$Name ddY must match." -} - -$straight = Invoke-Curve 0 0 1 0 0 0 2 0 1 0 0 0 -$straightStart = Invoke-Evaluate $straight 0.0 -$straightEnd = Invoke-Evaluate $straight 1.0 -$straightMid = Invoke-Evaluate $straight 0.5 -Assert-Boundary ([PSCustomObject]@{ X = 0.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.0 }) $straightStart 'Straight start' -Assert-Boundary ([PSCustomObject]@{ X = 2.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.0 }) $straightEnd 'Straight end' -Assert-Near 0.0 $straightMid.Y 1e-12 'A straight Hermite curve must remain on the axis.' - -$curved = Invoke-Curve 0 0 1 0 0 0.4 1 1 0 1 -0.4 0 -$curvedStart = Invoke-Evaluate $curved 0.0 -$curvedEnd = Invoke-Evaluate $curved 1.0 -Assert-Boundary ([PSCustomObject]@{ X = 0.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.4 }) $curvedStart 'Curved start' -Assert-Boundary ([PSCustomObject]@{ X = 1.0; Y = 1.0; Dx = 0.0; Dy = 1.0; Ddx = -0.4; Ddy = 0.0 }) $curvedEnd 'Curved end' -Assert-Near 0.4 (Get-Curvature $curvedStart) 1e-10 'Start curvature must match boundary derivatives.' -Assert-Near 0.4 (Get-Curvature $curvedEnd) 1e-10 'End curvature must match boundary derivatives.' - -$allFinite = $true -foreach ($u in @(0.0, 0.25, 0.5, 0.75, 1.0)) { - $sample = Invoke-Evaluate $curved $u - foreach ($value in @($sample.X, $sample.Y, $sample.Dx, $sample.Dy, $sample.Ddx, $sample.Ddy)) { - $allFinite = $allFinite -and (-not [double]::IsNaN($value)) -and (-not [double]::IsInfinity($value)) - } -} -Assert-True $allFinite 'All sampled values must be finite.' - -Assert-Rejected (Invoke-InvalidCurve 'ZeroFirstDerivative') ` - 'A curve endpoint with zero first derivative must be rejected.' -Assert-Rejected (Invoke-InvalidCurve 'NonFiniteInput') ` - 'A curve with a non-finite boundary value must be rejected.' -Assert-Throws { Invoke-Evaluate $curved -0.001 } 'Evaluate must reject parameters below zero.' -Assert-Throws { Invoke-Evaluate $curved 1.001 } 'Evaluate must reject parameters above one.' - -Write-Output 'Local G2 quintic curve checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 deleted file mode 100644 index 7b54982..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 +++ /dev/null @@ -1,129 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -$detectorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.CurvatureTransitionDetector' -$hooksType = $detectorType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $hooksType) 'CurvatureTransitionDetector must expose its narrowly scoped nested TestHooks helper.' -$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $executeMethod) 'TestHooks must expose deterministic scenario execution for reflection tests.' - -function Invoke-Scenario([string]$Scenario) { - return $executeMethod.Invoke($null, @($Scenario)) -} - -# Same direction: 0 -> 0.4167 is detected once. -$singleTransition = Invoke-Scenario 'SingleTransition' -Assert-Equal 1 $singleTransition.TransitionCount 'One primitive curvature jump must be detected.' -Assert-Near 0.4167 $singleTransition.MaximumJump 0.0001 'The jump magnitude must be retained.' - -# Same pose at a forward/reverse boundary: no event crosses the stop. -$gearSwitch = Invoke-Scenario 'GearSwitch' -Assert-Equal 0 $gearSwitch.TransitionCount 'A stopped gear switch must not be a smoothing event.' - -# A 0.01 1/m numerical change is below max(0.001, 5% of 0.8333). -$noise = Invoke-Scenario 'Noise' -Assert-Equal 0 $noise.TransitionCount 'Sub-threshold curvature noise must be ignored.' - -# Two 0.50 m windows whose ranges overlap are merged. -$overlap = Invoke-Scenario 'Overlap' -Assert-Equal 1 $overlap.RegionCount 'Overlapping windows must form one joint region.' -Assert-Equal 2 $overlap.TransitionCountInFirstRegion 'The merged region must retain both events.' - -# Near a segment start, the window becomes asymmetric without crossing the hard boundary. -$nearStart = Invoke-Scenario 'NearStart' -Assert-Near 0.0 $nearStart.StartArcLength 0.000000001 'A start window must be clamped to the segment.' -Assert-True ($nearStart.RightWindowLength -gt $nearStart.LeftWindowLength) ` - 'Unavailable left length must be shifted to the right.' - -# Finite endpoint curvatures may still overflow during subtraction; detection must fail rather than publish infinity. -$coarsePathPointType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.CoarsePathPoint' -$pathSegmentType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.PathSegment' -$directionType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.TravelDirection' -$coarseSourceType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.CoarsePathPointSource' -$requestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingRequest' -$configurationType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingConfiguration' -$optionsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2OptionsSnapshot' -$forward = [Enum]::Parse($directionType, 'Forward') -$motionPrimitive = [Enum]::Parse($coarseSourceType, 'MotionPrimitive') -$overflowPoints = [Array]::CreateInstance($coarsePathPointType, 2) -$overflowPoints.SetValue([Activator]::CreateInstance($coarsePathPointType, @( - [double]0.0, [double]0.0, [double]0.0, [double]0.0, [double]0.0, $forward, - -[double]::MaxValue, [double]1.0, $false, $motionPrimitive)), 0) -$overflowPoints.SetValue([Activator]::CreateInstance($coarsePathPointType, @( - [double]0.1, [double]0.0, [double]0.0, [double]0.0, [double]0.1, $forward, - [double]::MaxValue, [double]1.0, $false, $motionPrimitive)), 1) -$overflowSegments = [Array]::CreateInstance($pathSegmentType, 1) -$overflowSegments.SetValue([Activator]::CreateInstance($pathSegmentType, @(0, $forward, 0, 1, $false, $false)), 0) -$configuration = [Activator]::CreateInstance($configurationType) -$optionsConstructor = $optionsType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, @($configurationType), $null) -Assert-True ($null -ne $optionsConstructor) 'Local G2 options must be constructible for detector reflection tests.' -$options = $optionsConstructor.Invoke(@($configuration)) -$overflowRequest = [Activator]::CreateInstance($requestType, @($overflowPoints, $overflowSegments, $null, $null, $configuration)) -$detector = [Activator]::CreateInstance($detectorType, $true) -$tryDetect = $detectorType.GetMethod('TryDetect', [Reflection.BindingFlags]'Instance,NonPublic') -Assert-True ($null -ne $tryDetect) 'CurvatureTransitionDetector must retain its internal TryDetect contract.' -$overflowArguments = [object[]]@($overflowRequest, [double]0.8333, $options, $null, $null) -$overflowAccepted = $tryDetect.Invoke($detector, $overflowArguments) -Assert-False $overflowAccepted 'Curvature subtraction overflow must reject detection.' -Assert-True (-not [string]::IsNullOrWhiteSpace([string]$overflowArguments[4])) ` - 'Rejected overflow detection must provide a stable reason.' -Assert-Equal 0 $overflowArguments[3].Count 'Rejected overflow detection must not publish a curvature event.' - -$plannerType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2WindowPlanner' -$plannerHooksType = $plannerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $plannerHooksType) 'LocalG2WindowPlanner must expose narrow deterministic TestHooks.' -$planScenario = $plannerHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') - -$separated = $planScenario.Invoke($null, @('SeparatedByOneMeter')) -Assert-Equal 2 $separated.RegionCount 'Events 1.0 m apart cannot share a 0.80 m total window.' -Assert-Equal '1,1' $separated.TransitionCounts 'Separated events must remain one event per region.' - -$mergeable = $planScenario.Invoke($null, @('Mergeable')) -Assert-Equal 1 $mergeable.RegionCount 'Events covered by one legal total window must merge.' -Assert-Equal '2' $mergeable.TransitionCounts 'The merged region must retain both events.' - -$partition = $planScenario.Invoke($null, @('ThreeEventPartition')) -Assert-Equal 2 $partition.RegionCount 'Three events must split at the first infeasible joint window.' -Assert-Equal '2,1' $partition.TransitionCounts 'Only a feasible consecutive subgroup may merge.' - -$boundary = $planScenario.Invoke($null, @('NearBoundary')) -Assert-True ($boundary.FirstRightLength -gt $boundary.FirstLeftLength) ` - 'A boundary-clamped total window must transfer missing length to the available side.' -Assert-True ($boundary.MaximumWindowLength -le 0.80 + 1e-9) ` - 'No candidate window may exceed 0.80 m total length.' - -foreach ($snapshot in @($separated, $mergeable, $partition, $boundary)) { - Assert-True $snapshot.ExactEnvelope 'Region envelope must equal the extrema of actual legal variants.' - Assert-True ($snapshot.MaximumWindowLength -le 0.80 + 1e-9) ` - 'MaximumWindowLengthMeters is a total, not a per-side length.' -} - -$repeat = $planScenario.Invoke($null, @('ThreeEventPartition')) -Assert-Equal $partition.Signature $repeat.Signature ` - 'Repeated planning must preserve grouping, candidate numbering and variant order.' - -Write-Output 'Path smoothing Local G2 detection checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 deleted file mode 100644 index 0a9aab4..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 +++ /dev/null @@ -1,136 +0,0 @@ -param( - [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') -) - -$ErrorActionPreference = 'Stop' -$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { - throw "$Message Expected=$Expected Actual=$Actual" - } -} -function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$facade = $root + 'Facade.' -$test = $root + 'Test.' -$serviceType = Get-RequiredType ($facade + 'PathSmoothingService') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$methodType = Get-RequiredType ($root + 'SmoothingMethod') -$factoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory') -$localG2 = [Enum]::Parse($methodType, 'LocalG2Quintic') -$service = [Activator]::CreateInstance($serviceType) -$smooth = $serviceType.GetMethod('Smooth', [Type[]]@($requestType, [Threading.CancellationToken])) -$createFixtures = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) -Assert-True ($null -ne $smooth) 'Local G2 must use the public service entrypoint.' - -function New-LocalG2Request($BaseRequest, [scriptblock]$Configure = $null) { - $configuration = [Activator]::CreateInstance($configurationType) - $configuration.Method = $localG2 - $configuration.AllowFallbackToCoarsePath = $true - if ($null -ne $Configure) { & $Configure $configuration } - return [Activator]::CreateInstance($requestType, @( - $BaseRequest.CoarsePath, $BaseRequest.Segments, $BaseRequest.Map, - $BaseRequest.Vehicle, $configuration)) -} - -function Invoke-LocalG2($Request, [Threading.CancellationToken]$CancellationToken = [Threading.CancellationToken]::None) { - return $smooth.Invoke($service, @($Request, $CancellationToken)) -} - -function Assert-PublishedPath($Result, [string]$Name) { - Assert-True ($Result.Path.Count -gt 0) "$Name must publish a complete path." - Assert-Equal 0 $Result.Segments[0].StartIndex "$Name segments must start at zero." - Assert-Equal ($Result.Path.Count - 1) $Result.Segments[-1].EndIndex "$Name segments must cover the path." - Assert-True $Result.Diagnostics.Metrics.IsFeasible "$Name diagnostics must be feasible." - Assert-True ($Result.Diagnostics.Metrics.MinimumBodyClearanceMeters -ge 0.02) "$Name must retain the configured clearance reserve." -} - -function Assert-Equivalent($First, $Second, [string]$Name) { - Assert-Equal $First.Status $Second.Status "$Name must retain deterministic status." - Assert-Equal $First.Path.Count $Second.Path.Count "$Name must retain deterministic point count." - Assert-Equal $First.RegionReports.Count $Second.RegionReports.Count "$Name must retain deterministic report count." - for ($index = 0; $index -lt $First.Path.Count; $index++) { - Assert-Near $First.Path[$index].X $Second.Path[$index].X "$Name point $index must retain X." - Assert-Near $First.Path[$index].Y $Second.Path[$index].Y "$Name point $index must retain Y." - } - for ($index = 0; $index -lt $First.RegionReports.Count; $index++) { - Assert-Equal $First.RegionReports[$index].SelectedCandidateIndex $Second.RegionReports[$index].SelectedCandidateIndex "$Name report $index must retain candidate selection." - Assert-Equal $First.RegionReports[$index].Status $Second.RegionReports[$index].Status "$Name report $index must retain status." - Assert-Near $First.RegionReports[$index].StartArcLengthMeters $Second.RegionReports[$index].StartArcLengthMeters "$Name report $index must retain order." - } -} - -$fixtures = $createFixtures.Invoke($null, @((Resolve-Path $FixturePath).Path)) -$expected = @{ - 'straight' = @('NotNeeded') - 'single-turn' = @('Complete') - 'large-heading-change' = @('Complete') - 's-bend' = @('Complete', 'PartialImprovement') - 'rectangle-detour' = @('Complete', 'PartialImprovement') - 'multi-obstacle-detour' = @('Complete', 'PartialImprovement') - 'forward-reverse-switch' = @('Complete', 'PartialImprovement', 'NotNeeded') -} - -$results = @{} -foreach ($fixture in $fixtures) { - $request = New-LocalG2Request $fixture.SmoothingRequest - $first = Invoke-LocalG2 $request - $second = Invoke-LocalG2 $request - $scenario = $fixture.SmoothingRequest.CoarsePath[0].Source.ToString() # fixture index is resolved below by stable input order - $results[$results.Count] = $first - Write-Output ("Fixture $($results.Count - 1): status=$($first.Status); reports=$($first.RegionReports.Count); reason=$($first.Diagnostics.Reason)") - foreach ($report in $first.RegionReports) { - Write-Output (" region=$($report.Status)/$($report.FailureReason); candidates=$($report.CandidateCount); selected=$($report.SelectedCandidateIndex)") - } - Assert-Equivalent $first $second "Fixture $($results.Count - 1)" -} - -$fixtureIds = @('straight', 'single-turn', 's-bend', 'large-heading-change', 'rectangle-detour', 'multi-obstacle-detour', 'narrow-corridor', 'forward-reverse-switch') -for ($index = 0; $index -lt $fixtureIds.Count; $index++) { - $id = $fixtureIds[$index] - if (-not $expected.ContainsKey($id)) { continue } - $result = $results[$index] - Assert-True ($expected[$id] -contains $result.Status.ToString()) "$id must publish its required Local G2 status." - Assert-PublishedPath $result $id - if ($result.Status.ToString() -in @('Complete', 'PartialImprovement')) { - Assert-True (@($result.RegionReports | Where-Object { $_.Status.ToString() -eq 'Improved' }).Count -gt 0) "$id must report an accepted region." - } - if ($result.Status.ToString() -eq 'PartialImprovement') { - Assert-True (@($result.RegionReports | Where-Object { $_.Status.ToString() -eq 'RetainedOriginal' }).Count -gt 0) "$id must report retained regions." - } -} - -# The multi-obstacle fixture is a same-direction two-window integration case. Its two replacements -# change length, so processing it front-to-back would invalidate the later original arc window. -$twoRegion = $results[5] -Assert-True ($twoRegion.RegionReports.Count -ge 2) 'Two-region integration fixture must publish both detector regions.' -Assert-Equal 'Improved' $twoRegion.RegionReports[0].Status.ToString() 'First detector-order two-region report must improve.' -Assert-Equal 'Improved' $twoRegion.RegionReports[1].Status.ToString() 'Second detector-order two-region report must improve.' -Assert-True ($twoRegion.RegionReports[0].StartArcLengthMeters -lt $twoRegion.RegionReports[1].StartArcLengthMeters) 'Reports must retain detector ascending arc order.' -Assert-True (@($twoRegion.Path | Where-Object { $_.Source.ToString() -eq 'LocalG2Transition' }).Count -gt 2) 'Both Local G2 replacements must remain in the final path.' - -$strictRequest = New-LocalG2Request $fixtures[1].SmoothingRequest { - param($configuration) - $configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = 0.99 -} -$unchanged = Invoke-LocalG2 $strictRequest -Assert-Equal 'Unchanged' $unchanged.Status.ToString() 'Detected events with no accepted region must publish a verified raw path as Unchanged.' -Assert-PublishedPath $unchanged 'Unchanged Local G2' - -$cancelSource = [Threading.CancellationTokenSource]::new() -$cancelSource.Cancel() -$cancelled = Invoke-LocalG2 (New-LocalG2Request $fixtures[1].SmoothingRequest) $cancelSource.Token -Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancellation must propagate through Local G2.' -Assert-Equal 0 $cancelled.Path.Count 'A cancelled run must not publish a partial path.' - -Write-Output 'Local G2 service integration checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_png.ps1 b/.task8-sweep/tests/verify_path_smoothing_png.ps1 deleted file mode 100644 index b5e526c..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_png.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -param( - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') -) - -$ErrorActionPreference = 'Stop' - -$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' -if (-not (Test-Path -LiteralPath $hostProject)) { - throw "Path smoothing PNG verification host was not found: $hostProject" -} - -$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path -& dotnet run --project $hostProject --no-restore -- $resolvedFixturePath -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} - -Write-Output 'Path smoothing PNG checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_quintic.ps1 b/.task8-sweep/tests/verify_path_smoothing_quintic.ps1 deleted file mode 100644 index 3a0f210..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_quintic.ps1 +++ /dev/null @@ -1,338 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Get-PropertyValue($Instance, [string]$Name) { - $property = $Instance.GetType().GetProperty($Name, [Reflection.BindingFlags]'Instance,Public,NonPublic') - Assert-True ($null -ne $property) ("Missing property: " + $Name) - return $property.GetValue($Instance) -} - -function New-Point( - [double]$X, - [double]$Y, - [double]$ArcLength, - [double]$Heading, - [double]$BodyClearance = 1.0, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($pointType, @( - $X, $Y, $ArcLength, $Heading, $Heading, $BodyClearance, $IsGearSwitch, $anchor)) -} - -function New-DirectionSegment( - [int]$Index, - $Direction, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch)) -} - -function New-EmptyMap { - $request = [Activator]::CreateInstance($mapRequestType) - $request.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $map = [Activator]::CreateInstance($mapFactoryType).Create($request).Map - Assert-True ($null -ne $map) 'Quintic test must create an explicit empty planning map.' - return $map -} - -function New-AlgorithmInput( - [object[]]$Segments, - [double]$ReserveMeters, - [double]$KnotSpacingMeters = 1.0, - [double]$MinimumKnotSpacingMeters = 0.10) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]]@(,$typedSegments)) - $vehicle = [Activator]::CreateInstance($vehicleType) - $vehicle.LengthMeters = [double]0.20 - $vehicle.WidthMeters = [double]0.20 - $vehicle.SafetyMarginMeters = [double]0.0 - $vehicle.MaximumCurvaturePerMeter = [double]100.0 - $vehicle.MinimumTurningRadiusMeters = [double]0.01 - $configuration = [Activator]::CreateInstance($configurationType) - $configuration.PiecewiseQuintic.KnotSpacingMeters = $KnotSpacingMeters - $configuration.PiecewiseQuintic.MinimumKnotSpacingMeters = $MinimumKnotSpacingMeters - $options = $optionsConstructor.Invoke(@($configuration)) - return $inputConstructor.Invoke(@($preparedPath, (New-EmptyMap), $vehicle, [double]0.05, $ReserveMeters, $options)) -} - -function Invoke-Candidate( - [object[]]$Segments, - [double]$ReserveMeters = 0.0, - [double]$KnotSpacingMeters = 1.0, - [double]$MinimumKnotSpacingMeters = 0.10) { - return $smoothMethod.Invoke($smoother, @( - (New-AlgorithmInput $Segments $ReserveMeters $KnotSpacingMeters $MinimumKnotSpacingMeters), - [double]1.0, [Threading.CancellationToken]::None)) -} - -function Invoke-Smoothing( - [object[]]$Segments, - [double]$ReserveMeters = 0.0, - [double]$KnotSpacingMeters = 1.0, - [double]$MinimumKnotSpacingMeters = 0.10) { - $candidate = Invoke-Candidate $Segments $ReserveMeters $KnotSpacingMeters $MinimumKnotSpacingMeters - Assert-True (Get-PropertyValue $candidate 'Succeeded') 'Quintic smoothing must produce a candidate for the deterministic fixture.' - return @(Get-PropertyValue $candidate 'Segments') -} - -function Get-PointDistance($Left, $Right) { - $deltaX = $Left.X - $Right.X - $deltaY = $Left.Y - $Right.Y - return [Math]::Sqrt($deltaX * $deltaX + $deltaY * $deltaY) -} - -function Get-Reference([object[]]$Source, [double]$ArcLength) { - $typedPoints = [Array]::CreateInstance($pointType, $Source.Count) - for ($index = 0; $index -lt $Source.Count; $index++) { $typedPoints.SetValue($Source[$index], $index) } - $arguments = [object[]]@($typedPoints, $ArcLength, $null, $null) - Assert-True $interpolateMethod.Invoke($null, $arguments) 'Quintic test must resolve every sampled local-arc reference.' - return $arguments[2] -} - -function Get-PointAtArcLength([object[]]$Points, [double]$ArcLength) { - foreach ($point in $Points) { - if ([Math]::Abs($point.ArcLength - $ArcLength) -lt 0.000000000001) { return $point } - } - throw "Missing quintic sample at arc length $ArcLength" -} - -function Get-EndpointDerivative([object[]]$Samples, [double]$StepMeters, [bool]$AtStart) { - $firstCoefficients = @((-137.0 / 60.0), 5.0, -5.0, (10.0 / 3.0), (-5.0 / 4.0), (1.0 / 5.0)) - $x = 0.0 - $y = 0.0 - for ($index = 0; $index -lt 6; $index++) { - $sampleIndex = if ($AtStart) { $index } else { 5 - $index } - $sign = if ($AtStart) { 1.0 } else { -1.0 } - $x += $firstCoefficients[$index] * $Samples[$sampleIndex].X - $y += $firstCoefficients[$index] * $Samples[$sampleIndex].Y - } - return [PSCustomObject]@{ X = $sign * $x / $StepMeters; Y = $sign * $y / $StepMeters } -} - -function Get-EndpointSecondDerivative([object[]]$Samples, [double]$StepMeters, [bool]$AtStart) { - $coefficients = @((15.0 / 4.0), (-77.0 / 6.0), (107.0 / 6.0), -13.0, (61.0 / 12.0), (-5.0 / 6.0)) - $x = 0.0 - $y = 0.0 - for ($index = 0; $index -lt 6; $index++) { - $sampleIndex = if ($AtStart) { $index } else { 5 - $index } - $x += $coefficients[$index] * $Samples[$sampleIndex].X - $y += $coefficients[$index] * $Samples[$sampleIndex].Y - } - return [PSCustomObject]@{ X = $x / ($StepMeters * $StepMeters); Y = $y / ($StepMeters * $StepMeters) } -} - -function Get-IntervalSamples([object[]]$Points, [double]$StartArcLength, [double]$EndArcLength, [bool]$FromStart) { - $intervalLength = $EndArcLength - $StartArcLength - $result = @() - for ($index = 0; $index -lt 6; $index++) { - $parameter = if ($FromStart) { $index / 8.0 } else { (3.0 + $index) / 8.0 } - $result += Get-PointAtArcLength $Points ($StartArcLength + $parameter * $intervalLength) - } - return $result -} - -function Get-QuinticPositionFromInteriorSamples( - [object[]]$Points, - [double]$StartArcLength, - [double]$EndArcLength, - [double[]]$Parameters, - [double]$TargetParameter) { - $intervalLength = $EndArcLength - $StartArcLength - $x = 0.0 - $y = 0.0 - for ($index = 0; $index -lt $Parameters.Count; $index++) { - $weight = 1.0 - for ($otherIndex = 0; $otherIndex -lt $Parameters.Count; $otherIndex++) { - if ($index -ne $otherIndex) { - $weight *= ($TargetParameter - $Parameters[$otherIndex]) / ($Parameters[$index] - $Parameters[$otherIndex]) - } - } - $sample = Get-PointAtArcLength $Points ($StartArcLength + $Parameters[$index] * $intervalLength) - $x += $weight * $sample.X - $y += $weight * $sample.Y - } - return [PSCustomObject]@{ X = $x; Y = $y } -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$processing = $root + 'Processing.' -$algorithms = $root + 'Algorithms.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' - -$smootherType = Get-RequiredType ($algorithms + 'PiecewiseQuinticSmoother') -$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D') -$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') -$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') -$inputType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmInput') -$optionsType = Get-RequiredType ($algorithms + 'SmoothingOptionsSnapshot') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$interpolatorType = Get-RequiredType ($processing + 'PathReferenceInterpolator') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm' -$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap' -$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest' -$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory' - -$inputConstructor = $inputType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @($preparedPathType, $mapType, $vehicleType, [double], [double], $optionsType), $null) -Assert-True ($null -ne $inputConstructor) 'Algorithm input must carry immutable quintic options and clearance reserve.' -$optionsConstructor = $optionsType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, @($configurationType), $null) -Assert-True ($null -ne $optionsConstructor) 'Quintic tests must create immutable options snapshots.' -$interpolateMethod = $interpolatorType.GetMethod('TryInterpolateByArcLength', [Reflection.BindingFlags]'Static,Public,NonPublic') -Assert-True ($null -ne $interpolateMethod) 'PathReferenceInterpolator must expose local-arc interpolation.' -$smoother = [Activator]::CreateInstance($smootherType, $true) -$smoothMethod = $smootherType.GetMethod('Smooth', [Reflection.BindingFlags]'Instance,Public') -Assert-True ($null -ne $smoothMethod) 'PiecewiseQuinticSmoother must implement the internal smoother contract.' -Assert-Equal 'PiecewiseQuintic' $smoother.Method.ToString() 'Quintic smoother must identify its public smoothing method.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anchor = [Enum]::Parse($sourceType, 'Anchor') - -# The 1.0 m local-arc knot spacing creates shared knots at s=1 and s=2. -# The generated 1/8 samples allow exact one-sided quintic derivative reconstruction. -$continuitySource = @( - (New-Point 0.00 0.00 0.00 0.00), - (New-Point 0.50 0.00 0.50 0.00), - (New-Point 1.00 0.00 1.00 0.00), - (New-Point 1.00 0.50 1.50 ([Math]::PI / 2.0)), - (New-Point 1.00 1.00 2.00 ([Math]::PI / 2.0)), - (New-Point 1.30 1.30 2.40 ([Math]::PI / 4.0))) -$continuityOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $continuitySource)))[0].Points -foreach ($sharedArcLength in @(1.0, 2.0)) { - $leftSamples = Get-IntervalSamples $continuityOutput ($sharedArcLength - 1.0) $sharedArcLength $false - $rightEnd = if ($sharedArcLength -eq 2.0) { 2.4 } else { $sharedArcLength + 1.0 } - $rightSamples = Get-IntervalSamples $continuityOutput $sharedArcLength $rightEnd $true - $leftPosition = Get-QuinticPositionFromInteriorSamples $continuityOutput ($sharedArcLength - 1.0) $sharedArcLength ` - @((2.0 / 8.0), (3.0 / 8.0), (4.0 / 8.0), (5.0 / 8.0), (6.0 / 8.0), (7.0 / 8.0)) 1.0 - $rightPosition = Get-QuinticPositionFromInteriorSamples $continuityOutput $sharedArcLength $rightEnd ` - @((1.0 / 8.0), (2.0 / 8.0), (3.0 / 8.0), (4.0 / 8.0), (5.0 / 8.0), (6.0 / 8.0)) 0.0 - $leftFirst = Get-EndpointDerivative $leftSamples (1.0 / 8.0) $false - $rightFirst = Get-EndpointDerivative $rightSamples (($rightEnd - $sharedArcLength) / 8.0) $true - $leftSecond = Get-EndpointSecondDerivative $leftSamples (1.0 / 8.0) $false - $rightSecond = Get-EndpointSecondDerivative $rightSamples (($rightEnd - $sharedArcLength) / 8.0) $true - Assert-Near $leftPosition.X $rightPosition.X 0.000001 'Shared knot X position must match from both quintic intervals.' - Assert-Near $leftPosition.Y $rightPosition.Y 0.000001 'Shared knot Y position must match from both quintic intervals.' - Assert-Near $leftFirst.X $rightFirst.X 0.000001 'Shared knot X first derivative must be C1.' - Assert-Near $leftFirst.Y $rightFirst.Y 0.000001 'Shared knot Y first derivative must be C1.' - Assert-Near $leftSecond.X $rightSecond.X 0.000001 'Shared knot X second derivative must be C2.' - Assert-Near $leftSecond.Y $rightSecond.Y 0.000001 'Shared knot Y second derivative must be C2.' -} - -$firstOutput = $continuityOutput[0] -$lastOutput = $continuityOutput[$continuityOutput.Count - 1] -Assert-Near $continuitySource[0].X $firstOutput.X 0.0 'Quintic start X must remain exact.' -Assert-Near $continuitySource[0].Y $firstOutput.Y 0.0 'Quintic start Y must remain exact.' -Assert-Near $continuitySource[$continuitySource.Count - 1].X $lastOutput.X 0.0 'Quintic end X must remain exact.' -Assert-Near $continuitySource[$continuitySource.Count - 1].Y $lastOutput.Y 0.0 'Quintic end Y must remain exact.' -foreach ($point in $continuityOutput) { - $reference = Get-Reference $continuitySource $point.ArcLength - Assert-True ((Get-PointDistance $point $reference) -le ($reference.BodyClearance + 0.000000000001)) 'Every quintic sample must remain inside its local reference movement bound.' -} - -# Separate prepared direction segments must retain their exact duplicated switch pose and topology. -$reverseSource = @( - (New-Point 1.30 1.30 0.00 ([Math]::PI / 4.0) 1.0 $true), - (New-Point 1.30 0.80 0.50 ([Math]::PI / 2.0)), - (New-Point 1.30 0.30 1.00 ([Math]::PI / 2.0))) -$switchOutput = @(Invoke-Smoothing @( - (New-DirectionSegment 0 $forward $continuitySource $false $true), - (New-DirectionSegment 1 $reverse $reverseSource $true $false))) -Assert-Equal 2 $switchOutput.Count 'Quintic smoothing must retain separate direction segments.' -Assert-True $switchOutput[0].EndsAtGearSwitch 'Forward quintic segment must retain its gear-switch boundary flag.' -Assert-True $switchOutput[1].StartsAtGearSwitch 'Reverse quintic segment must retain its gear-switch boundary flag.' -$leftSwitch = $switchOutput[0].Points[$switchOutput[0].Points.Count - 1] -$rightSwitch = $switchOutput[1].Points[0] -Assert-Near $leftSwitch.X $rightSwitch.X 0.0 'Quintic smoothing must preserve switch X exactly.' -Assert-Near $leftSwitch.Y $rightSwitch.Y 0.0 'Quintic smoothing must preserve switch Y exactly.' - -# Spacing selects local-arc knots, while a valid minimum spacing does not change that selection. -$oneMeterOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $continuitySource)) 0.0 1.0 0.10)[0].Points -$halfMeterOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $continuitySource)) 0.0 0.50 0.10)[0].Points -$largeMinimumOutput = @(Invoke-Smoothing @((New-DirectionSegment 0 $forward $continuitySource)) 0.0 1.0 0.30)[0].Points -Assert-True ($halfMeterOutput.Count -gt $oneMeterOutput.Count) 'Custom knot spacing must create additional local-arc knot intervals.' -Assert-Equal $oneMeterOutput.Count $largeMinimumOutput.Count 'Valid minimum knot spacing must not change knot selection.' -for ($index = 0; $index -lt $oneMeterOutput.Count; $index++) { - Assert-Near $oneMeterOutput[$index].X $largeMinimumOutput[$index].X 0.000000000001 'Minimum knot spacing must not change valid quintic X geometry.' - Assert-Near $oneMeterOutput[$index].Y $largeMinimumOutput[$index].Y 0.000000000001 'Minimum knot spacing must not change valid quintic Y geometry.' -} - -$shortSource = @( - (New-Point 0.00 0.00 0.00 0.00), - (New-Point 0.05 0.00 0.05 0.00)) -$shortCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $shortSource)) 0.0 1.0 0.10 -Assert-Equal 'Failed' (Get-PropertyValue $shortCandidate 'Status').ToString() 'A segment shorter than the configured minimum knot spacing must fail terminally.' -Assert-True (-not (Get-PropertyValue $shortCandidate 'Succeeded')) 'A degenerate short quintic segment must not be executable.' -Assert-Equal 0 (Get-PropertyValue $shortCandidate 'Segments').Count 'A terminal quintic degeneracy must publish no geometry.' - -# Repeated decimal addition must not create a spurious 1e-16 m terminal residual when the -# requested spacing divides the local length exactly. A real 0.005 m remainder remains below -# the 0.01 m minimum and must still fail terminally. -$decimalMultipleSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 1.0 0.0 1.0 0.0)) -$decimalMultipleCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $decimalMultipleSource)) 0.0 0.10 0.01 -Assert-Equal 'Success' (Get-PropertyValue $decimalMultipleCandidate 'Status').ToString() 'Decimal-exact knot multiples must not become a terminal under-minimum residual failure.' -$decimalMultipleOutput = @(Get-PropertyValue $decimalMultipleCandidate 'Segments')[0].Points -Assert-Equal 81 $decimalMultipleOutput.Count 'A 1.0 m segment with 0.1 m spacing must create exactly ten valid quintic intervals.' -Assert-Near 1.0 $decimalMultipleOutput[$decimalMultipleOutput.Count - 1].ArcLength 0.0 'Decimal-multiple knot normalization must retain the exact terminal arc length.' - -$meaningfulShortResidualSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 1.005 0.0 1.005 0.0)) -$meaningfulShortResidualCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $meaningfulShortResidualSource)) 0.0 0.10 0.01 -Assert-Equal 'Failed' (Get-PropertyValue $meaningfulShortResidualCandidate 'Status').ToString() 'A genuine 0.005 m terminal residual must remain a terminal spacing failure.' -Assert-Equal 0 (Get-PropertyValue $meaningfulShortResidualCandidate 'Segments').Count 'A genuine under-minimum residual must publish no geometry.' - -$smallScaleResidualSource = @( - (New-Point 0.0 0.0 0.0 0.0), - (New-Point 0.000000000000105 0.0 0.000000000000105 0.0)) -$smallScaleResidualCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $smallScaleResidualSource)) 0.0 0.00000000000001 0.000000000000006 -Assert-Equal 'Failed' (Get-PropertyValue $smallScaleResidualCandidate 'Status').ToString() 'Endpoint normalization tolerance must scale with local arc magnitude and preserve a genuine small residual failure.' - -# Local-arc reference mapping, rather than sample index/global distance, must reject this unsafe nonuniform path. -$nonuniformUnsafeSource = @( - (New-Point 0.0 0.0 0.0 0.0 0.50), - (New-Point 1.0 0.0 4.0 0.0 0.50), - (New-Point 2.0 0.0 5.0 0.0 0.50), - (New-Point 3.0 0.0 6.0 0.0 0.50), - (New-Point 3.0 1.0 9.0 ([Math]::PI / 2.0) 0.50), - (New-Point 3.0 2.0 20.0 ([Math]::PI / 2.0) 0.50)) -$nonuniformUnsafeCandidate = Invoke-Candidate @((New-DirectionSegment 0 $forward $nonuniformUnsafeSource)) 0.0 5.0 0.10 -Assert-Equal 'RetryableInfeasible' (Get-PropertyValue $nonuniformUnsafeCandidate 'Status').ToString() 'Unsafe nonuniform local-arc quintic movement must be retryable.' -Assert-True (-not (Get-PropertyValue $nonuniformUnsafeCandidate 'Succeeded')) 'Unsafe nonuniform quintic geometry must not be executable.' -Assert-Equal 0 (Get-PropertyValue $nonuniformUnsafeCandidate 'Segments').Count 'Retryable quintic infeasibility must publish no geometry.' - -Write-Output 'Path smoothing piecewise quintic checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_runner.ps1 b/.task8-sweep/tests/verify_path_smoothing_runner.ps1 deleted file mode 100644 index b22b6d9..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_runner.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { - throw "$Message Expected=$Expected Actual=$Actual" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Assert-AttemptedStrengths($Snapshot, [double[]]$Expected, [string]$Message) { - Assert-Equal $Expected.Length $Snapshot.AttemptedStrengths.Count ($Message + ' count') - for ($index = 0; $index -lt $Expected.Length; $index++) { - Assert-Near $Expected[$index] $Snapshot.AttemptedStrengths[$index] ($Message + " index=$index") - } -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$algorithms = $root + 'Algorithms.' -$runnerType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmRunner') -$smootherType = Get-RequiredType ($algorithms + 'IPathSmoother') -$candidateType = Get-RequiredType ($algorithms + 'SmoothingCandidate') -$candidateStatusType = Get-RequiredType ($algorithms + 'SmoothingCandidateStatus') -Assert-False $smootherType.IsPublic 'IPathSmoother must remain internal to the algorithm assembly.' -Assert-Equal 3 ([Enum]::GetNames($candidateStatusType).Length) 'Smoothing candidate status must contain only the three defined feasibility states.' -Assert-Equal 'Success' ([Enum]::GetNames($candidateStatusType)[0]) 'Candidate status must expose Success.' -Assert-Equal 'RetryableInfeasible' ([Enum]::GetNames($candidateStatusType)[1]) 'Candidate status must expose RetryableInfeasible.' -Assert-Equal 'Failed' ([Enum]::GetNames($candidateStatusType)[2]) 'Candidate status must expose Failed.' -$retryableFactory = $candidateType.GetMethod('RetryableInfeasible', [Reflection.BindingFlags]'Static,NonPublic') -Assert-True ($null -ne $retryableFactory) 'SmoothingCandidate must create retryable infeasibility without executable geometry.' - -$hooksType = $runnerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $hooksType) 'SmoothingAlgorithmRunner must expose its narrowly scoped nested TestHooks helper.' -$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $executeMethod) 'TestHooks must expose deterministic scenario execution for reflection tests.' - -function Invoke-Scenario([string]$Scenario) { - return $executeMethod.Invoke($null, @($Scenario)) -} - -$allRetryable = Invoke-Scenario 'RetryableInfeasible' -Assert-Equal 'Infeasible' $allRetryable.Status 'Exhausted retryable infeasibility must produce an Infeasible runner result.' -Assert-AttemptedStrengths $allRetryable @(1.00, 0.75, 0.50, 0.25) 'Retryable infeasibility must use the finite retry schedule exactly.' -Assert-Equal 0 $allRetryable.AcceptedPathPointCount 'An infeasible runner result must not retain a retryable candidate as an accepted path.' -Assert-Equal 0 $allRetryable.RejectedComparisonCandidatePointCount 'Retryable infeasibility must not retain executable candidate geometry.' -Assert-Equal 4 $allRetryable.FailureCount 'Every retryable attempt must retain its failure reason.' - -$accepted = Invoke-Scenario 'AcceptFirst' -Assert-Equal 'Success' $accepted.Status 'The first safe candidate must be accepted.' -Assert-AttemptedStrengths $accepted @(1.00) 'The runner must stop immediately after the first accepted candidate.' -Assert-True ($accepted.AcceptedPathPointCount -gt 0) 'A successful runner result must publish the validated path internally.' -Assert-Equal 0 $accepted.RejectedComparisonCandidatePointCount 'An accepted candidate must not create rejected comparison geometry.' - -$terminalFailure = Invoke-Scenario 'TerminalFailed' -Assert-Equal 'Failed' $terminalFailure.Status 'A terminal candidate failure must stop the runner as Failed.' -Assert-AttemptedStrengths $terminalFailure @(1.00) 'Terminal candidate failure must run exactly once.' -Assert-Equal 0 $terminalFailure.RejectedComparisonCandidatePointCount 'A terminal failure must not retain comparison geometry.' - -$cancelled = Invoke-Scenario 'CancelBeforeNextAttempt' -Assert-True $cancelled.CancellationPropagated 'Cancellation between attempts must propagate out of the runner.' -Assert-AttemptedStrengths $cancelled @(1.00) 'Cancellation before the next attempt must prevent another smoother call.' -Assert-Equal 0 $cancelled.AcceptedPathPointCount 'A cancelled run must not publish a partial path.' - -Write-Output 'Path smoothing retry runner checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_service.ps1 b/.task8-sweep/tests/verify_path_smoothing_service.ps1 deleted file mode 100644 index 704e9a7..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_service.ps1 +++ /dev/null @@ -1,259 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function New-Map([bool]$WithObstacle) { - $mapRequest = [Activator]::CreateInstance($mapRequestType) - $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $mapRequest.ResolutionMm = [single]50 - if ($WithObstacle) { - $obstacle = [Activator]::CreateInstance($rectangleType, @([single]900, [single]1100, [single]400, [single]600)) - $obstacles = [Array]::CreateInstance($obstacleType, 1) - $obstacles.SetValue($obstacle, 0) - $source = [Activator]::CreateInstance($manualSourceType, @('service-safety-obstacle', [long]1, $true, $obstacles)) - $sources = [Array]::CreateInstance($obstacleSourceType, 1) - $sources.SetValue($source, 0) - $mapRequest.ObstacleSources = $sources - } - else { - $mapRequest.AllowExplicitEmptyMap = $true - } - $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map - Assert-True ($null -ne $map) 'Service test must create a planning map.' - return $map -} - -function New-EmptyMap { return New-Map $false } - -function New-CollidingMap { return New-Map $true } - -function New-Vehicle { - $vehicle = [Activator]::CreateInstance($vehicleType) - $vehicle.LengthMeters = [double]0.20 - $vehicle.WidthMeters = [double]0.20 - $vehicle.SafetyMarginMeters = [double]0.0 - $vehicle.MaximumCurvaturePerMeter = [double]100.0 - return $vehicle -} - -function New-CoarsePoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - $Direction, - [double]$BodyClearance = 1.0, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($coarsePointType, @( - $X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction, - [double]0.0, $BodyClearance, $IsGearSwitch, $coarseAnchor)) -} - -function New-Configuration($Method = $cubicBSpline) { - $configuration = [Activator]::CreateInstance($configurationType) - $configuration.Method = $Method - return $configuration -} - -function New-Request([object[]]$Points, $Configuration, $Map = $null) { - if ($null -eq $Map) { $Map = New-EmptyMap } - $typedPoints = [Array]::CreateInstance($coarsePointType, $Points.Count) - for ($index = 0; $index -lt $Points.Count; $index++) { - $typedPoints.SetValue($Points[$index], $index) - } - - $segments = [Array]::CreateInstance($coarseSegmentType, 1) - $segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @( - 0, $forward, 0, ($Points.Count - 1), $false, $false)), 0) - return [Activator]::CreateInstance($requestType, @($typedPoints, $segments, $Map, (New-Vehicle), $Configuration)) -} - -function New-StraightRequest($Configuration) { - return New-Request @( - (New-CoarsePoint 0.5 0.5 0.0 $forward), - (New-CoarsePoint 1.5 0.5 1.0 $forward)) $Configuration -} - -function New-InfeasibleRequest($Configuration) { - # Zero declared movement clearance makes every non-linear B-spline displacement retryably infeasible, - # while the empty map still permits the independently revalidated coarse-path fallback. - return New-Request @( - (New-CoarsePoint 0.5 0.5 0.0 $forward 0.0), - (New-CoarsePoint 1.0 0.5 0.5 $forward 0.0), - (New-CoarsePoint 1.0 1.0 1.0 $forward 0.0), - (New-CoarsePoint 1.5 1.0 1.5 $forward 0.0)) $Configuration -} - -function Invoke-Smooth($Request, [Threading.CancellationToken]$CancellationToken = [Threading.CancellationToken]::None) { - return $smoothMethod.Invoke($service, @($Request, $CancellationToken)) -} - -function Assert-NoGeometry($Result, [string]$Message) { - Assert-Equal 0 $Result.Path.Count "$Message A non-published result must not expose a path." - Assert-Equal 0 $Result.Segments.Count "$Message A non-published result must not expose segments." -} - -function Assert-InvalidInputBeforeRetry($Configuration, [string]$CaseName) { - $result = Invoke-Smooth (New-StraightRequest $Configuration) - Assert-Equal 'InvalidInput' $result.Status.ToString() "$CaseName must be rejected as invalid input." - Assert-NoGeometry $result $CaseName - Assert-Equal 0 $result.Diagnostics.RetryCount "$CaseName must be rejected before any smoothing retry." - Assert-Near 0.0 $result.Diagnostics.AcceptedStrength 0.0 "$CaseName must not accept a smoothing strength." -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$facade = $root + 'Facade.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - -$serviceType = Get-RequiredType ($facade + 'PathSmoothingService') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$resultType = Get-RequiredType ($root + 'PathSmoothingResult') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$methodType = Get-RequiredType ($root + 'SmoothingMethod') -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm') -$obstacleType = Get-RequiredType ($mapping + 'IMapObstacle') -$rectangleType = Get-RequiredType ($mapping + 'AxisAlignedRectangleObstacle') -$obstacleSourceType = Get-RequiredType ($mapping + 'IMapObstacleSource') -$manualSourceType = Get-RequiredType ($mapping + 'ManualObstacleSource') -$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest') -$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory') - -Assert-True $serviceType.IsPublic 'PathSmoothingService must be public.' -$service = [Activator]::CreateInstance($serviceType) -$smoothMethod = $serviceType.GetMethod('Smooth', [Type[]]@($requestType, [Threading.CancellationToken])) -Assert-True ($null -ne $smoothMethod) 'PathSmoothingService must expose Smooth(PathSmoothingRequest, CancellationToken).' -Assert-Equal $resultType $smoothMethod.ReturnType 'PathSmoothingService Smooth must return PathSmoothingResult.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$cubicBSpline = [Enum]::Parse($methodType, 'CubicBSpline') -$localCubicBezier = [Enum]::Parse($methodType, 'LocalCubicBezier') -$piecewiseQuintic = [Enum]::Parse($methodType, 'PiecewiseQuintic') -$localG2Quintic = [Enum]::Parse($methodType, 'LocalG2Quintic') -$coarseAnchor = [Enum]::Parse($coarsePointSourceType, 'Start') - -# The public method registry must retain every stable enum-to-algorithm mapping. -foreach ($method in @($cubicBSpline, $localCubicBezier, $piecewiseQuintic)) { - $result = Invoke-Smooth (New-StraightRequest (New-Configuration $method)) - Assert-Equal 'Success' $result.Status.ToString() "A valid straight path must succeed for $method." - Assert-Equal $method.ToString() $result.Method.ToString() "The result must retain the selected $method method." - Assert-True ($result.Path.Count -gt 0) "A successful $method result must publish geometry." - Assert-True $result.Diagnostics.Metrics.IsFeasible "A successful $method result must publish feasible diagnostics." -} - -# Local G2 is a dedicated service pipeline, but must remain available through the same public entrypoint. -$localG2Result = Invoke-Smooth (New-StraightRequest (New-Configuration $localG2Quintic)) -Assert-Equal 'NotNeeded' $localG2Result.Status.ToString() 'A straight Local G2 request must publish its verified baseline as NotNeeded.' -Assert-Equal 'LocalG2Quintic' $localG2Result.Method.ToString() 'Local G2 must retain the selected method.' -Assert-True ($localG2Result.Path.Count -gt 0) 'NotNeeded Local G2 must publish verified geometry.' - -# Every configuration scalar is checked before the options snapshot or retry runner starts. -$invalidConfigurationCases = @( - [PSCustomObject]@{ Name = 'NaN output spacing'; Mutate = { param($c) $c.OutputSpacingMeters = [double]::NaN } }, - [PSCustomObject]@{ Name = 'zero output spacing'; Mutate = { param($c) $c.OutputSpacingMeters = [double]0.0 } }, - [PSCustomObject]@{ Name = 'infinite collision step'; Mutate = { param($c) $c.MaximumCollisionCheckStepMeters = [double]::PositiveInfinity } }, - [PSCustomObject]@{ Name = 'zero collision step'; Mutate = { param($c) $c.MaximumCollisionCheckStepMeters = [double]0.0 } }, - [PSCustomObject]@{ Name = 'NaN clearance reserve'; Mutate = { param($c) $c.MinimumClearanceReserveMeters = [double]::NaN } }, - [PSCustomObject]@{ Name = 'negative clearance reserve'; Mutate = { param($c) $c.MinimumClearanceReserveMeters = [double]-0.01 } }, - [PSCustomObject]@{ Name = 'NaN smoothing strength'; Mutate = { param($c) $c.SmoothingStrength = [double]::NaN } }, - [PSCustomObject]@{ Name = 'zero smoothing strength'; Mutate = { param($c) $c.SmoothingStrength = [double]0.0 } }, - [PSCustomObject]@{ Name = 'NaN B-spline scale'; Mutate = { param($c) $c.CubicBSpline.EndpointTangentScale = [double]::NaN } }, - [PSCustomObject]@{ Name = 'zero B-spline scale'; Mutate = { param($c) $c.CubicBSpline.EndpointTangentScale = [double]0.0 } }, - [PSCustomObject]@{ Name = 'zero Bezier threshold'; Mutate = { param($c) $c.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.0 } }, - [PSCustomObject]@{ Name = 'over-pi Bezier threshold'; Mutate = { param($c) $c.LocalCubicBezier.CornerHeadingThresholdRadians = [Math]::PI + 0.01 } }, - [PSCustomObject]@{ Name = 'NaN Bezier window'; Mutate = { param($c) $c.LocalCubicBezier.MaximumWindowLengthMeters = [double]::NaN } }, - [PSCustomObject]@{ Name = 'zero Bezier window'; Mutate = { param($c) $c.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.0 } }, - [PSCustomObject]@{ Name = 'infinite Bezier handle scale'; Mutate = { param($c) $c.LocalCubicBezier.HandleLengthRatio = [double]::PositiveInfinity } }, - [PSCustomObject]@{ Name = 'zero Bezier handle scale'; Mutate = { param($c) $c.LocalCubicBezier.HandleLengthRatio = [double]0.0 } }, - [PSCustomObject]@{ Name = 'NaN quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]::NaN } }, - [PSCustomObject]@{ Name = 'zero quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]0.0 } }, - [PSCustomObject]@{ Name = 'infinite minimum quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]::PositiveInfinity } }, - [PSCustomObject]@{ Name = 'zero minimum quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.0 } }, - [PSCustomObject]@{ Name = 'quintic knot spacing below minimum'; Mutate = { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]0.05; $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.10 } } -) -foreach ($case in $invalidConfigurationCases) { - $configuration = New-Configuration - & $case.Mutate $configuration - Assert-InvalidInputBeforeRetry $configuration $case.Name -} - -$unknownMethodConfiguration = New-Configuration ([Enum]::ToObject($methodType, 99)) -Assert-InvalidInputBeforeRetry $unknownMethodConfiguration 'unknown smoothing method' - -$invalidCoarseConfiguration = New-Configuration -$invalidCoarseRequest = New-Request @( - (New-CoarsePoint ([double]::NaN) 0.5 0.0 $forward), - (New-CoarsePoint 1.5 0.5 1.0 $forward)) $invalidCoarseConfiguration -$invalidCoarseResult = Invoke-Smooth $invalidCoarseRequest -Assert-Equal 'InvalidInput' $invalidCoarseResult.Status.ToString() 'A non-finite coarse path coordinate must be invalid input.' -Assert-NoGeometry $invalidCoarseResult 'Invalid coarse path' -Assert-Equal 0 $invalidCoarseResult.Diagnostics.RetryCount 'Invalid coarse input must be rejected before retries.' - -# A finite, structurally valid coarse path may still be unsafe for the requested map and vehicle. -# It must be rejected before method selection/retries and may never use fallback to publish the unsafe geometry. -$unsafeCoarseResult = Invoke-Smooth (New-Request @( - (New-CoarsePoint 0.5 0.5 0.0 $forward), - (New-CoarsePoint 1.5 0.5 1.0 $forward)) (New-Configuration) (New-CollidingMap)) -Assert-Equal 'InvalidInput' $unsafeCoarseResult.Status.ToString() 'A colliding coarse path must be invalid before smoothing starts.' -Assert-NoGeometry $unsafeCoarseResult 'Unsafe coarse path' -Assert-Equal 0 $unsafeCoarseResult.Diagnostics.RetryCount 'Unsafe coarse geometry must be rejected before retry execution.' - -$cancelledConfiguration = New-Configuration -$cancellationSource = [Threading.CancellationTokenSource]::new() -$cancellationSource.Cancel() -try { - $cancelledResult = Invoke-Smooth (New-StraightRequest $cancelledConfiguration) $cancellationSource.Token - Assert-Equal 'Cancelled' $cancelledResult.Status.ToString() 'Pre-cancelled smoothing must return the explicit cancellation result.' - Assert-NoGeometry $cancelledResult 'Cancelled smoothing' - Assert-Equal 0 $cancelledResult.Diagnostics.RetryCount 'Cancellation before execution must not start retries.' -} -finally { - $cancellationSource.Dispose() -} - -$withoutFallbackConfiguration = New-Configuration -$withoutFallbackConfiguration.AllowFallbackToCoarsePath = $false -$withoutFallbackResult = Invoke-Smooth (New-InfeasibleRequest $withoutFallbackConfiguration) -Assert-Equal 'Infeasible' $withoutFallbackResult.Status.ToString() 'A retryably infeasible candidate without fallback must remain infeasible.' -Assert-NoGeometry $withoutFallbackResult 'Infeasible smoothing without fallback' -Assert-Equal 3 $withoutFallbackResult.Diagnostics.RetryCount 'Infeasible smoothing must exhaust the four configured strengths.' -Assert-Near 0.0 $withoutFallbackResult.Diagnostics.AcceptedStrength 0.0 'Infeasible smoothing must not accept a strength.' - -$withFallbackConfiguration = New-Configuration -$withFallbackConfiguration.AllowFallbackToCoarsePath = $true -$withFallbackResult = Invoke-Smooth (New-InfeasibleRequest $withFallbackConfiguration) -Assert-Equal 'FallbackToCoarsePath' $withFallbackResult.Status.ToString() 'A verified coarse path must return explicit fallback status.' -Assert-Equal 'CubicBSpline' $withFallbackResult.Method.ToString() 'Fallback must retain the originally selected method.' -Assert-True ($withFallbackResult.Path.Count -gt 0) 'A verified fallback must publish the revalidated coarse geometry.' -Assert-True $withFallbackResult.Diagnostics.Metrics.IsFeasible 'Fallback must publish feasible shared-geometry diagnostics.' -foreach ($point in $withFallbackResult.Path) { - Assert-Equal 'CoarsePathFallback' $point.Source.ToString() 'Every fallback point must be explicitly labeled as coarse-path fallback.' -} -Assert-Equal $withoutFallbackResult.Diagnostics.RetryCount $withFallbackResult.Diagnostics.RetryCount 'Fallback must preserve retry diagnostics from the failed method.' -Assert-Near $withoutFallbackResult.Diagnostics.AcceptedStrength $withFallbackResult.Diagnostics.AcceptedStrength 0.0 'Fallback must preserve the failed method accepted-strength diagnostic.' -Assert-Equal $withoutFallbackResult.Diagnostics.TerminationReason $withFallbackResult.Diagnostics.TerminationReason 'Fallback must preserve the failed method termination reason.' - -Write-Output 'Path smoothing service checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 b/.task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 deleted file mode 100644 index 80f7101..0000000 --- a/.task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 +++ /dev/null @@ -1,188 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { - throw "$Message Expected=$Expected Actual=$Actual" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Text-FromCodePoints([int[]]$CodePoints) { - return (($CodePoints | ForEach-Object { [Char]::ConvertFromUtf32($_) }) -join '') -} - -$visualization = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization.' -$styleType = Get-RequiredType ($visualization + 'IeeeFigureStyle') - -foreach ($expectation in @( - @{ Name = 'FigureWidthPoints'; Value = 515.52 }, - @{ Name = 'FigureHeightPoints'; Value = 374.4 }, - @{ Name = 'RawColor'; Value = '#4D4D4D' }, - @{ Name = 'BSplineColor'; Value = '#0072B2' }, - @{ Name = 'BezierColor'; Value = '#D55E00' }, - @{ Name = 'QuinticColor'; Value = '#009E73' }, - @{ Name = 'LimitColor'; Value = '#CC79A7' } -)) { - $field = $styleType.GetField($expectation.Name) - if ($null -eq $field) { throw "IeeeFigureStyle must expose $($expectation.Name)." } - if ($expectation.Value -is [double]) { - Assert-Near $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style." - } - else { - Assert-Equal $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style." - } -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$comparison = $root + 'Comparison.' -$facade = $root + 'Facade.' -$coarse = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' -$test = $root + 'Test.' - -$modelType = Get-RequiredType ($visualization + 'SmoothingFigureModel') -$builderType = Get-RequiredType ($visualization + 'SmoothingFigureModelBuilder') -$figureDefinitionType = Get-RequiredType ($visualization + 'SmoothingFigureDefinition') -$figureSetBuilderType = Get-RequiredType ($visualization + 'SmoothingFigureSetBuilder') -$rendererType = Get-RequiredType ($visualization + 'SmoothingSvgRenderer') -$csvWriterType = Get-RequiredType ($visualization + 'SmoothingCsvWriter') -$comparisonResultType = Get-RequiredType ($comparison + 'PathSmoothingComparisonResult') -$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') -$comparisonServiceType = Get-RequiredType ($facade + 'PathSmoothingComparisonService') -$scenarioFactoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory') -$gridMapType = Get-RequiredType ($mapping + 'PlanningGridMap') -$poseType = Get-RequiredType ($coarse + 'Pose2D') - -$buildMethod = $builderType.GetMethod('Build', [Type[]]@($comparisonResultType, $gridMapType, $poseType, $poseType, [string], [string])) -if ($null -eq $buildMethod) { throw 'SmoothingFigureModelBuilder must expose Build(comparison, map, start, goal, scenarioId, scenarioLabel).' } -Assert-Equal $modelType $buildMethod.ReturnType 'SmoothingFigureModelBuilder.Build must return the immutable figure model.' -$figureSetBuildMethod = $figureSetBuilderType.GetMethod('Build', [Type[]]@($modelType)) -if ($null -eq $figureSetBuildMethod) { throw 'SmoothingFigureSetBuilder must expose Build(SmoothingFigureModel).' } -$renderMethod = $rendererType.GetMethod('Render', [Type[]]@($figureDefinitionType)) -if ($null -eq $renderMethod) { throw 'SmoothingSvgRenderer must expose Render(SmoothingFigureDefinition).' } -$writeMethod = $csvWriterType.GetMethod('Write', [Type[]]@($modelType)) -if ($null -eq $writeMethod) { throw 'SmoothingCsvWriter must expose Write(SmoothingFigureModel).' } - -$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json' -$fixtureRequests = $scenarioFactoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])).Invoke($null, @((Resolve-Path $fixturePath).Path)) -$comparisonRequest = $fixtureRequests[0] -Assert-Equal $comparisonRequestType $comparisonRequest.GetType() 'The figure test must consume a real immutable comparison request.' -$comparisonResult = [Activator]::CreateInstance($comparisonServiceType).Compare($comparisonRequest, [Threading.CancellationToken]::None) -$coarsePath = $comparisonRequest.SmoothingRequest.CoarsePath -$start = [Activator]::CreateInstance($poseType, @($coarsePath[0].X, $coarsePath[0].Y, $coarsePath[0].Heading)) -$last = $coarsePath[$coarsePath.Count - 1] -$goal = [Activator]::CreateInstance($poseType, @($last.X, $last.Y, $last.Heading)) -$builder = [Activator]::CreateInstance($builderType) -$rawLabel = Text-FromCodePoints @(0x539F, 0x59CB, 0x7C97, 0x8DEF, 0x5F84) -$bSplineLabel = (Text-FromCodePoints @(0x4E09, 0x6B21)) + ' B ' + (Text-FromCodePoints @(0x6837, 0x6761)) -$bezierLabel = (Text-FromCodePoints @(0x5C40, 0x90E8, 0x4E09, 0x6B21)) + ' B' + [Char]::ConvertFromUtf32(0x00E9) + 'zier' -$quinticLabel = Text-FromCodePoints @(0x5206, 0x6BB5, 0x4E94, 0x6B21) -$scenarioId = (Text-FromCodePoints @(0x573A, 0x666F)) + '-escape' -$scenarioLabel = (Text-FromCodePoints @(0x8DEF, 0x5F84)) + ' ' -$model = $buildMethod.Invoke($builder, @($comparisonResult, $comparisonRequest.SmoothingRequest.Map, $start, $goal, $scenarioId, $scenarioLabel)) - -$figureSet = $figureSetBuildMethod.Invoke([Activator]::CreateInstance($figureSetBuilderType), @($model)) -$figures = @($figureSet.Figures) -$expectedStems = @('01-coarse-path-overview', '02-all-paths-comparison', '03-cubic-bspline-overview', '04-local-cubic-bezier-overview', '05-piecewise-quintic-overview', '06-curvature-comparison') -Assert-Equal ($expectedStems -join ',') (($figures | ForEach-Object { $_.FileStem }) -join ',') 'Figure set must publish the stable six-file order.' -Assert-True (-not $figures[1].ShowsMapContext) 'All-path comparison must exclude obstacles and endpoint decorations.' -Assert-True ($figures[2].ShowsMapContext -and $figures[2].Series[0].Opacity -lt 1) 'Individual smoother views must retain faded coarse-path context.' -Assert-Near (($figures[0].WorldXMaxMeters - $figures[0].WorldXMinMeters) / ($figures[0].WorldYMaxMeters - $figures[0].WorldYMinMeters)) ($figures[0].PlotWidthPoints / $figures[0].PlotHeightPoints) 'Overhead figures must preserve equal X/Y scale.' - -$renderer = [Activator]::CreateInstance($rendererType) -$svgByStem = @{} -foreach ($figure in $figures) { $svgByStem[$figure.FileStem] = $renderMethod.Invoke($renderer, @($figure)) } -$svg = (($expectedStems | ForEach-Object { $svgByStem[$_] }) -join "`n") -Assert-True ($svg -match ']+viewBox="0 0 515\.52 374\.4"') 'SVG must use the fixed physical view box in typographic points.' -Assert-True $svg.Contains(($scenarioLabel -replace '&', '&' -replace '<', '<' -replace '>', '>')) 'SVG text must XML-escape scenario labels.' -Assert-True $svg.Contains('font-family="SimSun"') 'SVG must emit explicit SimSun Chinese text runs.' -Assert-True $svg.Contains('font-family="Times New Roman"') 'SVG must emit explicit Times New Roman Latin/math text runs.' -foreach ($color in @('#4D4D4D', '#0072B2', '#D55E00', '#009E73', '#CC79A7')) { Assert-True $svg.Contains($color) "SVG must retain the fixed color $color." } -Assert-True $svg.Contains('class="trajectory-point"') 'All visible trajectory samples must render as point markers.' -Assert-True (-not $svg.Contains('stroke-dasharray')) 'Trajectory reports must not use dashed path styles.' -Assert-True (-not $svg.Contains('').Count - if ($count -lt $MinimumCount) { throw "Insufficient public API documentation in $RelativePath ExpectedAtLeast=$MinimumCount Actual=$count" } -} - -$readmePath = Join-Path $MapRoot 'README.md' -if (-not (Test-Path -LiteralPath $readmePath)) { throw "Map README is required: $readmePath" } -$readme = Get-Content -LiteralPath $readmePath -Raw -Assert-Contains $readme 'PlanningMapFactory' 'README must identify the public creation facade.' -Assert-Contains $readme 'IMapObstacleSource' 'README must explain unified obstacle sources.' -Assert-Contains $readme 'PlanningGridMap' 'README must explain the planning snapshot output.' -Assert-Contains $readme 'SourceVersion' 'README must explain cache invalidation versions.' -Assert-Contains $readme 'PlanningMapBuildStatus' 'README must explain explicit map build termination states.' -Assert-Contains $readme 'PNG' 'README must explain debug image output.' -Assert-Contains $readme 'TrapMap' 'README must describe the legacy-map boundary.' - -Assert-DocumentationCount 'PlanningMapFactory.cs' 2 -Assert-DocumentationCount 'PlanningMapRequest.cs' 5 -Assert-DocumentationCount 'PlanningMapBuildResult.cs' 9 -Assert-DocumentationCount 'Core\MapBuildRequest.cs' 4 -Assert-DocumentationCount 'Core\EnvironmentMapBuildResult.cs' 7 -Assert-DocumentationCount 'Core\MapBoundsMm.cs' 12 -Assert-DocumentationCount 'Core\EnvironmentGridMap.cs' 13 -Assert-DocumentationCount 'Core\EnvironmentMapBuilder.cs' 2 -Assert-DocumentationCount 'Obstacles\IMapObstacle.cs' 2 -Assert-DocumentationCount 'Obstacles\CircleObstacle.cs' 6 -Assert-DocumentationCount 'Obstacles\AxisAlignedRectangleObstacle.cs' 7 -Assert-DocumentationCount 'Obstacles\MapObstacleRasterizer.cs' 2 -Assert-DocumentationCount 'Sources\IMapObstacleSource.cs' 5 -Assert-DocumentationCount 'Sources\ManualObstacleSource.cs' 6 -Assert-DocumentationCount 'Sources\TwoLegProjectionInput.cs' 13 -Assert-DocumentationCount 'Sources\TwoLegObstacleSource.cs' 6 -Assert-DocumentationCount 'Sources\TwoLegObstacleProjector.cs' 2 -Assert-DocumentationCount 'Sources\ObstacleProjectionResult.cs' 8 -Assert-DocumentationCount 'Sources\ObstacleSourceStatus.cs' 5 -Assert-DocumentationCount 'Planning\PlanningGridMap.cs' 15 -Assert-DocumentationCount 'Planning\PlanningMapAdapter.cs' 2 -Assert-DocumentationCount 'Planning\ObstacleDistanceField.cs' 2 -Assert-DocumentationCount 'Planning\EuclideanDistanceTransform.cs' 2 -Assert-DocumentationCount 'Planning\PlanningMapCache.cs' 5 -Assert-DocumentationCount 'Test\MovementTest.MapTest.cs' 3 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportRequest.cs' 3 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportResult.cs' 8 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExporter.cs' 6 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageRenderer.cs' 2 -Assert-DocumentationCount 'Test\Visualization\ValidatedPngWriter.cs' 2 - -Write-Output 'Planning map documentation checks passed.' diff --git a/.task8-sweep/tests/verify_planning_map_factory.ps1 b/.task8-sweep/tests/verify_planning_map_factory.ps1 deleted file mode 100644 index 2f3c763..0000000 --- a/.task8-sweep/tests/verify_planning_map_factory.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } -function Assert-False($Actual, [string]$Message) { if ($Actual) { throw $Message } } -function Assert-Null($Actual, [string]$Message) { if ($null -ne $Actual) { throw $Message } } -function Find-NonPublicInstanceMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Instance,NonPublic')) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } - } - if ($matches) { return $candidate } - } - return $null -} -$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' -$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) -$obstacleType = $assembly.GetType($ns + 'IMapObstacle', $true) -$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) -$circleType = $assembly.GetType($ns + 'CircleObstacle', $true) -$manualType = $assembly.GetType($ns + 'ManualObstacleSource', $true) -$twoLegInputType = $assembly.GetType($ns + 'TwoLegProjectionInput', $true) -$twoLegType = $assembly.GetType($ns + 'TwoLegObstacleSource', $true) -$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) -$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) -$mapResultType = $assembly.GetType($ns + 'PlanningMapBuildResult', $true) -$mapBuildStatusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) -$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $false) -$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $false) -Assert-True ($operationBudgetType -ne $null) 'PlanningOperationBudget must exist for shared planning cancellation and timeout handling.' -Assert-True ($operationStopReasonType -ne $null) 'PlanningOperationStopReason must exist for shared planning cancellation and timeout handling.' -Assert-True $operationStopReasonType.IsEnum 'PlanningOperationStopReason must be an enum.' -foreach ($expectedStopReason in @('None', 'Cancelled', 'TimedOut')) { - Assert-True ($operationStopReasonType.GetEnumNames() -contains $expectedStopReason) "PlanningOperationStopReason must contain $expectedStopReason." -} -Assert-True ($mapResultType.GetProperty('Status') -ne $null) 'PlanningMapBuildResult must expose an explicit build status.' -foreach ($expectedMapStatus in @('Success', 'Failed', 'Cancelled', 'TimedOut')) { - Assert-True ($mapBuildStatusType.GetEnumNames() -contains $expectedMapStatus) "PlanningMapBuildStatus must contain $expectedMapStatus." -} -$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]2000, [single]0, [single]2000)) -$obstacles = [Array]::CreateInstance($obstacleType, 1) -$obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]100)), 0) -$manual = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) -$twoLegInput = [Activator]::CreateInstance($twoLegInputType, @($true, [single]1000, [single]1000, [double]([Math]::PI / 2), [single]100, [single]0, [single]-100, [single]0, [single]40, 'test snapshot')) -$twoLeg = [Activator]::CreateInstance($twoLegType, @('two-leg', [long]1, $false, $twoLegInput)) -$sources = [Array]::CreateInstance($sourceType, 2); $sources.SetValue($twoLeg, 0); $sources.SetValue($manual, 1) -function New-Request($sourceArray, [bool]$allowEmpty) { $request = [Activator]::CreateInstance($requestType); $request.Bounds = $bounds; $request.ResolutionMm = [single]50; $request.ObstacleSources = $sourceArray; $request.AllowExplicitEmptyMap = $allowEmpty; return $request } -$factory = [Activator]::CreateInstance($factoryType) -$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @([Threading.CancellationToken], [TimeSpan]), $null) -Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' -$createWithBudget = Find-NonPublicInstanceMethod $factoryType 'Create' @($requestType, $operationBudgetType) -Assert-True ($createWithBudget -ne $null) 'PlanningMapFactory must expose an internal budget-aware Create overload.' -$cancelledSource = New-Object Threading.CancellationTokenSource -$cancelledSource.Cancel() -$cancelledBudget = $budgetConstructor.Invoke(@($cancelledSource.Token, [TimeSpan]::FromSeconds(1))) -$cancelled = $createWithBudget.Invoke($factory, @((New-Request $sources $false), $cancelledBudget)) -Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancelled map construction must retain the cancellation status.' -Assert-False $cancelled.Succeeded 'Cancelled map construction must not succeed.' -Assert-Null $cancelled.Map 'Cancelled map construction must not publish a map.' -Assert-Equal 'None' $cancelled.CacheHit.ToString() 'Cancelled map construction must not publish a cache hit.' -$first = $factory.Create((New-Request $sources $false)) -Assert-True $first.Succeeded 'Mixed source request must build.' -Assert-True $first.Map.PlanningReady 'Applied source geometry must make the map ready.' -Assert-True ($first.Map.IsOccupiedWorld(1.0, 1.1)) 'TwoLeg must project detection-time local coordinates into world metres.' -$second = $factory.Create((New-Request $sources $false)) -Assert-Equal 'Input' $second.CacheHit.ToString() 'Same snapshot must hit complete input cache.' -Assert-True ([object]::ReferenceEquals($first.Map, $second.Map)) 'Same snapshot must return the exact immutable map object.' -$manualVersionTwo = [Activator]::CreateInstance($manualType, @('manual', [long]2, $true, $obstacles)) -$sourcesVersionTwo = [Array]::CreateInstance($sourceType, 2); $sourcesVersionTwo.SetValue($manualVersionTwo, 0); $sourcesVersionTwo.SetValue($twoLeg, 1) -$third = $factory.Create((New-Request $sourcesVersionTwo $false)) -Assert-Equal 'Occupancy' $third.CacheHit.ToString() 'Version change with equal occupancy must reuse occupancy buffers.' -Assert-True ($third.Map.SnapshotId -gt $first.Map.SnapshotId) 'Occupancy reuse must still issue a fresh snapshot id.' -$emptySources = [Array]::CreateInstance($sourceType, 0) -$emptyBlocked = $factory.Create((New-Request $emptySources $false)) -Assert-True (-not $emptyBlocked.Map.PlanningReady) 'Implicit empty map must be blocked.' -$emptyAllowed = $factory.Create((New-Request $emptySources $true)) -Assert-True $emptyAllowed.Map.PlanningReady 'Explicit empty map must be accepted.' -$invalidManual = [Activator]::CreateInstance($manualType, @('invalid', [long]0, $true, $null)) -$invalidSources = [Array]::CreateInstance($sourceType, 1); $invalidSources.SetValue($invalidManual, 0) -$invalid = $factory.Create((New-Request $invalidSources $false)) -Assert-True (-not $invalid.Succeeded) 'Required invalid source must reject the full build.' -Write-Output 'Planning map factory checks passed.' diff --git a/.task8-sweep/tests/verify_planning_map_image.ps1 b/.task8-sweep/tests/verify_planning_map_image.ps1 deleted file mode 100644 index b674c57..0000000 --- a/.task8-sweep/tests/verify_planning_map_image.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' -$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) -$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) -$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) -$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) -$imageRequestType = $assembly.GetType($ns + 'PlanningMapImageExportRequest', $true) -$exporterType = $assembly.GetType($ns + 'PlanningMapImageExporter', $true) -$mapRequest = [Activator]::CreateInstance($requestType) -$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]1000, [single]0, [single]1000)) -$mapRequest.ResolutionMm = [single]50 -$mapRequest.ObstacleSources = [Array]::CreateInstance($sourceType, 0) -$mapRequest.AllowExplicitEmptyMap = $true -$map = [Activator]::CreateInstance($factoryType).Create($mapRequest).Map -$imageRequest = [Activator]::CreateInstance($imageRequestType) -$imageRequest.Map = $map -$imageRequest.OutputRootDirectory = (Join-Path $PSScriptRoot '..\obj\planning_map_image_test') -$export = $exporterType.GetMethod('ExportIfEnabled').Invoke($null, @($true, $imageRequest)) -Assert-True $export.Saved ('Image export failed: ' + $export.Message) -Assert-True (Test-Path -LiteralPath $export.FilePath -PathType Leaf) 'Image output is missing.' -$png = [IO.File]::ReadAllBytes($export.FilePath) -Assert-True ($png.Length -gt 45) 'PNG is too small.' -Assert-True ($png[0] -eq 137 -and $png[1] -eq 80 -and $png[2] -eq 78 -and $png[3] -eq 71) 'PNG signature is invalid.' -Assert-True ([Text.Encoding]::ASCII.GetString($png, 12, 4) -eq 'IHDR') 'PNG must begin with IHDR.' -Assert-True ([Text.Encoding]::ASCII.GetString($png, $png.Length - 8, 4) -eq 'IEND') 'PNG must end with IEND.' -$source = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\Visualization\PlanningMapImageExporter.cs') -Raw -Encoding UTF8 -Assert-True ($source -notmatch 'GridMapData|TrapMapVehiclePose|TwoLegDetect') 'New exporter must consume only PlanningGridMap.' -Write-Output 'Planning map image checks passed.' diff --git a/.task8-sweep/tests/verify_planning_map_test_config.ps1 b/.task8-sweep/tests/verify_planning_map_test_config.ps1 deleted file mode 100644 index ad52397..0000000 --- a/.task8-sweep/tests/verify_planning_map_test_config.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -param([string]$SourcePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\MovementTest.MapTest.cs')) - -$ErrorActionPreference = 'Stop' -$source = Get-Content -LiteralPath $SourcePath -Raw - -function Assert-Contains([string]$Expected, [string]$Message) { - if (-not $source.Contains($Expected)) { throw "$Message Missing=$Expected" } -} - -Assert-Contains 'EnableTerminalDebugLog' 'MapTest must expose a terminal logging switch.' -Assert-Contains 'SavePng' 'MapTest must expose a PNG export switch.' -$begin = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5F00, 0x59CB) -$sources = -join [char[]](0x969C, 0x788D, 0x7269, 0x6765) -$projection = -join [char[]](0x6295, 0x5F71, 0x7ED3, 0x679C) -$snapshot = -join [char[]](0x5730, 0x56FE, 0x5FEB, 0x7167) -$end = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5B8C, 0x6210) -Assert-Contains 'FormatSourceStatus' 'MapTest must translate projection status for terminal output.' -Assert-Contains 'FormatCacheHit' 'MapTest must translate cache hits for terminal output.' -Assert-Contains 'FormatImageResult' 'MapTest must translate image export results for terminal output.' -if ($source.Contains('PLANNING_MAP_CREATE_BEGIN')) { throw 'MapTest must not expose English terminal log markers.' } -Assert-Contains 'PlanningMapFactory' 'MapTest must continue using the public map facade.' - -Write-Output 'Planning map MovementTest configuration checks passed.' diff --git a/ClumsyPilot/ClumsyPilot.csproj b/ClumsyPilot/ClumsyPilot.csproj index 0832f10..19e3dd8 100644 --- a/ClumsyPilot/ClumsyPilot.csproj +++ b/ClumsyPilot/ClumsyPilot.csproj @@ -22,6 +22,7 @@ + @@ -29,6 +30,11 @@ + + + + diff --git a/ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html b/ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html deleted file mode 100644 index 6269f51..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html +++ /dev/null @@ -1,812 +0,0 @@ - - - - - -AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案 - - - -

-

AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案

-

- 面向四舵轮AMR的第一阶段工程实现:暂不考虑蟹行与纯横移,仅采用车式运动模型, - 支持前进、倒车及换向,输出供后续SQP使用的无时间空间粗路径。 - 本文统一整理已经确认的第1至第14步,作为软件设计、编码、调试与验收依据。 -

-
- 步骤1–12:已确认 - 步骤13:性能优化TODO - 步骤14:接口与验收已确认 - 地图分辨率初值:0.05 m - 原语长度:0.50 m - 积分步长:0.05 m -
-
- -
- - -
-
-

0. 总体概览

-
-
车式运动
车头/车尾方向运动,不考虑蟹行
-
前进 + 倒车
允许在原语边界切换方向
-
空间粗路径
不包含速度、加速度、时间戳
-
- -

0.1 总体职责边界

- - - - - - -
模块主要职责不负责
Hybrid A*拓扑可达、无碰撞、基本运动学可行、前进/倒车结构、目标位置与航向接近最终速度、加速度、时间、四轮舵角与轮速
路径后处理回溯、稠密点恢复、关键点保留、弧长重采样、快速曲线平滑、完整复核动态约束和最终轨迹时间参数化
后续SQP终点精确收敛、速度/加速度/角速度/时间戳、换向点停车、动态可跟踪性重新决定绕障侧或凭空增加倒车拓扑
底盘逆解与控制四轮舵角、轮速分配和轨迹跟踪全局绕障搜索
- -

0.2 总体数据流

-
-
感知障碍物 / 点云 → 过滤、投影到二维
-
二维占据栅格 + 障碍物距离场
-
起点、终点、车辆参数、规划配置
-
二维Dijkstra启发图 + Hybrid A*搜索
-
机会式/强制 Reeds–Shepp 终点连接
-
父节点回溯 + 0.05 m内部积分点恢复
-
关键点保留 + 分方向弧长重采样
-
三次B样条优先,局部Bézier/五次多项式,局部QP兜底
-
最终碰撞、净空、曲率、换向结构校验
-
输出空间粗路径 → 后续SQP
-
- -
-第一版核心原则:先保证算法完整跑通、路径正确、可解释、可复现;性能优化整体放入第十三步TODO,待前12步稳定后再开展。 -
-
- -
-

1任务边界与输出定义 已确认

-

1.1 车辆运动模式

-

目标平台为四舵轮AMR,但第一阶段只使用车式运动能力:

-
    -
  • 车辆沿车头或车尾方向运动;
  • -
  • 允许前进、倒车以及换向;
  • -
  • 暂不考虑蟹行、纯横移和任意方向全向运动;
  • -
  • 车体航向角始终表示车头朝向,即使车辆正在倒车。
  • -
- -

1.2 Hybrid A*最小输出

-

输出空间路径点序列,至少包含:

-
位置:x, y
-车体航向:heading
-运动方向:Forward / Reverse
-累计弧长:s
-参考/几何曲率:kappa
-换向标记:isGearSwitchPoint
-安全净空:clearance
-

不输出速度、加速度、角速度、时间戳、四个舵轮角度与轮速。

- -

1.3 成功判据

-
    -
  • 路径全程无碰撞且不进入未知区域;
  • -
  • 粗路径基本满足最大曲率约束;
  • -
  • 包含正确绕障侧、前进/后退和换向结构;
  • -
  • 终点位置与航向满足第九步终止规则;
  • -
  • 可作为后续SQP的有效初值。
  • -
-
- -
-

2地图表达与环境输入 已确认

-

2.1 地图生成流程

-
原始点云 / 障碍信息 - ↓ 过滤噪声、地面、离群点 -投影到二维平面 - ↓ -二维占据栅格 Occupancy Grid - ↓ -障碍物距离场 Distance Field / EDT
- -

2.2 栅格定义

-
    -
  • 地图分辨率初值:0.05 m
  • -
  • 状态:Free、Occupied、Unknown;
  • -
  • 第一版中Unknown按障碍物处理;
  • -
  • 地图包含Origin、Resolution、Width、Height及Version;
  • -
  • 算法内部不得写死分辨率,统一读取地图参数。
  • -
- -

2.3 车体碰撞模型

-

AMR使用旋转矩形车体碰撞模型,不能只用中心点或单圆近似。安全余量主要加到车体矩形上:

-
CheckLength = VehicleLength + 2 × SafetyMargin
-CheckWidth  = VehicleWidth  + 2 × SafetyMargin
-

初始安全余量为0.03 m。避免地图膨胀与车体扩大同时重复计算同一余量。

-
- -
-

3车辆运动模型与最大曲率 已确认

-

3.1 参考点

-

车辆状态参考点采用AMR几何中心。

- -

3.2 基于路径弧长的运动学

-
dx/ds = cos(theta)
-dy/ds = sin(theta)
-dtheta/ds = kappa
-

其中theta为车头航向角,kappa为车体中心路径曲率。

- -

3.3 最大曲率来源

-

必须同时支持两种来源:

-
    -
  1. 外部直接输入MaxCurvature,或输入MinTurningRadius并换算kappa_max = 1 / R_min
  2. -
  3. 由四轮转向几何调用ComputeMaxCurvature(VehicleKinematicParameters)计算。
  4. -
-

推荐三种模式:

-
ExternalOnly
-GeometryOnly
-ConservativeMinimum
-

当两种来源同时存在时,建议取更保守的较小值。若均无有效值,则返回配置错误。

- -
-曲率变化率不在本步骤强制建模。第一版先保证曲率不超过上限;渐变曲率原语和实车舵轮速率约束作为后续升级。 -
-
- -
-

4Hybrid A*搜索节点状态 已确认

-

4.1 连续状态

-
(x, y, theta, direction, curvatureIndex)
-
    -
  • x, y, theta保持连续值用于运动积分;
  • -
  • direction为Forward或Reverse;
  • -
  • curvatureIndex表示当前离散曲率等级。
  • -
- -

4.2 Closed Set键

-
(ix, iy, iHeading, direction, curvatureIndex)
-

推荐初始离散:

-
    -
  • 位置Closed Set分辨率:0.05–0.10 m
  • -
  • 航向离散:
  • -
  • 方向和曲率等级均进入键,避免把运动状态不同的节点错误合并。
  • -
- -

4.3 搜索元数据

-

节点还应保存G/H/F代价、父节点索引、生成当前节点的原语积分点、最小净空、终止原因等。这些属于搜索管理信息,不属于车辆物理状态。

-
- -
-

5运动原语与扩展规则 已确认

-

5.1 第一版原语集合

-
{ -kappa_max, -0.5 kappa_max, 0, 0.5 kappa_max, kappa_max }
-

每种曲率均支持Forward与Reverse。

- -

5.2 曲率切换限制

-

相邻原语曲率等级最多变化1级。例如:

-
允许:0 → 0.5κmax
-允许:0.5κmax → κmax
-不允许:κmax → -κmax
-

该限制可减少“左打死后立即右打死”的不合理跳变。

- -

5.3 换向规则

-
    -
  • 前进/倒车只能在运动原语边界切换;
  • -
  • 起始曲率若上层可提供则使用实际值,否则默认0;
  • -
  • 第一版采用恒定曲率原语。
  • -
- -
-TODO:当恒定曲率原语造成曲率跳变过大、平滑后仍不可跟踪或实车舵轮变化速度不足时,增加线性渐变曲率原语。 -
-
- -
-

6原语长度、积分与终止 已确认

-

6.1 固定参数

-
PrimitiveLength = 0.50 m
-IntegrationStep = 0.05 m
-

完整原语最多包含10个内部积分点。只有原语终点进入Open List,内部积分点用于碰撞、净空和最终路径恢复。

- -

6.2 终止条件

- - - - - - - -
条件处理
累计长度达到0.50 m生成正常候选节点
任一积分点碰撞原语无效,立即终止
越界或进入未知区域原语无效
中途满足目标规则提前终止并保存实际有效段
Reeds–Shepp连接成功结束搜索
- -
-TODO:后续根据障碍物距离、目标距离和窄通道自动调整原语长度;碰撞积分步长仍保持0.05 m。 -
-
- -
-

7碰撞检测与航向栅格模板 已确认

-

7.1 两级碰撞检测

-
    -
  1. 距离场快速放行:若车辆中心到最近障碍物距离大于扩大车体外接圆半径及离散补偿,则该点必然安全;
  2. -
  3. 旋转矩形模板精确检测:距离不足以保证安全时,检查当前航向下扩大车体覆盖的所有栅格。
  4. -
- -

7.2 外接圆仅用于“安全放行”

-
R_outer = 0.5 × sqrt(CheckLength² + CheckWidth²)
-

D > R_outer表示安全;D ≤ R_outer只表示“无法确定”,不等于碰撞,必须进入矩形模板检查。

- -

7.3 航向栅格模板的准备

-

航向分辨率5°时,在初始化阶段自动生成72组模板:

-
0°, 5°, 10°, ... , 355°
-

每个模板保存旋转后的扩大车体矩形与地图栅格相交所对应的相对栅格偏移:

-
GridOffset { Dx, Dy }
-

运行时把当前车辆中心转换为栅格坐标,选择最近航向模板,将模板中的相对偏移平移到当前位置并查询占据状态。

- -

7.4 模板生成规则

-
    -
  • 模板以车体几何中心为原点;
  • -
  • 车体尺寸包含安全余量与少量离散补偿;
  • -
  • 只要栅格方块与旋转矩形有交集,就加入模板;
  • -
  • 不能只判断栅格中心是否在矩形内部,否则可能漏掉边角碰撞;
  • -
  • 真实车体始终是矩形,模板呈阶梯状只是栅格化结果。
  • -
- -

7.5 每个积分点的处理

-
读取距离场
-  ↓
-可安全放行?——是→继续
-  ↓否
-选择航向模板
-  ↓
-平移模板并查询Occupied / Unknown / 越界
-  ↓
-任一命中→碰撞;全部通过→安全
-
- -
-

8搜索代价与启发函数 已确认

-

8.1 节点排序

-
f(n) = g(n) + w_h × h(n)
-

第一版HeuristicWeight = 1.3

- -

8.2 累计代价

-
g_new =
-g_parent
-+ C_length
-+ C_reverse
-+ C_switch
-+ C_curvature
-+ C_curvatureChange
-+ C_obstacle
-
    -
  • 路径长度是基础代价;
  • -
  • 后退系数初值ReversePenalty = 1.3
  • -
  • 换向固定惩罚初值GearSwitchPenalty = 2.0
  • -
  • 曲率代价轻度抑制长期极限转向;
  • -
  • 曲率变化代价偏好平缓动作序列;
  • -
  • 障碍物代价让路径尽量远离障碍物。
  • -
- -

8.3 双启发

-
h = max(h_2D, h_RS)
-
    -
  • h_2D:从目标反向运行二维Dijkstra得到的绕障距离;
  • -
  • h_RS:基于当前位姿、目标位姿和最小转弯半径的Reeds–Shepp距离。
  • -
-

所有权重必须外部可配置。

-
- -
-

9终点判定、SQP精修与Reeds–Shepp 已确认

-

9.1 基本终点容差

-
GoalPositionTolerance = 0.15 m
-GoalHeadingTolerance  = 5°
-

在每个0.05 m内部积分点检查位置和航向误差,避免越过目标。

- -

9.2 容差到达与SQP

-

Hybrid A*可以在容差范围内结束,由后续SQP通过终端约束精确收敛到目标。但必须满足:

-
    -
  • 粗路径已包含正确绕障、倒车和换向拓扑;
  • -
  • 当前终点、目标位姿及末端局部区域无碰撞;
  • -
  • 终点附近存在足够局部调整空间;
  • -
  • SQP只负责局部修正,不负责创造新的换向或改变绕障侧。
  • -
- -

9.3 目标进入方向

-
GoalDirectionConstraint = Any | Forward | Reverse
-

默认Any。该字段约束最后一段运动方向,不改变目标车头航向定义。

- -

9.4 Reeds–Shepp模式

- - - - - -
模式连接失败后的行为适用场景
Disabled不尝试,满足容差即可结束开阔终点、调试对照
Opportunistic失败继续搜索,满足容差仍可交SQP默认、普通导航
RequiredNearGoal失败不能按容差结束,必须继续找可连接节点狭窄钻入、精确泊入
- -

9.5 自动触发

-
2–5 m:每扩展10个节点尝试一次
-≤2 m:每扩展3个节点尝试一次
-采样碰撞步长:0.05 m
-

连接路径必须检查地图边界、Unknown、扩大车体碰撞、安全净空、最大曲率、方向约束及与当前曲率的衔接。

- -

9.6 默认策略

-
ReedsSheppMode = Opportunistic
-

无需人工每次开启,由程序自动触发。任务类型可覆盖默认模式。

-
- -
-

10搜索回溯与原始稠密路径 已确认

-

10.1 回溯内容

-

搜索成功后从终止节点沿父节点索引回溯到起点,反转节点链,并恢复每条原语保存的0.05 m内部积分点。

- -

10.2 不能只输出0.5 m搜索节点

-

只输出原语终点会丢失圆弧形状、碰撞检查细节和换向局部结构。因此必须复用搜索时已经计算的内部点。

- -

10.3 路径点字段

-
x, y
-heading, unwrappedHeading
-curvature
-direction
-arcLength
-clearance
-isGearSwitchPoint
-source
- -

10.4 特殊处理

-
    -
  • 删除相邻原语重复边界点;
  • -
  • 中途终止原语只保留真实有效积分点;
  • -
  • Reeds–Shepp连接点追加到普通搜索路径之后;
  • -
  • 机会式容差终点不允许在本步骤被“硬改”为目标点;
  • -
  • 航向同时保存归一化值和连续解缠值;
  • -
  • 累计弧长始终单调增加,方向单独保存;
  • -
  • 按换向点划分Forward/Reverse连续段。
  • -
-
- -
-

11关键点保留与弧长重采样 已确认

-

11.1 处理目标

-

原始0.05 m稠密路径不直接全部送入平滑和SQP。先保留关键结构,再按照累计弧长重采样。

- -

11.2 必须保留的关键点

-
    -
  • 起点与终点;
  • -
  • 所有换向点;
  • -
  • 曲率等级变化点;
  • -
  • 直线/弯道切换点;
  • -
  • Reeds–Shepp分段和连接点;
  • -
  • 障碍物附近最小净空点;
  • -
  • 必要的明显航向变化点。
  • -
- -

11.3 分方向重采样

-

每个Forward或Reverse段独立处理,不能跨越换向点插值。

- -

11.4 推荐间距

-
普通区域:0.10 m
-重点区域:0.05 m
-

以下区域采用0.05 m:

-
    -
  • 障碍距离小于0.50 m;
  • -
  • |κ| ≥ 0.5 κmax;
  • -
  • 目标2 m范围内;
  • -
  • 换向点前后0.30 m;
  • -
  • 曲率变化点前后0.25 m;
  • -
  • Reeds–Shepp连接段与窄通道。
  • -
- -

11.5 重采样后复核

-

新插值点必须重新执行车体碰撞检查。最终输出点可为0.10 m,但碰撞验证内部步长仍不得大于0.05 m。

-
- -
-

12快速曲线平滑、局部QP兜底与最终校验 已确认

-

12.1 最终采用的分层方案

-
-
按Forward/Reverse方向段拆分
-
普通路径段:三次B样条快速平滑
-
局部短转角:分段三次Bézier
-
末端短连接:五次多项式(可选)
-
0.05 m完整碰撞、净空、曲率验证
-
失败局部:安全走廊约束QP
-
局部QP仍失败:回退第十一步原始可行路径
-
- -

12.2 为什么不默认全路径QP

-

全路径多轮QP还需要走廊生成、曲率线性化和反复碰撞检测,可能与后续SQP功能重复并增加耗时。因此第一版采用快速曲线优先,仅对失败局部使用QP。

- -

12.3 平滑结构约束

-
    -
  • 换向点固定,不能使用一条曲线跨越换向;
  • -
  • 不改变绕障侧、前进/后退顺序和换向次数;
  • -
  • Reeds–Shepp精确目标点固定;
  • -
  • 容差终点不在本步骤强行修改为目标;
  • -
  • 障碍物附近控制点移动范围收紧。
  • -
- -

12.4 典型偏移范围

-
普通区域最大偏移:0.10–0.15 m
-障碍物附近最大偏移:0.02–0.05 m
-局部QP问题区间:失败点前后各约1.0 m
- -

12.5 航向与曲率

-
    -
  • 平滑后必须根据新几何路径重新计算切线与航向;
  • -
  • 前进段车头航向与路径切线一致;
  • -
  • 倒车段车头航向与路径切线相差π;
  • -
  • 区分几何曲率与车辆控制曲率;
  • -
  • 建议平滑后最大车辆曲率不超过0.95 κmax
  • -
- -

12.6 最终验证

-
    -
  • 0.05 m采样完整车体碰撞检测;
  • -
  • Unknown与越界检查;
  • -
  • 安全余量检查;
  • -
  • 最大曲率与曲率变化检查;
  • -
  • 起终点、换向点、方向分段和路径连续性检查。
  • -
-
- -
-

13性能优化阶段 整体TODO

-

第十三步不作为第一版主流程实现要求。前12步完整跑通并建立正确性基线后,再进行性能剖析与针对性优化。

- -

13.1 第一版仅保留防失控措施

-
    -
  • 较宽松的搜索超时;
  • -
  • 最大扩展节点和最大生成节点限制;
  • -
  • 取消请求机制;
  • -
  • 各阶段基础耗时与节点数量统计。
  • -
- -

13.2 后续性能TODO

-
    -
  1. 统计地图处理、Dijkstra、Hybrid A*、碰撞检测、Reeds–Shepp、回溯和平滑耗时;
  2. -
  3. 根据剖析结果识别真实瓶颈;
  4. -
  5. 优化Open List、Closed Set、节点内存与对象分配;
  6. -
  7. 预计算运动原语、航向模板、距离场和启发图;
  8. -
  9. 地图裁剪、路径热启动、增量更新、多分辨率搜索;
  10. -
  11. 最终验证平均、P95、最坏时间和100 ms达成率。
  12. -
- -
-开发顺序:正确性 → 可行性 → 可复现性 → 性能剖析 → 针对性优化 → 100 ms验收。 -
-
- -
-

14输入输出接口、错误状态与验收 已确认

-

14.1 推荐模块

-
HybridAStarPlanner
-├── MapProcessor
-├── VehicleModel
-├── MotionPrimitiveGenerator
-├── CollisionChecker
-├── HeuristicProvider
-├── ReedsSheppConnector
-├── GoalChecker
-├── PathBacktracker
-├── PathResampler
-├── PathSmoother
-├── PathValidator
-└── PlanningDiagnostics
- -

14.2 主接口

-
public interface IHybridAStarPlanner
-{
-    PlanningResult Plan(PlanningRequest request);
-}
- -

14.3 输入

-
    -
  • 二维占据栅格;
  • -
  • 起点位姿与可选起始方向、起始曲率;
  • -
  • 目标位姿与目标进入方向;
  • -
  • 车辆尺寸、安全余量、最大曲率/最小转弯半径;
  • -
  • Hybrid A*配置、Reeds–Shepp模式和平滑配置。
  • -
- -

14.4 输出

-

输出不带时间的空间路径点和分段信息:

-
X, Y
-Heading, UnwrappedHeading
-ArcLength
-GeometricCurvature
-VehicleCurvature
-Direction
-BodyClearance
-IsGearSwitchPoint
-Source
- -

14.5 终点到达类型

-
ExactByReedsShepp
-ReachedWithinTolerance
-ExactAfterDirectSearch
- -

14.6 主要返回状态

-
Success
-SuccessWithToleranceGoal
-SuccessWithSmoothingFallback
-InvalidRequest / InvalidMap / InvalidVehicleParameters
-StartOutsideMap / StartInCollision / StartInUnknownArea
-GoalOutsideMap / GoalInCollision / GoalInUnknownArea
-InvalidCurvatureConfiguration
-SearchTimeout / SearchNodeLimitExceeded / NoFeasiblePath
-ReedsSheppRequiredButFailed
-BacktrackingFailed / ResamplingFailed / FinalValidationFailed
-Cancelled / InternalError
- -

14.7 第一版验收重点

-
    -
  • 有效场景能输出完整路径;
  • -
  • 起点一致,终点满足第九步规则;
  • -
  • 完整车体全程无碰撞且不进入Unknown;
  • -
  • 最大曲率不超过车辆上限;
  • -
  • 前进、倒车和换向结构正确;
  • -
  • 回溯无断点和异常重复;
  • -
  • 平滑失败可以回退原路径;
  • -
  • 失败场景返回明确状态;
  • -
  • 后续SQP能够读取并收敛。
  • -
-
- -
-

附录A:第一版推荐配置参数

- - - - - - - - - - - - - - - - - - - - - - - - - - -
参数推荐初值说明
MapResolution0.05 m读取地图配置,不写死
HeadingResolution72个航向模板
SafetyMargin0.03 m主要加在车体矩形
PrimitiveLength0.50 m第一版固定
IntegrationStep0.05 m碰撞与积分步长
CurvatureLevels-1,-0.5,0,0.5,1 × κmax5级
GoalPositionTolerance0.15 mHybrid A*基础容差
GoalHeadingToleranceHybrid A*基础容差
HeuristicWeight1.3Weighted A*
ReversePenalty1.3允许倒车但略微惩罚
GearSwitchPenalty2.0减少频繁换向
ReedsSheppModeOpportunistic默认自动机会式
AnalyticExpansionDistance5.0 m进入后周期尝试
NearGoalDistance2.0 m提高尝试频率
AnalyticExpansionInterval10节点2–5 m
NearGoalInterval3节点≤2 m
NormalResampleSpacing0.10 m普通区域
FineResampleSpacing0.05 m重点区域
FineObstacleDistance0.50 m小于此值加密
FineGoalDistance2.0 m目标附近加密
GearSwitchDenseRange±0.30 m换向点附近
CurvatureTransitionDenseRange±0.25 m曲率切换附近
CurvatureLimitRatio0.95平滑后留控制余量
SearchTimeLimitMs3000–5000 ms第一版防失控,不是性能指标
-
- -
-

附录B:总体伪代码

-
PlanningResult Plan(request)
-{
-    ValidateRequest(request);
-    ValidateMap(request.Map);
-    ResolveVehicleCurvatureLimit(request.Vehicle);
-    ValidateStartAndGoal();
-
-    PrepareDistanceFieldIfNeeded();
-    PrepareHeadingFootprintTemplatesIfNeeded();
-    BuildGoalDijkstraHeuristic();
-
-    startNode = CreateStartNode();
-    PushOpen(startNode);
-
-    while (OpenList not empty)
-    {
-        CheckCancellationAndSafetyLimits();
-
-        current = PopBestValidNode();
-
-        if (ShouldTryReedsShepp(current))
-        {
-            rsPath = TryReedsShepp(current, goal);
-            if (ValidateAnalyticPath(rsPath))
-                return BuildFinalResult(current, rsPath);
-        }
-
-        if (GoalChecker.CanTerminateByTolerance(current))
-            return BuildFinalResult(current, noAnalyticPath);
-
-        foreach (primitive in GenerateAllowedPrimitives(current))
-        {
-            integrated = IntegratePrimitive(
-                current,
-                primitive,
-                step = 0.05 m,
-                maxLength = 0.50 m);
-
-            if (!integrated.Valid)
-                continue;
-
-            child = CreateChildNode(integrated);
-            if (!ImproveBestCost(child))
-                continue;
-
-            PushOpen(child);
-        }
-    }
-
-    return Failure(NoFeasiblePath);
-}
-
-BuildFinalResult(goalNode, analyticPath)
-{
-    rawDense = BacktrackAndRestorePrimitivePoints(goalNode);
-    AppendAnalyticPathIfAny(rawDense, analyticPath);
-
-    segmented = SplitByMotionDirection(rawDense);
-    resampled = PreserveKeyPointsAndResample(segmented);
-
-    smoothed = TryFastSplineSmoothing(resampled);
-    if (!ValidatePath(smoothed))
-        smoothed = TryLocalCurveOrQPFallback(resampled);
-
-    if (!ValidatePath(smoothed))
-        smoothed = resampled;
-
-    if (!ValidatePath(smoothed))
-        return Failure(FinalValidationFailed);
-
-    return Success(smoothed, diagnostics);
-}
-
- -
-

附录C:建议开发里程碑

- - - - - - - - - - - -
阶段范围完成标准
M0 数据与工具Pose、地图、车辆参数、角度/坐标工具单元测试通过
M1 运动学与碰撞恒曲率积分、航向模板、矩形碰撞可视化验证不同角度无漏检
M2 最小Hybrid A*无障碍、只前进、基础启发稳定从起点到终点
M3 障碍与倒车Dijkstra、前进/倒车、换向代价绕障与一次倒车场景通过
M4 终点模块容差、GoalDirection、Reeds–Shepp三模式普通与狭窄目标场景通过
M5 后处理回溯、0.05 m稠密点、分段、重采样路径结构无断点、换向明确
M6 平滑与复核B样条、Bézier、局部QP兜底不改变拓扑,失败可回退
M7 接口与集成PlanningRequest/Result、状态、诊断可接入SQP
M8 性能TODO剖析、优化、100 ms目标在正确性基线后执行
-
-
-
- -
- 文档用途:四舵轮AMR第一阶段非结构化道路粗路径规划开发依据。内部单位统一采用米、弧度、1/米。 -
- - diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/CorridorConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/CorridorConfiguration.cs index 0f0cbe2..159222a 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/CorridorConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/CorridorConfiguration.cs @@ -1,13 +1,34 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 静态走廊采样、偏移和净空配置;距离单位均为 m。 +/// public sealed class CorridorConfiguration { + /// + /// 沿参考弧长的走廊采样间距;默认值 0.10 m,必须为正有限值,见 。 + /// public double LongitudinalSampleSpacingMeters { get; set; } + /// + /// 每个采样站横向搜索的间距;默认值 0.025 m,必须为正有限值,见 。 + /// public double LateralSampleSpacingMeters { get; set; } + /// + /// 相对参考线允许搜索的最大横向偏移;单位 m,默认值 0.30,必须为正有限值。 + /// public double MaximumLateralOffsetMeters { get; set; } + /// + /// 碰撞检测之外预留的附加净空;单位 m,默认值 0.02,必须为非负有限值。 + /// public double AdditionalClearanceReserveMeters { get; set; } + /// + /// 足迹碰撞检测的最大行进步长;单位 m,默认值 0.025,必须为正有限值。 + /// public double MaximumCollisionCheckStepMeters { get; set; } + /// + /// 复制当前走廊配置;输入为当前五个标量,输出为无共享可变状态的可修改快照,不对数值作校验且不产生失败状态。 + /// internal CorridorConfiguration Copy() { return new CorridorConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs index 5840009..44fbf33 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs @@ -1,15 +1,42 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// EM 规划器的完整可变配置树;所有子配置均须非空,建议通过 创建协调且彼此独立的默认快照。 +/// public sealed partial class EmPlannerConfiguration { + /// + /// 重规划节奏、时间窗口和资源上限配置;无直接单位,必须为非空的 ,默认值为新的调度配置对象。 + /// public SchedulingConfiguration Scheduling { get; set; } + /// + /// 世界坐标至 Frenet 坐标的投影容差配置;无直接单位,必须为非空的 ,默认值为新的 Frenet 配置对象。 + /// public FrenetConfiguration Frenet { get; set; } + /// + /// 静态无碰撞走廊的采样与净空配置;无直接单位,必须为非空的 ,默认值为新的走廊配置对象。 + /// public CorridorConfiguration Corridor { get; set; } + /// + /// LS 横向优化的约束与权重配置;无直接单位,必须为非空的 ,默认值为新的横向配置对象。 + /// public LateralConfiguration Lateral { get; set; } + /// + /// ST 纵向优化的速度、舒适性与权重配置;无直接单位,必须为非空的 ,默认值为新的纵向配置对象。 + /// public LongitudinalConfiguration Longitudinal { get; set; } + /// + /// QP 求解器的迭代次数、容差和运行选项;无直接单位,必须为非空的 ,默认值为新的求解器配置对象。 + /// public SolverConfiguration Solver { get; set; } + /// + /// 发布前几何、运动学和终端姿态校验容差;无直接单位,必须为非空的 ,默认值为新的校验配置对象。 + /// public ValidationConfiguration Validation { get; set; } + /// + /// 创建用于低速泊车的全套默认配置;每次调用均构造新的子配置和权重对象,因此调用方可修改返回值而不影响其他默认快照。 + /// public static EmPlannerConfiguration CreateDefault() { return new EmPlannerConfiguration @@ -17,8 +44,8 @@ public sealed partial class EmPlannerConfiguration Scheduling = new SchedulingConfiguration { ReplanPeriodSeconds = 0.20d, - TimeHorizonSeconds = 6d, //窗口的规划时间长度。单位s - DistanceHorizonMeters = 500d, //最大探索s里程距离,单位m + TimeHorizonSeconds = 6d, // 单次规划的时间窗口,单位 s。 + DistanceHorizonMeters = 500d, // 单次规划的最大前向探索距离,单位 m。 OutputTimeStepSeconds = 0.05d, SolverTimeoutSeconds = 0.10d, HandoffLookaheadSeconds = 0.30d, @@ -103,6 +130,9 @@ public sealed partial class EmPlannerConfiguration }; } + /// + /// 深复制当前配置树;输入为当前实例状态,输出与当前实例不共享非空嵌套对象的可修改快照,原本为 null 的子配置保持为 null 且不产生失败状态。 + /// internal EmPlannerConfiguration Copy() { return new EmPlannerConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/FrenetConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/FrenetConfiguration.cs index 2aa3c0a..56a12dc 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/FrenetConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/FrenetConfiguration.cs @@ -1,11 +1,26 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 世界坐标与 Frenet 参考之间投影和边界判定的容差配置。 +/// public sealed class FrenetConfiguration { + /// + /// 世界姿态投影到方向段所允许的最大欧氏距离;单位 m,默认值 0.50,必须为正有限值。 + /// public double MaximumProjectionDistanceMeters { get; set; } + /// + /// Frenet 重建中 1-kappa*l 的最小绝对安全余量;无量纲,默认值 0.20,必须为大于 0 且小于 1 的有限值。 + /// public double MinimumFrenetDenominator { get; set; } + /// + /// 判断投影是否锚定在段边界的距离容差;单位 m,默认值 1e-8,必须为正有限值。 + /// public double BoundaryAnchorToleranceMeters { get; set; } + /// + /// 复制当前 Frenet 配置;输入为当前三个标量,输出为无共享可变状态的标量快照,不对数值作校验且不产生失败状态。 + /// internal FrenetConfiguration Copy() { return new FrenetConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralConfiguration.cs index 6d622c5..63d628b 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralConfiguration.cs @@ -1,13 +1,34 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// LS 横向优化的离散、收敛、曲率和边界限制配置。 +/// public sealed class LateralConfiguration { + /// + /// 相邻外层迭代横向解之间允许的最大偏移量;单位 m,默认值 0.05,必须为正有限值。 + /// public double MaximumLateralStepPerIterationMeters { get; set; } + /// + /// 横向偏移对弧长的一阶导数上限;无量纲,默认值 0.50,必须为正有限值。 + /// public double MaximumLateralSlope { get; set; } + /// + /// 横向偏移对弧长的二阶导数上限;单位 1/m,默认值 1,必须为正有限值。 + /// public double MaximumLateralSecondDerivativePerMeter { get; set; } + /// + /// 横向偏移对弧长的三阶导数上限;单位 1/m²,默认值 2,必须为正有限值。 + /// public double MaximumLateralThirdDerivativePerSquareMeter { get; set; } + /// + /// 横向 QP 目标函数的权重集合;无直接单位,必须非空且其每项为非负有限值,默认值为新的 对象。 + /// public LateralWeights Weights { get; set; } + /// + /// 复制当前横向配置;输入为当前标量和权重引用,输出为不共享非空权重对象的可修改快照,权重为 null 时保持为空且不产生失败状态。 + /// internal LateralConfiguration Copy() { return new LateralConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralWeights.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralWeights.cs index ac98cdb..d953871 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralWeights.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LateralWeights.cs @@ -1,16 +1,46 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 横向 QP 目标函数的权重快照;权重只影响偏好,不放宽安全约束。 +/// public sealed class LateralWeights { + /// + /// 惩罚偏离参考线横向偏移的权重;无量纲,默认值 10,必须为非负有限值。 + /// public double ReferenceOffset { get; set; } + /// + /// 惩罚横向偏移一阶导数的权重;无量纲,默认值 1,必须为非负有限值。 + /// public double HeadingDeviation { get; set; } + /// + /// 惩罚横向偏移二阶导数的权重;无量纲,默认值 5,必须为非负有限值。 + /// public double SecondDerivative { get; set; } + /// + /// 惩罚横向偏移三阶导数的权重;无量纲,默认值 10,必须为非负有限值。 + /// public double ThirdDerivative { get; set; } + /// + /// 惩罚由横向解导出的车辆曲率的权重;无量纲,默认值 5,必须为非负有限值。 + /// public double Curvature { get; set; } + /// + /// 惩罚相邻站曲率变化的权重;无量纲,默认值 20,必须为非负有限值。 + /// public double CurvatureVariation { get; set; } + /// + /// 惩罚偏离上一轮横向轨迹的权重;无量纲,默认值 5,必须为非负有限值。 + /// public double PreviousTrajectory { get; set; } + /// + /// 滚动规划末端横向状态的稳定权重;无量纲,默认值 10,必须为非负有限值。 + /// public double RollingTerminal { get; set; } + /// + /// 复制当前横向权重;输入为当前八个标量,输出为无共享可变状态的标量快照,不对数值作校验且不产生失败状态。 + /// internal LateralWeights Copy() { return new LateralWeights diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs index 8c201e9..e03147f 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs @@ -1,20 +1,62 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// ST 纵向优化的速度、舒适性和终端保持限制配置;所有数值由 提供低速泊车默认值。 +/// public sealed class LongitudinalConfiguration { + /// + /// 前进方向允许的最大速度;单位 m/s,默认值 1,必须为正有限值。 + /// public double MaximumForwardSpeedMetersPerSecond { get; set; } + /// + /// 倒车方向允许的最大速度;单位 m/s,默认值 0.5,必须为正有限值。 + /// public double MaximumReverseSpeedMetersPerSecond { get; set; } + /// + /// 前进方向的目标巡航速度;单位 m/s,默认值 1,必须为正有限值且不得超过 。 + /// public double DesiredForwardSpeedMetersPerSecond { get; set; } + /// + /// 倒车方向的目标巡航速度;单位 m/s,默认值 0.5,必须为正有限值且不得超过 。 + /// public double DesiredReverseSpeedMetersPerSecond { get; set; } + /// + /// 允许的最大纵向加速度;单位 m/s²,默认值 0.20,必须为正有限值。 + /// public double MaximumAccelerationMetersPerSecondSquared { get; set; } + /// + /// 允许的最大纵向减速度幅值;单位 m/s²,默认值 0.30,必须为正有限值。 + /// public double MaximumDecelerationMetersPerSecondSquared { get; set; } + /// + /// 允许的最大纵向加加速度幅值;单位 m/s³,默认值 0.50,必须为正有限值。 + /// public double MaximumJerkMetersPerSecondCubed { get; set; } + /// + /// 由速度和曲率共同施加的最大横向加速度;单位 m/s²,默认值 0.20,必须为正有限值。 + /// public double MaximumLateralAccelerationMetersPerSecondSquared { get; set; } + /// + /// 车辆曲率随时间变化的上限;单位 1/(m·s),默认值 0.50,必须为正有限值。 + /// public double MaximumCurvatureRatePerMeterPerSecond { get; set; } + /// + /// 将进度速度视为停止的容差;单位 m/s,默认值 0.01,必须为非负有限值。 + /// public double StopSpeedToleranceMetersPerSecond { get; set; } + /// + /// 到达零速度后需保持的时长;单位 s,默认值 0.20,必须为正有限值。 + /// public double ZeroSpeedHoldSeconds { get; set; } + /// + /// 纵向 QP 目标函数的权重集合;无直接单位,必须非空且其每项为非负有限值,默认值为新的 对象。 + /// public LongitudinalWeights Weights { get; set; } + /// + /// 复制当前纵向配置;输入为当前标量和权重引用,输出为不共享非空权重对象的可修改快照,权重为 null 时保持为空且不产生失败状态。 + /// internal LongitudinalConfiguration Copy() { return new LongitudinalConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalWeights.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalWeights.cs index 6967f66..9d7f22f 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalWeights.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalWeights.cs @@ -1,13 +1,34 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 纵向 QP 目标函数的权重快照;权重只改变偏好而不放宽硬约束。 +/// public sealed class LongitudinalWeights { + /// + /// 惩罚偏离参考速度的权重;无量纲,默认值 10,必须为非负有限值。 + /// public double ReferenceSpeed { get; set; } + /// + /// 惩罚纵向加速度的权重;无量纲,默认值 1,必须为非负有限值。 + /// public double Acceleration { get; set; } + /// + /// 惩罚纵向加加速度的权重;无量纲,默认值 10,必须为非负有限值。 + /// public double Jerk { get; set; } + /// + /// 惩罚偏离上一条轨迹速度解的权重;无量纲,默认值 5,必须为非负有限值。 + /// public double PreviousTrajectory { get; set; } + /// + /// 惩罚末端加速度的权重;无量纲,默认值 1,必须为非负有限值。 + /// public double TerminalAcceleration { get; set; } + /// + /// 复制当前纵向权重;输入为当前五个标量,输出为无共享可变状态的标量快照,不对数值作校验且不产生失败状态。 + /// internal LongitudinalWeights Copy() { return new LongitudinalWeights diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs index 5116224..3f01605 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs @@ -1,19 +1,58 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// EM 规划的触发节奏、预测窗口、离散上限和发布样本上限配置;默认值由 为低速泊车协调。 +/// public sealed class SchedulingConfiguration { + /// + /// 两次规划触发之间的周期;单位 s,默认值 0.20,必须为正有限值。 + /// public double ReplanPeriodSeconds { get; set; } + /// + /// 单次优化覆盖的预测时间窗口;单位 s,默认值 6,必须为正有限值。 + /// public double TimeHorizonSeconds { get; set; } + /// + /// 单次优化覆盖的最大参考线距离;单位 m,默认值 500,必须为正有限值;滚动规划时还须覆盖制动距离及一个重规划周期内的行程。 + /// public double DistanceHorizonMeters { get; set; } + /// + /// 发布轨迹相邻样本的时间间隔;单位 s,默认值 0.05,必须为正有限值。 + /// public double OutputTimeStepSeconds { get; set; } + /// + /// 单次求解允许使用的超时预算;单位 s,默认值 0.10,必须为正有限值。 + /// public double SolverTimeoutSeconds { get; set; } + /// + /// 与已发布轨迹交接时向前查看的时长;单位 s,默认值 0.30,必须为正有限值。 + /// public double HandoffLookaheadSeconds { get; set; } + /// + /// 接受车辆状态输入的最大时效;单位 s,默认值 0.20,必须为正有限值。 + /// public double MaximumVehicleStateAgeSeconds { get; set; } + /// + /// 纵向优化结点允许的最大时间间距;单位 s,默认值 0.20,必须为正有限值。 + /// public double MaximumOptimizationTimeStepSeconds { get; set; } + /// + /// 横向或走廊优化结点允许的最大空间间距;单位 m,默认值 0.10,必须为正有限值。 + /// public double MaximumOptimizationSpatialStepMeters { get; set; } + /// + /// 一次优化允许的最大结点数量;单位为结点数,默认值 401,必须为不小于 3 的整数。 + /// public int MaximumOptimizationKnotCount { get; set; } + /// + /// 一条发布轨迹允许的最大样本数量;单位为样本数,默认值 5001,必须为不小于 2 的整数。 + /// public int MaximumPublishedSampleCount { get; set; } + /// + /// 复制当前调度配置;输入为当前标量限制,输出为无共享可变状态的标量快照,不对数值作校验且不产生失败状态。 + /// internal SchedulingConfiguration Copy() { return new SchedulingConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SolverConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SolverConfiguration.cs index b648664..7332fd8 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SolverConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SolverConfiguration.cs @@ -1,16 +1,46 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// LS/ST QP 求解器的迭代预算、收敛容差和运行选项。 +/// public sealed class SolverConfiguration { + /// + /// 外层顺序凸化最大迭代次数;默认值 5,必须为正整数。 + /// public int MaximumOuterIterations { get; set; } + /// + /// 单次 OSQP 求解的最大迭代次数;默认值 4000,必须为正整数。 + /// public int MaximumOsqpIterations { get; set; } + /// + /// 求解器绝对残差容差;默认值 1e-5,必须为正有限值。 + /// public double AbsoluteTolerance { get; set; } + /// + /// 求解器相对残差容差;默认值 1e-5,必须为正有限值。 + /// public double RelativeTolerance { get; set; } + /// + /// 发布前接受解的严格残差容差;默认值 1e-5,必须为正有限值。 + /// public double StrictResidualTolerance { get; set; } + /// + /// 是否将上一轮可用解作为初值;无单位,默认值 true,仅接受布尔值。 + /// public bool WarmStart { get; set; } + /// + /// 是否请求 OSQP 进行结果修正;无单位,默认值 true,仅接受布尔值。 + /// public bool Polish { get; set; } + /// + /// 是否启用原生求解器诊断输出;无单位,默认值 false,仅接受布尔值。 + /// public bool NativeVerbose { get; set; } + /// + /// 复制当前求解器配置;输入为当前迭代预算、容差和布尔选项,输出为无共享可变状态的标量快照,不对数值作校验且不产生失败状态。 + /// internal SolverConfiguration Copy() { return new SolverConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs index 0bd9da5..d20bbb2 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs @@ -1,12 +1,30 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 发布前轨迹几何、运动学和终端误差的验收容差配置;默认值由 提供。 +/// public sealed class ValidationConfiguration { + /// + /// 几何位置、弧长和采样比较使用的空间容差;单位 m,默认值 1e-8,必须为正有限值。 + /// public double SpatialToleranceMeters { get; set; } + /// + /// 速度、加速度和曲率等无量纲相对比较的容差;无量纲,默认值 1e-5,必须为正有限值。 + /// public double KinematicTolerance { get; set; } + /// + /// 精确终止模式下终端位置允许的误差;单位 m,默认值 0,必须为非负有限值。 + /// public double TerminalPositionToleranceMeters { get; set; } + /// + /// 精确终止模式下终端航向允许的误差;单位 rad,默认值 0,必须为 [0, π] 内的有限值。 + /// public double TerminalYawToleranceRadians { get; set; } + /// + /// 复制当前校验配置;输入为当前四个容差,输出为无共享可变状态的标量快照,不对数值作校验且不产生失败状态。 + /// internal ValidationConfiguration Copy() { return new ValidationConfiguration diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/ContractNumeric.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/ContractNumeric.cs index d8d0537..146799d 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/ContractNumeric.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/ContractNumeric.cs @@ -2,8 +2,14 @@ using System; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 验证规划契约中必须为有限数的标量输入。 +/// internal static class ContractNumeric { + /// + /// 拒绝 NaN 或无穷数值。 的单位由调用方语境决定; 原样传给异常构造器,方法本身不验证它。 + /// public static void RequireFinite(double value, string parameterName) { if (double.IsNaN(value) || double.IsInfinity(value)) diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmBoundaryType.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmBoundaryType.cs index 275901a..22ad4f0 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmBoundaryType.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmBoundaryType.cs @@ -1,10 +1,28 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 表示 EM 规划中参考段终端或滚动窗口终端的边界类型。 +/// public enum EmBoundaryType { + /// + /// 非边界采样点;不携带终端或换挡边界语义。 + /// None, + /// + /// 滚动窗口因安全约束形成的停车终点,而非方向段的真实终点。 + /// RollingSafetyStop, + /// + /// 到达换挡位置前的真实停车边界。 + /// GearSwitchApproach, + /// + /// 换挡后新方向段开始时的离开边界。 + /// GearSwitchDeparture, + /// + /// 整条参考路径或当前任务目标的真实终点边界。 + /// Goal, } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs index d94f77c..5353710 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs @@ -1,8 +1,20 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 指定纵向规划面对当前窗口末端时采用的速度终端语义。 +/// public enum EmLongitudinalMode { + /// + /// 普通滚动窗口末端;保持连续行驶,不要求在本窗口内停止。 + /// RollingContinuation, + /// + /// 将真实停车边界纳入窗口,但在可达性不足时仅以可停车方式接近。 + /// ApproachStopBoundary, + /// + /// 要求在当前窗口内到达真实边界并保持零速。 + /// ExactStopAtBoundary, } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmMotionModel.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmMotionModel.cs index 64fd731..3a765aa 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmMotionModel.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmMotionModel.cs @@ -1,8 +1,20 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 请求允许的车辆运动模型;服务依据此值选择或拒绝相应规划分支。 +/// public enum EmMotionModel { + /// + /// 常规非完整约束车辆,可前进或倒车但不能侧移。 + /// NonholonomicForwardReverse, + /// + /// 蟹行平移模型;当前 EM 管线不支持时返回明确状态。 + /// CrabTranslation, + /// + /// 原地旋转模型;当前 EM 管线不支持时返回明确状态。 + /// InPlaceRotation, } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs index ef6aad6..b6acfd5 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs @@ -1,12 +1,136 @@ using System; +using System.Diagnostics; +using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +internal interface IEmPlanningPublicationAuthorization +{ + TimeSpan Remaining { get; } + + bool IsExpired { get; } + + EmPlanningPublicationDecision TryPublish(CancellationToken requestCallerCancellationToken, + CancellationToken coordinatorCallerCancellationToken, Action publish); +} + +internal enum EmPlanningPublicationDecision +{ + Published = 0, + CallerCancelled = 1, + DeadlineExpired = 2, +} + +internal sealed class EmPlanningRequestPublicationAuthorization : IEmPlanningPublicationAuthorization +{ + private readonly object gate = new object(); + private readonly TimeSpan? initialRemaining; + private readonly Stopwatch stopwatch; + private readonly CancellationToken deadlineToken; + private bool callerCancelled; + private bool deadlineExpired; + + public EmPlanningRequestPublicationAuthorization(TimeSpan? remaining, CancellationToken deadlineToken) + { + initialRemaining = remaining; + this.deadlineToken = deadlineToken; + stopwatch = remaining.HasValue ? Stopwatch.StartNew() : null; + deadlineExpired = remaining.HasValue && remaining.Value <= TimeSpan.Zero || + deadlineToken.IsCancellationRequested; + } + + public TimeSpan Remaining + { + get + { + lock (gate) + { + ObserveDeadlineInsideGate(); + return ComputeRemainingInsideGate(); + } + } + } + + public bool IsExpired + { + get + { + lock (gate) + { + ObserveDeadlineInsideGate(); + return deadlineExpired; + } + } + } + + public EmPlanningPublicationDecision TryPublish(CancellationToken requestCallerCancellationToken, + CancellationToken coordinatorCallerCancellationToken, Action publish) + { + if (publish == null) + throw new ArgumentNullException(nameof(publish)); + + using CancellationTokenRegistration requestCallerRegistration = + requestCallerCancellationToken.Register(ObserveCallerCancellation); + using CancellationTokenRegistration coordinatorCallerRegistration = + coordinatorCallerCancellationToken.Register(ObserveCallerCancellation); + using CancellationTokenRegistration deadlineRegistration = + deadlineToken.Register(ObserveDeadlineCancellation); + lock (gate) + { + if (requestCallerCancellationToken.IsCancellationRequested || + coordinatorCallerCancellationToken.IsCancellationRequested) + { + callerCancelled = true; + } + ObserveDeadlineInsideGate(); + if (callerCancelled) + return EmPlanningPublicationDecision.CallerCancelled; + if (deadlineExpired) + return EmPlanningPublicationDecision.DeadlineExpired; + publish(); + return EmPlanningPublicationDecision.Published; + } + } + + private void ObserveCallerCancellation() + { + lock (gate) + callerCancelled = true; + } + + private void ObserveDeadlineCancellation() + { + lock (gate) + deadlineExpired = true; + } + + private void ObserveDeadlineInsideGate() + { + if (deadlineToken.IsCancellationRequested || ComputeRemainingInsideGate() <= TimeSpan.Zero) + deadlineExpired = true; + } + + private TimeSpan ComputeRemainingInsideGate() + { + if (!initialRemaining.HasValue) + return deadlineExpired ? TimeSpan.Zero : TimeSpan.MaxValue; + TimeSpan remaining = initialRemaining.Value - stopwatch.Elapsed; + return remaining > TimeSpan.Zero && !deadlineExpired ? remaining : TimeSpan.Zero; + } +} + +/// +/// EM 单次规划的只读输入引用快照,绑定平滑参考路径、地图、车辆状态、目标方向段和输出身份。 +/// 坐标使用 m,航向使用 rad,时间使用 UTC;调用方必须在进入服务前保证这些输入属于同一业务版本。 +/// public sealed class EmPlanningRequest { + /// + /// 保存一次规划所需的引用快照和发布身份。构造器只赋值而不校验、复制或取得对象所有权;调用方仍拥有传入引用,服务随后负责验证并复制配置。 + /// public EmPlanningRequest( PathSmoothingResult referencePath, PlanningGridMap map, @@ -21,7 +145,10 @@ public sealed class EmPlanningRequest string referencePathId, string previousTrajectoryId, EmMotionModel motionModel, - EmPlanningScope planningScope) + EmPlanningScope planningScope, + TimeSpan? cycleDeadlineRemaining = null, + CancellationToken callerCancellationToken = default, + CancellationToken cycleDeadlineToken = default) { ReferencePath = referencePath; Map = map; @@ -37,25 +164,166 @@ public sealed class EmPlanningRequest PreviousTrajectoryId = previousTrajectoryId; MotionModel = motionModel; PlanningScope = planningScope; + CycleDeadlineRemaining = cycleDeadlineRemaining; + CallerCancellationToken = callerCancellationToken; + CycleDeadlineToken = cycleDeadlineToken; + PublicationAuthorization = new EmPlanningRequestPublicationAuthorization( + cycleDeadlineRemaining, cycleDeadlineToken); } + internal EmPlanningRequest( + PathSmoothingResult referencePath, + PlanningGridMap map, + VehicleParameters vehicle, + VehicleMotionState vehicleState, + EmPlannerConfiguration configuration, + int segmentIndex, + EmTrajectory previousTrajectory, + DateTimeOffset requestedAtUtc, + DateTimeOffset effectiveAtUtc, + string outputTrajectoryId, + string referencePathId, + string previousTrajectoryId, + EmMotionModel motionModel, + EmPlanningScope planningScope, + TimeSpan? cycleDeadlineRemaining, + CancellationToken callerCancellationToken, + CancellationToken cycleDeadlineToken, + IEmPlanningPublicationAuthorization publicationAuthorization) + : this(referencePath, map, vehicle, vehicleState, configuration, segmentIndex, previousTrajectory, + requestedAtUtc, effectiveAtUtc, outputTrajectoryId, referencePathId, previousTrajectoryId, + motionModel, planningScope, cycleDeadlineRemaining, callerCancellationToken, cycleDeadlineToken) + { + PublicationAuthorization = publicationAuthorization ?? + new EmPlanningRequestPublicationAuthorization(cycleDeadlineRemaining, cycleDeadlineToken); + } + + /// + /// 平滑后的参考路径结果引用;服务要求其为可消费的成功结果,且调用方负责保持其与其他输入版本一致。 + /// public PathSmoothingResult ReferencePath { get; } + + /// + /// 规划使用的栅格地图快照引用;地图坐标单位和快照身份由地图契约定义,构造器不检查 null 或就绪状态。 + /// public PlanningGridMap Map { get; } + + /// + /// 车辆几何与运动限制引用;调用方拥有该对象,服务在请求验证后使用其约束。 + /// public VehicleParameters Vehicle { get; } + + /// + /// 一次性捕获的车辆运动状态;其位置为世界坐标 m、航向为 rad,时效和数值有效性由服务验证。 + /// public VehicleMotionState VehicleState { get; } + + /// + /// 本次规划的配置引用;请求不深拷贝它,验证通过后服务取得配置副本供本次规划使用。 + /// public EmPlannerConfiguration Configuration { get; } + + /// + /// 参考路径中待规划的方向段索引;必须由服务验证为有效的非负范围。 + /// public int SegmentIndex { get; } + + /// + /// 可选的上一条已发布轨迹引用,用于衔接或诊断;构造器允许为 null,调用方保有其所有权。 + /// public EmTrajectory PreviousTrajectory { get; } + + /// + /// 本次规划请求发起的 UTC 时刻;用于车辆状态时效判断和生成轨迹元数据,构造器不验证其时间关系。 + /// public DateTimeOffset RequestedAtUtc { get; } + + /// + /// 成功发布轨迹开始生效的 UTC 时刻;由下游消费者依据其调度语义使用。 + /// public DateTimeOffset EffectiveAtUtc { get; } + + /// + /// 成功轨迹的发布标识;构造器不校验,轨迹元数据构造时要求为非空白字符串。 + /// public string OutputTrajectoryId { get; } + + /// + /// 输入参考路径的业务标识,用于成功轨迹的来源追踪;构造器不校验其 null 或空白值。 + /// public string ReferencePathId { get; } + + /// + /// 上一条轨迹的业务标识;允许为 null,成功轨迹元数据会将 null 规范化为空字符串。 + /// public string PreviousTrajectoryId { get; } + + /// + /// 请求的车辆运动模型;不支持或无效的枚举值由服务转换为失败状态。 + /// public EmMotionModel MotionModel { get; } + + /// + /// 本次请求覆盖滚动窗口或整个方向段的范围;服务负责验证其枚举值和资源约束。 + /// public EmPlanningScope PlanningScope { get; } + + /// Optional remaining wall-clock budget for the complete outer planning cycle. + public TimeSpan? CycleDeadlineRemaining { get; } + + /// Dynamic caller-cancellation origin used to preserve terminal-status precedence. + public CancellationToken CallerCancellationToken { get; } + + /// Dynamic shared-cycle deadline origin checked again at atomic publication. + public CancellationToken CycleDeadlineToken { get; } + + internal IEmPlanningPublicationAuthorization PublicationAuthorization { get; } + + internal bool IsCycleDeadlineExpired() + { + try + { + return PublicationAuthorization.IsExpired; + } + catch + { + return true; + } + } + + internal TimeSpan? DynamicCycleDeadlineRemaining() + { + try + { + TimeSpan remaining = PublicationAuthorization.Remaining; + return remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero; + } + catch + { + return TimeSpan.Zero; + } + } + + internal EmPlanningPublicationDecision TryAuthorizePublication( + CancellationToken coordinatorCallerCancellationToken, Action publish) + { + if (publish == null) + throw new ArgumentNullException(nameof(publish)); + try + { + return PublicationAuthorization.TryPublish(CallerCancellationToken, + coordinatorCallerCancellationToken, publish); + } + catch + { + return EmPlanningPublicationDecision.DeadlineExpired; + } + } } -// The request owns this DTO contract; Task 2 adds its request-bound settings. +/// +/// 为请求契约保留的 部分声明;实际配置成员由其他同名分部提供。 +/// public sealed partial class EmPlannerConfiguration { } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningResult.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningResult.cs index e0e2f08..a39d106 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningResult.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningResult.cs @@ -2,8 +2,14 @@ using System; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 一次 EM 规划的不可变结果。只有成功状态才携带可发布的完整轨迹;其余状态仅提供诊断,不能作为部分执行轨迹消费。 +/// public sealed class EmPlanningResult { + /// + /// 创建规划结果并强制成功状态与轨迹发布的一致性: 必须提供非 null 轨迹,其他状态必须提供 null 轨迹;null 失败原因规范化为空字符串。 + /// public EmPlanningResult(EmPlanningStatus status, EmTrajectory trajectory, string failureReason) { if (!Enum.IsDefined(typeof(EmPlanningStatus), status)) @@ -20,9 +26,18 @@ public sealed class EmPlanningResult FailureReason = failureReason ?? string.Empty; } + /// + /// 本次规划的最终状态;消费者必须先判断其是否为成功状态,再访问可发布轨迹。 + /// public EmPlanningStatus Status { get; } + /// + /// 仅在成功或成功降级状态下存在的完整可发布轨迹;失败、取消和无效输入结果始终为 null,消费者不得把失败结果当作部分轨迹执行。 + /// public EmTrajectory Trajectory { get; } + /// + /// 面向诊断的失败或降级原因;null 输入已规范化为空字符串,不替代 的机器可读状态。 + /// public string FailureReason { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs index af878b4..099ee8c 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs @@ -1,7 +1,16 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 定义一次 EM 规划覆盖当前滚动窗口还是完整方向段。 +/// public enum EmPlanningScope { + /// + /// 仅覆盖有限的滚动规划窗口;段末之外保留给后续重规划。 + /// RollingHorizon, + /// + /// 覆盖选定方向段直至真实停车边界,并受完整段资源上限保护。 + /// FullDirectionSegment, } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs index 4b0ce29..078dcd3 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs @@ -1,26 +1,96 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 表示 EM 规划请求的最终处理状态;仅两个成功状态允许发布轨迹,其余状态要求消费者保留现有安全策略并读取诊断。 +/// public enum EmPlanningStatus { + /// + /// 所有规划和验证阶段成功,结果可按其生效时间发布。 + /// Success, + /// + /// 已发布经过验证的降级或回退轨迹;消费者仍可使用轨迹,并应记录诊断原因。 + /// SuccessWithFallback, + /// + /// 请求对象或其成员未满足服务输入要求。 + /// InvalidInput, + /// + /// 请求的车辆运动模型未被当前 EM 管线支持。 + /// UnsupportedMotionMode, + /// + /// 车辆状态采集时刻相对于请求时刻过旧。 + /// StaleVehicleState, + /// + /// 车辆状态的行驶方向与选定参考路径方向段不一致。 + /// StateDirectionMismatch, + /// + /// 参考路径结果、方向段或其状态不能用于规划。 + /// InvalidReferencePath, + /// + /// 无法将车辆状态投影到选定参考路径或方向段。 + /// ProjectionFailed, + /// + /// 地图和车辆约束下未能构造可行的行驶走廊。 + /// CorridorInfeasible, + /// + /// 横向优化未得到满足约束的候选解。 + /// LateralInfeasible, + /// + /// 纵向优化未得到满足时空与动力学约束的候选解。 + /// LongitudinalInfeasible, + /// + /// 当前速度、距离或限制不足以在所需边界前完成停车。 + /// StoppingDistanceInsufficient, + /// + /// 所需二次规划求解器不可用。 + /// SolverUnavailable, + /// + /// 求解在共享时间预算或迭代限制内未完成。 + /// SolverTimedOut, + /// + /// 调用方在可发布结果生成前取消了请求。 + /// Cancelled, + /// + /// 候选轨迹未通过最终世界空间、动力学或终端语义验证。 + /// ValidationFailed, + /// + /// 请求在处理期间被更新版本的规划工作替代。 + /// Superseded, + /// + /// 候选轨迹相对于当前状态未形成足够的有效进展。 + /// NoProgress, + /// + /// 轨迹末端位姿未满足要求的真实终端位姿。 + /// TerminalPoseMismatch, + /// + /// 完整方向段规划超过配置的时间、采样或求解资源上限。 + /// FullSegmentResourceLimitExceeded, + /// + /// 未被其他状态细分的规划失败。 + /// Failed, + /// + /// The outer planning cycle exhausted its shared bootstrap-to-publication deadline. + /// + CycleDeadlineExpired, } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTerminalType.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTerminalType.cs index 089c74c..8e64f73 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTerminalType.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTerminalType.cs @@ -1,8 +1,20 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 表示已发布轨迹末端所对应的规划终端语义。 +/// public enum EmTerminalType { + /// + /// 因滚动窗口安全约束形成的停车终端,不代表方向段完成。 + /// RollingSafetyStop, + /// + /// 用于在方向改变前后执行换挡的终端。 + /// GearSwitch, + /// + /// 最终任务目标的终端。 + /// Goal, } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectory.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectory.cs index c7da7d5..fe304fa 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectory.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectory.cs @@ -4,8 +4,14 @@ using System.Collections.ObjectModel; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 供成功规划结果发布的时间参数化轨迹;本类型只施加结构约束,世界空间复核由上游服务完成,点序列、元数据和终端语义在发布后保持不可变。 +/// public sealed class EmTrajectory { + /// + /// 创建可发布的轨迹并复制点列表容器。元数据和每个点对象按引用保存、调用方仍拥有它们;传入列表可随后修改而不影响 ,且 null 或空列表会抛出异常。 + /// public EmTrajectory(EmTrajectoryMetadata metadata, IReadOnlyList points) { if (metadata == null) @@ -27,7 +33,13 @@ public sealed class EmTrajectory Points = new ReadOnlyCollection(copy); } + /// + /// 轨迹的不可变发布元数据引用;构造时必须非 null,未在本类中深拷贝。 + /// public EmTrajectoryMetadata Metadata { get; } + /// + /// 按调用方提供顺序保存的只读点列表;列表容器为构造时复制的快照,至少包含一个非 null 点,时间单调性由上游验证保证而非本构造器检查。 + /// public IReadOnlyList Points { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs index e8bcdaa..6f05450 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs @@ -3,8 +3,14 @@ using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 轨迹发布身份、来源版本、有效时间、方向段和终端边界的不可变元数据。 +/// public sealed class EmTrajectoryMetadata { + /// + /// 创建轨迹的发布和来源元数据。验证标识、序列号、索引和枚举值; 可为 null 并会规范化为空字符串,两个 UTC 时刻的先后关系不在此处验证。 + /// public EmTrajectoryMetadata( string trajectoryId, DateTimeOffset generatedAtUtc, @@ -52,16 +58,63 @@ public sealed class EmTrajectoryMetadata PlanningScope = planningScope; } + /// + /// 非空白的本次发布轨迹标识,由消费者用于去重、替换和追踪。 + /// public string TrajectoryId { get; } + + /// + /// 生成此元数据的 UTC 时刻;值原样保存,构造器不与生效时刻比较。 + /// public DateTimeOffset GeneratedAtUtc { get; } + + /// + /// 轨迹计划开始生效的 UTC 时刻;执行消费者负责按其调度策略解释。 + /// public DateTimeOffset EffectiveAtUtc { get; } + + /// + /// 地图快照的非负版本标识;用于确认轨迹依赖的环境版本。 + /// public long MapSnapshotId { get; } + + /// + /// 非空白的输入参考路径标识;用于关联轨迹的几何来源。 + /// public string ReferencePathId { get; } + + /// + /// 车辆状态的非负序列版本;用于判断轨迹是否基于当前状态快照。 + /// public long VehicleStateSequenceId { get; } + + /// + /// 前一轨迹的可选标识;null 输入已规范化为空字符串,空字符串表示没有可关联的前轨迹标识。 + /// public string PreviousTrajectoryId { get; } + + /// + /// 参考路径中此轨迹所属的非负方向段索引。 + /// public int SegmentIndex { get; } + + /// + /// 所属方向段的已验证行驶方向。 + /// public TravelDirection Direction { get; } + + /// + /// 轨迹最后一个终端的已验证业务类型。 + /// public EmTerminalType TerminalType { get; } + + /// + /// 生成轨迹时采用的已验证纵向终端速度语义。 + /// public EmLongitudinalMode LongitudinalMode { get; } + + /// + /// 生成轨迹时采用的已验证规划覆盖范围。 + /// public EmPlanningScope PlanningScope { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryPoint.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryPoint.cs index 1cd136b..6a293f4 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryPoint.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryPoint.cs @@ -3,8 +3,14 @@ using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 轨迹中的单个时间样本。世界位置单位 m、航向单位 rad、速度单位 m/s、曲率单位 1/m。 +/// public sealed class EmTrajectoryPoint { + /// + /// 创建一个轨迹时间样本。所有浮点输入必须有限;时间、段内弧长和路径弧长必须非负,段索引和枚举值必须有效。派生的绝对速度、世界速度分量和偏航角速度由有符号纵向速度、航向和曲率计算。 + /// public EmTrajectoryPoint( double x, double y, @@ -62,23 +68,88 @@ public sealed class EmTrajectoryPoint LongitudinalJerk = longitudinalJerk; } + /// + /// 世界坐标系 X 位置,单位 m。 + /// public double X { get; } + + /// + /// 世界坐标系 Y 位置,单位 m。 + /// public double Y { get; } + + /// + /// 车辆在世界坐标系中的航向,单位 rad;构造器仅要求有限,不归一化角度。 + /// public double Yaw { get; } + + /// + /// 沿车辆前向轴的有符号纵向速度,单位 m/s;符号表示前进或倒车。 + /// public double SignedLongitudinalVelocity { get; } + + /// + /// 有符号纵向速度的绝对值,单位 m/s。 + /// public double Speed { get; } + + /// + /// 由有符号纵向速度和航向导出的世界坐标 X 速度分量,单位 m/s。 + /// public double VelocityX { get; } + + /// + /// 由有符号纵向速度和航向导出的世界坐标 Y 速度分量,单位 m/s。 + /// public double VelocityY { get; } + + /// + /// 由有符号纵向速度乘车辆曲率导出的偏航角速度,单位 rad/s。 + /// public double YawRate { get; } + + /// + /// 从本条轨迹开始执行起累计的非负时间,单位 s。 + /// public double TimeFromStart { get; } + + /// + /// 车辆路径曲率,单位 1/m;符号约定由上游几何计算定义。 + /// public double VehicleCurvature { get; } + + /// + /// 此点所属的非负方向段索引。 + /// public int SegmentIndex { get; } + + /// + /// 从该方向段开始沿参考路径累计的非负弧长,单位 m。 + /// public double SegmentLocalS { get; } + + /// + /// 从整条参考路径开始累计的非负弧长,单位 m。 + /// public double PathS { get; } + + /// + /// 此点的已验证行驶方向。 + /// public TravelDirection Direction { get; } + + /// + /// 此点的已验证边界语义;普通内部点使用 。 + /// public EmBoundaryType BoundaryType { get; } + /// + /// 沿车辆前向轴的纵向加速度,单位 m/s²;仅供同程序集的轨迹验证和执行逻辑读取。 + /// internal double LongitudinalAcceleration { get; } + /// + /// 沿车辆前向轴的纵向加加速度,单位 m/s³;仅供同程序集的轨迹验证和执行逻辑读取。 + /// internal double LongitudinalJerk { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/VehicleMotionState.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/VehicleMotionState.cs index da743b0..4fd0768 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/VehicleMotionState.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/VehicleMotionState.cs @@ -3,8 +3,14 @@ using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 调用方一次性捕获的车辆运动状态;EMPlanner 只消费此快照,不直接读取定位、轮速或系统时钟。 +/// public sealed class VehicleMotionState { + /// + /// 保存调用方捕获的车辆状态。构造器不校验 null、数值有限性、UTC 时效或序列号;服务在接受请求前负责验证,传入 的所有权仍归调用方。 + /// public VehicleMotionState( Pose2D pose, double signedLongitudinalSpeedMetersPerSecond, @@ -19,13 +25,28 @@ public sealed class VehicleMotionState SequenceId = sequenceId; } + /// + /// 车辆世界位姿引用,其中位置单位为 m、航向单位为 rad;构造器允许 null,但服务要求有效位姿。 + /// public Pose2D Pose { get; } + /// + /// 沿车辆前向轴的有符号纵向速度,单位 m/s;正负方向约定由上游状态生产者负责,服务要求其为有限数。 + /// public double SignedLongitudinalSpeedMetersPerSecond { get; } + /// + /// 可选的沿车辆前向轴纵向加速度,单位 m/s²;null 表示采集方未提供该量,非 null 值必须由服务验证为有限数。 + /// public double? LongitudinalAccelerationMetersPerSecondSquared { get; } + /// + /// 采集此状态的 UTC 时刻;服务用它相对请求发起时刻判断状态是否过期。 + /// public DateTimeOffset CapturedAtUtc { get; } + /// + /// 调用方提供的状态快照序列版本;服务要求其非负,成功轨迹将其写入元数据供消费者关联。 + /// public long SequenceId { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/LateralInterval.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/LateralInterval.cs index 47c798d..f00171e 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/LateralInterval.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/LateralInterval.cs @@ -2,9 +2,17 @@ using System; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Connected free lateral interval at one exact reference-S station. +/// +/// 一个精确参考站 S 处与种子横向位置连通的自由横向区间。 +/// 参考站和横向偏移均以 m 计,横向正负号遵循该方向段的行驶坐标系;构造时拒绝非有限值、反向边界,或超出边界 1e-12 m 的种子。 +/// public sealed class LateralInterval { + /// + /// 创建一个固定参考站上的闭合自由横向区间。 + /// 参数:referenceS、minimumL、maximumL 与 seedL 均为 m;seedL 必须在闭区间内(容许 1e-12 m 数值误差)。 + /// 返回:保存已验证边界的不可变区间;非法数值或不连通种子会引发 。 + /// public LateralInterval(double referenceS, double minimumL, double maximumL, double seedL) { if (!IsFinite(referenceS) || !IsFinite(minimumL) || !IsFinite(maximumL) || !IsFinite(seedL) || @@ -17,11 +25,30 @@ public sealed class LateralInterval SeedL = seedL; } + /// + /// 本区间所属方向段局部参考弧长 S,单位 m。 + /// public double ReferenceS { get; } + + /// + /// 可通行横向偏移闭区间的下界,单位 m。 + /// public double MinimumL { get; } + + /// + /// 可通行横向偏移闭区间的上界,单位 m。 + /// public double MaximumL { get; } + + /// + /// 用于保持拓扑连通性的种子横向偏移,单位 m。 + /// public double SeedL { get; } + /// + /// 判定标量能否参与走廊边界计算。 + /// 参数:value 为无单位或 m 制实数;返回:仅非 NaN 且非无穷大时为 true。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridor.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridor.cs index eebf384..35b529f 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridor.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridor.cs @@ -4,9 +4,16 @@ using System.Collections.ObjectModel; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Immutable lateral hard bounds for one already-selected topological corridor. +/// +/// 一个已选择拓扑走廊在离散参考站上的不可变横向硬边界集合。 +/// 站点按方向段局部 S(m)非递减保存,调用方只能沿每个站点的种子连通自由区间优化。 +/// public sealed class StaticCorridor { + /// + /// 从站点序列创建走廊的防御性只读副本。 + /// 参数:stations 不能为空且至少含一个按 ReferenceS(m)非递减的非空区间;违反这些约束会被拒绝。 + /// public StaticCorridor(IReadOnlyList stations) { if (stations == null || stations.Count == 0) @@ -25,5 +32,8 @@ public sealed class StaticCorridor Stations = new ReadOnlyCollection(copy); } + /// + /// 按方向段局部参考弧长 S(m)排序的横向硬边界只读列表。 + /// public IReadOnlyList Stations { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridorBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridorBuilder.cs index 58aa867..f939cd6 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridorBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/StaticCorridorBuilder.cs @@ -6,24 +6,49 @@ using MultiWheelC.TrajectoryPlanning.Mapping; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Builds only the seed-connected static free-space corridor of a direction segment. +/// +/// 为单一方向段构建仅与种子轨迹横向连通的静态自由空间走廊。 +/// 采样站和横向偏移使用 Frenet S/L(m),碰撞在世界 X/Y(m)车体几何中验证;任何无效、碰撞或断连站都会拒绝整个走廊。 +/// public sealed class StaticCorridorBuilder { + /// + /// 横向边界、采样去重和相邻区间重叠判定使用的数值容差,单位 m。 + /// private const double Epsilon = 1e-12d; + + /// + /// Frenet 重建时拒绝 1-κL 接近零的最小正分母,无量纲。 + /// private const double ReconstructionDenominator = 1e-12d; + /// + /// 对未被保守净空快速放行的位置执行精确车体碰撞复核的依赖项。 + /// private readonly FootprintCollisionChecker _collisionChecker; + /// + /// 创建使用默认连续车体碰撞检查器的静态走廊构建器。 + /// public StaticCorridorBuilder() : this(new FootprintCollisionChecker()) { } + /// + /// 创建使用指定车体碰撞检查器的静态走廊构建器。 + /// 参数:collisionChecker 不可为空;该检查器在世界坐标中按车辆尺寸和安全裕度验证采样位姿。 + /// public StaticCorridorBuilder(FootprintCollisionChecker collisionChecker) { _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); } + /// + /// 从 S 起止锚点、种子轨迹和栅格地图构建种子连通的横向硬边界。 + /// 参数:startReferenceS/endReferenceS、种子 S/L 和配置采样尺度均为 m,map 使用世界 X/Y 栅格,vehicle 提供 m 制尺寸;区间限定在一个方向段内。 + /// 返回:每站均存在含种子的自由区间且相邻区间在 1e-12 m 内重叠时返回 true;否则 corridor 为 null 并写入拒绝原因。 + /// public bool TryBuild(DirectionSegmentView segment, double startReferenceS, double endReferenceS, IReadOnlyList seed, PlanningGridMap map, VehicleParameters vehicle, CorridorConfiguration configuration, out StaticCorridor corridor, out string failureReason) @@ -82,6 +107,10 @@ public sealed class StaticCorridorBuilder return true; } + /// + /// 验证构建走廊所需的段、S 范围、地图、车辆和采样配置。 + /// 参数:S 锚点和配置距离均为 m,车辆尺寸为 m;返回:地图未就绪、非有限量、越段锚点或非正采样尺度时为 false 并说明原因。 + /// private static bool TryValidateInput(DirectionSegmentView segment, double startReferenceS, double endReferenceS, PlanningGridMap map, VehicleParameters vehicle, CorridorConfiguration configuration, out string failureReason) { @@ -115,6 +144,10 @@ public sealed class StaticCorridorBuilder return true; } + /// + /// 验证并按局部参考 S 排序输入种子投影。 + /// 参数:input 可为 null(表示中心线 L=0 种子),各投影 S/L 为 m;返回:种子属于当前段且横向量有限时为 true,否则为 false。 + /// private static bool TryReadSeeds(IReadOnlyList input, DirectionSegmentView segment, out List seeds, out string failureReason) { @@ -139,6 +172,10 @@ public sealed class StaticCorridorBuilder return true; } + /// + /// 生成包含起止锚点的纵向采样站。 + /// 参数:起止 S 和 spacing 单位均为 m;返回:起点、严格内部等距站以及不同于起点超过 1e-12 m 的终点。 + /// private static IEnumerable CreateStations(double startReferenceS, double endReferenceS, double spacing) { yield return startReferenceS; @@ -148,6 +185,10 @@ public sealed class StaticCorridorBuilder yield return endReferenceS; } + /// + /// 找出横向离散样本中包含种子的连续无碰撞区间。 + /// 参数:reference 为世界几何,seedL 及配置横向距离为 m;返回:种子样本自由时给出 [minimumL, maximumL],碰撞或断连时返回 false。 + /// private bool TrySelectSeedConnectedInterval(FrenetReferencePoint reference, double seedL, PlanningGridMap map, VehicleParameters vehicle, CorridorConfiguration configuration, out double minimumL, out double maximumL) { @@ -190,6 +231,10 @@ public sealed class StaticCorridorBuilder return false; } + /// + /// 在配置横向范围内生成有序且包含种子的唯一 L 采样值。 + /// 参数:seedL、maximumOffset 和 spacing 为 m;返回:包含两端和种子、以 1e-12 m 去重的升序列表。 + /// private static List CreateLateralSamples(double seedL, double maximumOffset, double spacing) { var samples = new List(); @@ -202,6 +247,10 @@ public sealed class StaticCorridorBuilder return samples; } + /// + /// 重建给定 L 的世界车体位姿并检查其是否无碰撞。 + /// 参数:lateralOffset 为 m,reference 使用世界 X/Y(m)和 rad 航向,车辆与储备净空为 m;返回:重建奇异、越图或碰撞均为 false。 + /// private bool IsCollisionFree(FrenetReferencePoint reference, double lateralOffset, PlanningGridMap map, VehicleParameters vehicle, CorridorConfiguration configuration) { @@ -214,6 +263,10 @@ public sealed class StaticCorridorBuilder configuration.AdditionalClearanceReserveMeters, out _); } + /// + /// 用保守障碍距离和四个扩张车体角点快速确认明显净空。 + /// 参数:pose 为世界 X/Y(m)和 rad,车辆尺寸及额外储备为 m;返回:圆形下界安全且四角均在图内时为 true,否则交由精确检查器。 + /// private static bool IsObviouslyClear(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, double additionalClearanceReserveMeters) { @@ -237,6 +290,10 @@ public sealed class StaticCorridorBuilder return true; } + /// + /// 以相邻种子 S 线性插值得到采样站的横向种子。 + /// 参数:seeds 已按局部 S(m)升序,referenceS 为 m;返回:区间外保持端点 L,重合 S 间隔不超过 1e-12 m 时取上端 L。 + /// private static double GetSeedL(IReadOnlyList seeds, double referenceS) { if (seeds.Count == 0) @@ -257,6 +314,10 @@ public sealed class StaticCorridorBuilder return seeds[seeds.Count - 1].LateralOffset; } + /// + /// 向样本列表加入未在 1e-12 m 容差内出现过的横向值。 + /// 参数:samples 保存 m 制 L 值,value 为待加入 L(m);返回:重复近似值被拒绝,唯一值追加后由调用方排序。 + /// private static void AddSortedUnique(List samples, double value) { for (int index = 0; index < samples.Count; index++) @@ -265,25 +326,48 @@ public sealed class StaticCorridorBuilder samples.Add(value); } + /// + /// 判定采样步长或车辆尺寸是否为正的有限值。 + /// 参数:value 为对应单位的标量;返回:仅有限且严格大于零时为 true。 + /// private static bool IsPositiveFinite(double value) { return IsFinite(value) && value > 0d; } + /// + /// 判定走廊计算输入是否为有限实数。 + /// 参数:value 为任意标量;返回:NaN 和无穷时为 false。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } + /// + /// 走廊种子在一个局部参考站上的轻量不可变记录。 + /// ReferenceS 与 LateralOffset 均以 m 计,仅用于站间种子插值。 + /// private sealed class SeedSample { + /// + /// 创建一个种子记录。 + /// 参数:referenceS 为段局部 S(m),lateralOffset 为行驶坐标系中的 L(m)。 + /// public SeedSample(double referenceS, double lateralOffset) { ReferenceS = referenceS; LateralOffset = lateralOffset; } + /// + /// 种子所在方向段局部参考弧长 S,单位 m。 + /// public double ReferenceS { get; } + + /// + /// 种子相对中心线的横向偏移 L,单位 m。 + /// public double LateralOffset { get; } } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs index e574c83..e728e04 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath; @@ -9,28 +10,65 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// Runs one deterministic EM LS/ST planning pipeline and publishes only independently validated trajectories. public sealed class EmPlanningService : IEmPlanningService { + private static readonly TimeSpan MaximumCycleDeadlineRemaining = + TimeSpan.FromMilliseconds(int.MaxValue); private readonly IQpSolver qpSolver; private readonly IEmPlannerDebugSink defaultDebugSink; + private readonly Func cycleElapsedForTesting; public EmPlanningService(IQpSolver qpSolver, IEmPlannerDebugSink defaultDebugSink = null) + : this(qpSolver, defaultDebugSink, null) + { + } + + internal EmPlanningService(IQpSolver qpSolver, IEmPlannerDebugSink defaultDebugSink, + Func cycleElapsedForTesting) { this.qpSolver = qpSolver ?? throw new ArgumentNullException(nameof(qpSolver)); this.defaultDebugSink = defaultDebugSink; + this.cycleElapsedForTesting = cycleElapsedForTesting; } public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) + Stopwatch cycleStopwatch = cycleElapsedForTesting == null ? Stopwatch.StartNew() : null; + Func cycleElapsed = cycleElapsedForTesting ?? (() => cycleStopwatch.Elapsed); + if (CallerCancellationRequested(request, cancellationToken)) return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled before request validation."); + if (request?.CycleDeadlineRemaining is TimeSpan suppliedRemaining && + (suppliedRemaining < TimeSpan.Zero || suppliedRemaining > MaximumCycleDeadlineRemaining)) + { + return Failure(EmPlanningStatus.InvalidInput, request, + "CycleDeadlineRemaining must be between zero and " + + MaximumCycleDeadlineRemaining.TotalMilliseconds + " milliseconds."); + } + if (DeadlineExpired(request, cycleElapsed)) + return CycleDeadlineFailure(request, "request", CycleRemaining(request, cycleElapsed)); EmPlanningRequestValidationResult requestValidation = EmPlanningRequestValidator.Validate(request); if (!requestValidation.IsValid) return Failure(requestValidation.Status, request, requestValidation.FailureReason); EmPlannerConfiguration configuration = requestValidation.Snapshot.Configuration; EmitDebug(request, "request/config validation succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "request-validation", out EmPlanningResult validationFailure)) + { + return validationFailure; + } + TimeSpan expirationRemaining = CycleRemaining(request, cycleElapsed); + using var cycleExpiration = request.CycleDeadlineRemaining.HasValue + ? new CancellationTokenSource(expirationRemaining) + : null; + using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, request.CallerCancellationToken, request.CycleDeadlineToken, + cycleExpiration?.Token ?? CancellationToken.None); + CancellationToken planningCancellationToken = linkedCancellation.Token; try { + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "projection", out EmPlanningResult deadlineFailure)) + return deadlineFailure; IReadOnlyList segments = ReferencePathSegmenter.Create(request.ReferencePath); if (request.SegmentIndex < 0 || request.SegmentIndex >= segments.Count) return Failure(EmPlanningStatus.InvalidReferencePath, request, "The requested direction segment is unavailable."); @@ -59,7 +97,11 @@ public sealed class EmPlanningService : IEmPlanningService "Vehicle travel heading differs by at least 90 degrees from the selected direction segment."); } EmitDebug(request, "bounded ego projection succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "projection", out deadlineFailure)) + return deadlineFailure; + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "corridor", out deadlineFailure)) + return deadlineFailure; double initialProgressSpeed = Math.Abs(request.VehicleState.SignedLongitudinalSpeedMetersPerSecond); double initialAcceleration = request.VehicleState.LongitudinalAccelerationMetersPerSecondSquared ?? 0d; var horizonSelector = new PlanningHorizonSelector(); @@ -84,18 +126,31 @@ public sealed class EmPlanningService : IEmPlanningService return Failure(EmPlanningStatus.CorridorInfeasible, request, corridorReason); } EmitDebug(request, "static connected corridor succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "corridor", out deadlineFailure)) + return deadlineFailure; + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "lateral", out deadlineFailure)) + return deadlineFailure; + TimeSpan lateralBudget = SmallerBudget( + TimeSpan.FromSeconds(configuration.Scheduling.SolverTimeoutSeconds), + CycleRemaining(request, cycleElapsed)); + EmPlannerConfiguration lateralConfiguration = configuration.Copy(); + lateralConfiguration.Scheduling.SolverTimeoutSeconds = lateralBudget.TotalSeconds; var lateralInput = new LateralPlanningInput(segment, corridor, startProjection, horizon.TerminalType, - request.Vehicle, configuration, previousSeed); - TimeSpan totalSolveBudget = TimeSpan.FromSeconds(configuration.Scheduling.SolverTimeoutSeconds); + request.Vehicle, lateralConfiguration, previousSeed); + TimeSpan totalSolveBudget = lateralBudget; var solveBudgetStopwatch = Stopwatch.StartNew(); - LateralPlanningResult lateral = new LateralPlanner(qpSolver).Plan(lateralInput, cancellationToken); + LateralPlanningResult lateral = new LateralPlanner(qpSolver).Plan(lateralInput, planningCancellationToken); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "lateral", out deadlineFailure)) + return deadlineFailure; if (!IsSuccess(lateral.Status)) return Failure(lateral.Status, request, lateral.FailureReason); if (cancellationToken.IsCancellationRequested) return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled after LS optimization."); EmitDebug(request, "LS optimization and validation succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "envelope", out deadlineFailure)) + return deadlineFailure; EmPlanningStatus envelopeStatus = new PathSpeedLimitBuilder().Build(lateral.Path, segment.Direction, initialProgressSpeed, initialAcceleration, horizon.TerminalType, configuration, out PathSpeedLimit speedLimit, out string envelopeReason); @@ -119,9 +174,15 @@ public sealed class EmPlanningService : IEmPlanningService new LongitudinalPreviousTrajectorySeedBuilder().Build( request.PreviousTrajectory, lateral.Path, request.EffectiveAtUtc, knotSchedule, segment.SegmentIndex, segment.Direction); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "envelope", out deadlineFailure)) + return deadlineFailure; TimeSpan remainingSolveBudget = totalSolveBudget - solveBudgetStopwatch.Elapsed; + remainingSolveBudget = SmallerBudget(remainingSolveBudget, CycleRemaining(request, cycleElapsed)); if (remainingSolveBudget <= TimeSpan.Zero) { + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "longitudinal", out deadlineFailure)) + return deadlineFailure; return Failure(EmPlanningStatus.SolverTimedOut, request, "LS/ST optimization exhausted the shared solve budget before ST optimization."); } @@ -136,13 +197,22 @@ public sealed class EmPlanningService : IEmPlanningService return Failure(envelopeStatus, request, envelopeReason); EmitDebug(request, "PathS speed envelope succeeded"); - LongitudinalPlanningResult longitudinal = new LongitudinalPlanner(qpSolver).Plan(longitudinalInput, cancellationToken); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "longitudinal", out deadlineFailure)) + return deadlineFailure; + LongitudinalPlanningResult longitudinal = new LongitudinalPlanner(qpSolver).Plan( + longitudinalInput, planningCancellationToken); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "longitudinal", out deadlineFailure)) + return deadlineFailure; if (!IsSuccess(longitudinal.Status)) return Failure(longitudinal.Status, request, longitudinal.FailureReason); if (cancellationToken.IsCancellationRequested) return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled after ST optimization."); EmitDebug(request, "ST optimization and validation succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "assembly", out deadlineFailure)) + return deadlineFailure; var metadata = new EmTrajectoryMetadata(request.OutputTrajectoryId, request.RequestedAtUtc, request.EffectiveAtUtc, request.Map.SnapshotId, request.ReferencePathId, request.VehicleState.SequenceId, request.PreviousTrajectoryId, segment.SegmentIndex, segment.Direction, horizon.TerminalType, horizon.LongitudinalMode, @@ -151,10 +221,15 @@ public sealed class EmPlanningService : IEmPlanningService longitudinal, metadata, out EmTrajectory trajectory, out string assemblyFailure); if (assemblyStatus != EmPlanningStatus.Success) return Failure(assemblyStatus, request, assemblyFailure); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "assembly", out deadlineFailure)) + return deadlineFailure; if (cancellationToken.IsCancellationRequested) return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled after trajectory assembly."); EmitDebug(request, "trajectory assembly succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "world-validation", out deadlineFailure)) + return deadlineFailure; EmBoundaryType terminalBoundary = slice.TerminalBoundary.BoundaryType; Pose2D terminalPose = IsRealTerminalBoundary(terminalBoundary) ? TerminalPose(lateral.Path) @@ -167,8 +242,13 @@ public sealed class EmPlanningService : IEmPlanningService return Failure(MapPublicationFailure(publication.Failure), request, publication.Failure + " at point " + publication.PointIndex + ": " + publication.Message); } + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "world-validation", out deadlineFailure)) + return deadlineFailure; EmitDebug(request, "world-space publication validation succeeded"); + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "publication", out deadlineFailure)) + return deadlineFailure; if (cancellationToken.IsCancellationRequested) return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled before trajectory publication."); @@ -176,11 +256,25 @@ public sealed class EmPlanningService : IEmPlanningService longitudinal.Status == EmPlanningStatus.SuccessWithFallback ? EmPlanningStatus.SuccessWithFallback : EmPlanningStatus.Success; - return new EmPlanningResult(finalStatus, trajectory, - DiagnosticsPrefix(request) + ";terminal=" + horizon.TerminalType + ";publication=validated"); + string publicationDiagnostic = DiagnosticsPrefix(request) + ";terminal=" + horizon.TerminalType + + ";publication=validated"; + if (lateral.Status == EmPlanningStatus.SuccessWithFallback) + publicationDiagnostic += ";lateralFallback=" + lateral.FailureReason; + if (!string.IsNullOrWhiteSpace(longitudinal.FailureReason)) + { + publicationDiagnostic += longitudinal.Status == EmPlanningStatus.SuccessWithFallback + ? ";longitudinalFallback=" + longitudinal.FailureReason + : ";longitudinal=" + longitudinal.FailureReason; + } + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, "publication", out deadlineFailure)) + return deadlineFailure; + return new EmPlanningResult(finalStatus, trajectory, publicationDiagnostic); } catch (OperationCanceledException) { + if (TryTerminalFailure(request, cancellationToken, cycleElapsed, + "operation", out EmPlanningResult deadlineFailure)) + return deadlineFailure; return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled."); } catch (ArgumentException exception) @@ -193,6 +287,71 @@ public sealed class EmPlanningService : IEmPlanningService } } + private static bool TryTerminalFailure(EmPlanningRequest request, CancellationToken cancellationToken, + Func elapsed, string phase, out EmPlanningResult failure) + { + if (CallerCancellationRequested(request, cancellationToken)) + { + failure = Failure(EmPlanningStatus.Cancelled, request, + "callerCancellation=true;phase=" + phase); + return true; + } + TimeSpan remaining = CycleRemaining(request, elapsed); + if (!DeadlineExpired(request, elapsed)) + { + failure = null; + return false; + } + failure = CycleDeadlineFailure(request, phase, remaining); + return true; + } + + private static bool CallerCancellationRequested(EmPlanningRequest request, + CancellationToken cancellationToken) + { + if (request?.CallerCancellationToken.IsCancellationRequested == true) + return true; + if (!cancellationToken.IsCancellationRequested) + return false; + return request == null || !request.CycleDeadlineToken.IsCancellationRequested || + !request.CallerCancellationToken.CanBeCanceled; + } + + private static bool DeadlineExpired(EmPlanningRequest request, Func elapsed) + { + return request != null && (request.CycleDeadlineToken.IsCancellationRequested || + request.IsCycleDeadlineExpired() || + request.CycleDeadlineRemaining.HasValue && CycleRemaining(request, elapsed) <= TimeSpan.Zero); + } + + private static TimeSpan CycleRemaining(EmPlanningRequest request, Func elapsed) + { + TimeSpan snapshotRemaining = TimeSpan.MaxValue; + if (request.CycleDeadlineRemaining.HasValue) + { + snapshotRemaining = request.CycleDeadlineRemaining.Value - elapsed(); + if (snapshotRemaining < TimeSpan.Zero) + snapshotRemaining = TimeSpan.Zero; + } + TimeSpan? dynamicRemaining = request.DynamicCycleDeadlineRemaining(); + if (!dynamicRemaining.HasValue) + return snapshotRemaining; + return dynamicRemaining.Value < snapshotRemaining ? dynamicRemaining.Value : snapshotRemaining; + } + + private static TimeSpan SmallerBudget(TimeSpan configured, TimeSpan cycleRemaining) + { + return configured < cycleRemaining ? configured : cycleRemaining; + } + + private static EmPlanningResult CycleDeadlineFailure(EmPlanningRequest request, string phase, + TimeSpan remaining) + { + return Failure(EmPlanningStatus.CycleDeadlineExpired, request, + "cycleDeadlineExpired=true;phase=" + phase + ";remainingMs=" + + remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); + } + private void EmitDebug(EmPlanningRequest request, string message) { if (defaultDebugSink == null || request == null || request.Configuration == null || request.Configuration.Solver == null || diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjection.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjection.cs index 6aa1609..f56e714 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjection.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjection.cs @@ -2,9 +2,17 @@ using System; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// World pose projected onto one bounded direction segment. +/// +/// 世界车辆位姿投影到单一受限方向段后的 Frenet 描述。 +/// S 与 L 单位均为 m,航向误差为 rad,距离以 m² 保存;对象不代表跨越换向边界的投影。 +/// public sealed class FrenetProjection { + /// + /// 从参考点和相对量创建不可变投影结果。 + /// 参数:lateralOffset 为沿行驶方向左法线的 L(m),headingError 为行驶航向差(rad),squaredDistanceMeters 为非负 m²。 + /// 返回:携带参考 S 的投影;空参考点、非有限值或负平方距离会被拒绝。 + /// public FrenetProjection(FrenetReferencePoint referencePoint, double lateralOffset, double headingError, double squaredDistanceMeters) { @@ -20,12 +28,35 @@ public sealed class FrenetProjection SquaredDistanceMeters = squaredDistanceMeters; } + /// + /// 投影命中的插值参考点,其坐标为世界 X/Y(m)。 + /// public FrenetReferencePoint ReferencePoint { get; } + + /// + /// 命中点在当前方向段的局部参考弧长 S,单位 m。 + /// public double ReferenceS { get; } + + /// + /// 世界位姿相对参考行驶方向左法线的横向偏移 L,单位 m。 + /// public double LateralOffset { get; } + + /// + /// 车辆行驶航向相对参考行驶航向的归一化误差,单位 rad。 + /// public double HeadingError { get; } + + /// + /// 世界位置与命中参考位置的欧氏距离平方,单位 m²。 + /// public double SquaredDistanceMeters { get; } + /// + /// 判定投影标量是否有限。 + /// 参数:value 为任意实数;返回:NaN 和正负无穷均返回 false。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjector.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjector.cs index cfa5848..b2b01c6 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjector.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetProjector.cs @@ -4,11 +4,21 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Deterministically projects a world pose only inside the supplied direction segment and S window. +/// +/// 在指定方向段及局部 S 窗口内确定性地投影世界位姿。 +/// 投影不跨越换向边界;世界 X/Y 与距离为 m,航向为 rad,并以种子 S 消除等距候选的拓扑歧义。 +/// public sealed class FrenetProjector { + /// + /// 等距候选比较和零长度线段识别使用的 S/距离平方数值容差 1e-14。 + /// private const double TieTolerance = 1e-14d; + /// + /// 在窗口内投影世界位姿,并以窗口起点作为等距候选的种子 S。 + /// 参数:窗口与 maximumDistanceMeters 均为当前段局部 m 制距离;返回:命中距离不超过阈值时返回 true,否则 projection 为 null。 + /// public bool TryProject(Pose2D worldPose, DirectionSegmentView segment, double minimumReferenceS, double maximumReferenceS, double maximumDistanceMeters, out FrenetProjection projection) { @@ -16,6 +26,10 @@ public sealed class FrenetProjector minimumReferenceS, out projection); } + /// + /// 在窗口内投影世界位姿,按距离、种子 S 距离及较小 S 的顺序稳定选择候选。 + /// 参数:worldPose 使用世界 X/Y(m)和车身航向(rad),S 窗口、距离阈值和种子均为 m;返回:窗口非法、无候选或候选超阈值时返回 false。 + /// public bool TryProject(Pose2D worldPose, DirectionSegmentView segment, double minimumReferenceS, double maximumReferenceS, double maximumDistanceMeters, double seedReferenceS, out FrenetProjection projection) { @@ -63,6 +77,10 @@ public sealed class FrenetProjector return true; } + /// + /// 考察被 S 窗口裁剪后的参考折线边,并将世界点的正交投影加入最优候选比较。 + /// 参数:端点为世界 X/Y(m)和局部 S(m),seedReferenceS 为 m;零长度(不超过 1e-14)边退化为两个端点比较。 + /// private static void ConsiderLine(Pose2D worldPose, FrenetReferencePoint start, FrenetReferencePoint end, double seedReferenceS, ref Candidate best) { @@ -84,12 +102,20 @@ public sealed class FrenetProjector Consider(worldPose, referenceS, x, y, seedReferenceS, ref best); } + /// + /// 将一个离散参考点作为投影候选参与比较。 + /// 参数:point 的位置为世界 m 制坐标,seedReferenceS 为局部 m 制种子;结果通过 best 原位更新。 + /// private static void ConsiderPoint(Pose2D worldPose, FrenetReferencePoint point, double seedReferenceS, ref Candidate best) { Consider(worldPose, point.ReferenceS, point.X, point.Y, seedReferenceS, ref best); } + /// + /// 依据世界平面平方距离登记候选,并按确定性优先级替换当前最佳值。 + /// 参数:referenceS、x、y 与 seedReferenceS 分别为局部 S(m)和世界坐标(m);平方距离由内部计算,单位 m²。 + /// private static void Consider(Pose2D worldPose, double referenceS, double x, double y, double seedReferenceS, ref Candidate best) { @@ -101,13 +127,25 @@ public sealed class FrenetProjector best = candidate; } + /// + /// 判定投影计算的标量是否有限。 + /// 参数:value 为任意实数;返回:NaN 与无穷均返回 false。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } + /// + /// 保存一个待比较的折线投影候选。 + /// ReferenceS 和 SeedDistance 使用 m,SquaredDistanceMeters 使用 m²,优先级由距离、种子距离及较小 S 依次决定。 + /// private sealed class Candidate { + /// + /// 创建投影候选。 + /// 参数:referenceS 与 seedDistance 为 m,squaredDistanceMeters 为 m²;调用方仅传入有限的已计算值。 + /// public Candidate(double referenceS, double squaredDistanceMeters, double seedDistance) { ReferenceS = referenceS; @@ -115,10 +153,25 @@ public sealed class FrenetProjector SeedDistance = seedDistance; } + /// + /// 候选在当前方向段的局部参考弧长 S,单位 m。 + /// public double ReferenceS { get; } + + /// + /// 候选世界位置到车辆位置的距离平方,单位 m²。 + /// public double SquaredDistanceMeters { get; } + + /// + /// 候选 S 与调用方种子 S 的绝对距离,单位 m。 + /// public double SeedDistance { get; } + /// + /// 比较两个候选的稳定优先级。 + /// 参数:other 为非空候选;返回:平方距离差超过 1e-14 时取较小者,随后取较近种子,仍相等时取较小 S。 + /// public bool IsPreferredTo(Candidate other) { if (SquaredDistanceMeters < other.SquaredDistanceMeters - TieTolerance) return true; @@ -129,3 +182,7 @@ public sealed class FrenetProjector } } } + /// + /// 在窗口内投影世界位姿,按距离、种子 S 距离及较小 S 的顺序稳定选择候选。 + /// 参数:worldPose 使用世界 X/Y(m)和车身航向(rad),S 窗口、距离阈值和种子均为 m;返回:窗口非法、无候选或候选超阈值时返回 false。 + /// diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetReferencePoint.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetReferencePoint.cs index 043b70a..95b8afe 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetReferencePoint.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetReferencePoint.cs @@ -4,9 +4,17 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Immutable interpolated reference sample in a single direction segment. +/// +/// 单一方向段内的不可变插值参考样本,连接世界几何与 Frenet 坐标。 +/// X/Y 与参考 S 为 m,航向为 rad,曲率为 1/m、曲率导数为 1/m²;倒车段仍以车辆航向保存几何。 +/// public sealed class FrenetReferencePoint { + /// + /// 创建已验证的方向段参考样本。 + /// 参数:位置采用世界 X/Y(m),referenceS 为段局部弧长(m),航向为 rad,曲率量遵循 1/m 与 1/m²;所有数值必须有限。 + /// 返回:车辆航向会规范到 [-π, π];任一非有限输入会引发 。 + /// public FrenetReferencePoint(double referenceS, double x, double y, double vehicleYaw, double unwrappedVehicleYaw, TravelDirection direction, double geometricCurvature, double vehicleCurvature, double vehicleCurvatureDerivative, double bodyClearance) @@ -33,23 +41,69 @@ public sealed class FrenetReferencePoint BodyClearance = bodyClearance; } + /// + /// 样本在当前方向段的局部参考弧长 S,单位 m。 + /// public double ReferenceS { get; } + + /// + /// 参考中心线点的世界 X 坐标,单位 m。 + /// public double X { get; } + + /// + /// 参考中心线点的世界 Y 坐标,单位 m。 + /// public double Y { get; } + + /// + /// 归一化后的车辆车身航向,单位 rad。 + /// public double VehicleYaw { get; } + + /// + /// 连续展开的车辆车身航向,单位 rad,供跨点几何插值使用。 + /// public double UnwrappedVehicleYaw { get; } + + /// + /// 该参考样本的行驶方向,决定 Frenet 横向正负号和行驶航向。 + /// public TravelDirection Direction { get; } + + /// + /// 中心线几何曲率,单位 1/m。 + /// public double GeometricCurvature { get; } + + /// + /// 满足车辆模型后的车辆曲率,单位 1/m。 + /// public double VehicleCurvature { get; } + + /// + /// 车辆曲率相对弧长的导数,单位 1/m²。 + /// public double VehicleCurvatureDerivative { get; } + + /// + /// 参考点记录的车体净空或可用裕度,单位 m。 + /// public double BodyClearance { get; } - /// Unwrapped direction of travel, used internally for geometry. + /// + /// 获取与当前行驶方向一致的连续航向。 + /// 返回:前进时为车辆展开航向,倒车时加 π;单位 rad,仅供 Frenet 几何计算,不重新归一化。 + /// public double TravelYaw { get { return Direction == TravelDirection.Forward ? UnwrappedVehicleYaw : UnwrappedVehicleYaw + Math.PI; } } + /// + /// 拒绝不能安全保存为参考几何的数值。 + /// 参数:value 是待验证的任意单位标量,name 是异常参数名;NaN 或无穷会引发 。 + /// private static void RequireFinite(double value, string name) { if (double.IsNaN(value) || double.IsInfinity(value)) diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetTransform.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetTransform.cs index 3ca1d29..1ddaa1a 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetTransform.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/FrenetTransform.cs @@ -4,9 +4,17 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Coordinate conversion that keeps Frenet lateral sign relative to travel direction. +/// +/// 在世界坐标与 Frenet 横向约定间转换,并始终以实际行驶方向确定 L 的正负。 +/// 世界位置使用 X/Y(m)、航向使用 rad;倒车时车辆车身航向与行驶航向相差 π。 +/// public static class FrenetTransform { + /// + /// 将参考点及其横向状态重建为世界车辆中心位姿。 + /// 参数:lateralOffset 为 L(m),lateralDerivative 为 dL/dS(无量纲),minimumFrenetDenominator 为正的奇异性下界;参考点采用世界 X/Y(m)与 rad 航向。 + /// 返回:当 1-κL 有限且不小于下界、重建位姿有限时返回 true;空参考点、非法输入或接近 Frenet 奇异点时返回 false。 + /// public static bool TryReconstruct(FrenetReferencePoint referencePoint, double lateralOffset, double lateralDerivative, double minimumFrenetDenominator, out Pose2D pose) { @@ -33,11 +41,19 @@ public static class FrenetTransform return true; } + /// + /// 从车辆车身航向取得对应实际行驶的连续航向。 + /// 参数:vehicleYaw 为 rad,direction 指定前进或倒车;返回:前进原样、倒车加 π,结果不归一化。 + /// internal static double GetTravelYaw(double vehicleYaw, TravelDirection direction) { return direction == TravelDirection.Forward ? vehicleYaw : vehicleYaw + Math.PI; } + /// + /// 判定重建中间量是否为有限实数。 + /// 参数:value 为任意标量;返回:NaN 或正负无穷时为 false。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/ReferencePathInterpolator.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/ReferencePathInterpolator.cs index c4ce493..db4babd 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/ReferencePathInterpolator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/ReferencePathInterpolator.cs @@ -4,11 +4,22 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Interpolates reference geometry inside exactly one direction segment. +/// +/// 在恰好一个方向段内按局部参考弧长插值参考几何。 +/// 输入和输出 S、X/Y 与净空均为 m,航向为 rad,曲率为 1/m;插值不会越过方向段边界。 +/// public static class ReferencePathInterpolator { + /// + /// 接受端点轻微 S 超差并判断精确节点的数值容差,单位 m。 + /// private const double Epsilon = 1e-12d; + /// + /// 为给定局部 S 生成方向一致的 Frenet 参考样本。 + /// 参数:segment 不可为空,referenceS 为 m;[-1e-12, Length+1e-12] 内的端点超差会夹紧到边界。 + /// 返回:精确点直接转换,区间内对位置、展开航向和几何量线性插值;超界或退化跨度会引发异常。 + /// public static FrenetReferencePoint Interpolate(DirectionSegmentView segment, double referenceS) { if (segment == null) @@ -45,6 +56,10 @@ public static class ReferencePathInterpolator return FromPoint(segment.Points[segment.Points.Count - 1]); } + /// + /// 将原始平滑路径点转换为同一局部 S 的 Frenet 参考样本。 + /// 参数:point 已携带世界 X/Y(m)、航向(rad)和曲率量;返回:保持其方向及所有几何量的不可变副本。 + /// private static FrenetReferencePoint FromPoint(SmoothedPathPoint point) { return new FrenetReferencePoint(point.ArcLength, point.X, point.Y, point.Heading, point.UnwrappedHeading, @@ -52,11 +67,19 @@ public static class ReferencePathInterpolator point.BodyClearance); } + /// + /// 在线性标量区间内计算插值值。 + /// 参数:lower、upper 为同单位端点,fraction 为无单位比例;返回:同单位的未夹紧线性结果。 + /// private static double Lerp(double lower, double upper, double fraction) { return lower + (upper - lower) * fraction; } + /// + /// 判定插值输入是否为有限实数。 + /// 参数:value 为任意标量;返回:NaN 或无穷时为 false。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs index f8f1fb7..96725a5 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; @@ -16,7 +17,15 @@ public sealed class LongitudinalConstraintBuilder public bool TryBuild(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate iterate, out QuadraticProgram problem, out string failureReason) { - return TryBuildCore(input, speedLimit, iterate, false, out problem, out failureReason); + return TryBuildCore(input, speedLimit, iterate, false, null, null, 0d, out problem, out failureReason); + } + + internal bool TryBuildTrusted(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, + LongitudinalCandidate anchor, LongitudinalEnvelopeTrustRegion trustRegion, double strictTolerance, + out QuadraticProgram problem, out string failureReason) + { + return TryBuildCore(input, speedLimit, anchor, false, trustRegion, anchor, strictTolerance, + out problem, out failureReason); } /// Builds the bounded full-scope feasibility projection before objective optimization. @@ -38,12 +47,14 @@ public sealed class LongitudinalConstraintBuilder failureReason = "Initial feasibility projection is only defined for full-direction exact-stop planning."; return false; } - return TryBuildCore(input, speedLimit, linearizationIterate, true, out problem, - out failureReason); + return TryBuildCore(input, speedLimit, linearizationIterate, true, null, null, 0d, + out problem, out failureReason); } private bool TryBuildCore(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate iterate, - bool useScheduleReferenceObjective, out QuadraticProgram problem, out string failureReason) + bool useScheduleReferenceObjective, LongitudinalEnvelopeTrustRegion trustRegion, + LongitudinalCandidate trustedAnchor, double strictTolerance, out QuadraticProgram problem, + out string failureReason) { problem = null; failureReason = string.Empty; @@ -63,6 +74,15 @@ public sealed class LongitudinalConstraintBuilder { throw new ArgumentException("The ST iterate does not match the configured knot layout."); } + if ((trustRegion == null) != (trustedAnchor == null)) + throw new ArgumentException("Trusted QP construction requires both a trust region and anchor."); + if (trustRegion != null && (trustRegion.MinimumPathS.Count != layout.KnotCount || + trustRegion.MaximumPathS.Count != layout.KnotCount || + trustRegion.SpeedSlope.Count != layout.KnotCount || + trustRegion.SpeedIntercept.Count != layout.KnotCount)) + { + throw new ArgumentException("The trust region does not match the configured knot layout."); + } if (!PathSpeedLimitBuilder.TryGetLimits(input, out double directionMaximum, out double maximumAcceleration, out double maximumDeceleration, out double maximumJerk, out _, out _, out failureReason)) { @@ -84,12 +104,14 @@ public sealed class LongitudinalConstraintBuilder _objectiveBuilder.AddTerms(input, speedLimit, layout, iterate, hessian, linearCost); int stabilizationStart = GetStabilizationStart(input, expectedTimes, layout.KnotCount); int stationaryKnotCount = layout.KnotCount - stabilizationStart; - int expectedRows = 9 * layout.KnotCount - 3 + 3 * stationaryKnotCount; + int expectedRows = 10 * layout.KnotCount - 3 + 3 * stationaryKnotCount; var constraints = new SparseTripletBuilder(expectedRows, layout.VariableCount); var lower = new List(expectedRows); var upper = new List(expectedRows); int row = 0; - AddVariableBounds(input, speedLimit, iterate, layout, maximumAcceleration, maximumDeceleration, maximumJerk, + AddVariableBounds(input, speedLimit, iterate, trustRegion, layout, maximumAcceleration, + maximumDeceleration, maximumJerk, constraints, lower, upper, ref row); + AddLowSpeedDecelerationReleaseEnvelope(layout, iterate, maximumJerk, constraints, lower, upper, ref row); AddMonotonicProgress(layout, constraints, lower, upper, ref row); AddExactDynamics(expectedTimes, layout, constraints, lower, upper, ref row); @@ -99,6 +121,20 @@ public sealed class LongitudinalConstraintBuilder if (row != expectedRows) throw new InvalidOperationException("ST constraint row accounting is inconsistent."); problem = new QuadraticProgram(hessian.Build(), linearCost, constraints.Build(), lower, upper); + if (trustedAnchor != null) + { + LongitudinalQpAuditResult audit = LongitudinalQpFeasibilityAudit.Evaluate(problem, trustedAnchor, + strictTolerance, layout, stabilizationStart); + if (!audit.IsFeasible) + { + problem = null; + failureReason = "Planner invariant failure: strict anchor is outside trusted QP" + + ";row=" + audit.WorstRow + ";category=" + audit.Category + + ";residual=" + audit.MaximumResidual.ToString("R", CultureInfo.InvariantCulture) + + audit.Unit + ";tolerance=" + strictTolerance.ToString("R", CultureInfo.InvariantCulture); + return false; + } + } return true; } catch (ArgumentException exception) @@ -145,7 +181,8 @@ public sealed class LongitudinalConstraintBuilder } private static void AddVariableBounds(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, - LongitudinalCandidate iterate, LongitudinalVariableLayout layout, double maximumAcceleration, + LongitudinalCandidate iterate, LongitudinalEnvelopeTrustRegion trustRegion, + LongitudinalVariableLayout layout, double maximumAcceleration, double maximumDeceleration, double maximumJerk, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { @@ -153,14 +190,32 @@ public sealed class LongitudinalConstraintBuilder { if (iterate.S[index] < 0d || iterate.S[index] > input.PathUpperBoundS) throw new ArgumentException("The ST iterate progress lies outside actual PathS bounds."); - AddSingleVariableRow(constraints, lower, upper, layout.S(index), 0d, input.PathUpperBoundS, ref row); + if (trustRegion == null) + AddSingleVariableRow(constraints, lower, upper, layout.S(index), 0d, input.PathUpperBoundS, ref row); + else + AddSingleVariableRow(constraints, lower, upper, layout.S(index), + trustRegion.MinimumPathS[index], trustRegion.MaximumPathS[index], ref row); double maximumSpeed = index == 0 ? input.DirectionMaximumSpeedMetersPerSecond : input.DirectionMaximumSpeedMetersPerSecond; AddSingleVariableRow(constraints, lower, upper, layout.U(index), 0d, maximumSpeed, ref row); if (index > 0) - AddLinearizedSpeedEnvelopeRow(speedLimit, iterate.S[index], layout.S(index), layout.U(index), - constraints, lower, upper, ref row); + { + if (trustRegion == null) + { + AddLinearizedSpeedEnvelopeRow(speedLimit, iterate.S[index], layout.S(index), layout.U(index), + constraints, lower, upper, ref row); + } + else + { + AddRow(constraints, lower, upper, row, new[] + { + new Coefficient(layout.U(index), 1d), + new Coefficient(layout.S(index), -trustRegion.SpeedSlope[index]), + }, -QuadraticProgram.MaximumFiniteBound, trustRegion.SpeedIntercept[index]); + row++; + } + } AddSingleVariableRow(constraints, lower, upper, layout.A(index), -maximumDeceleration, maximumAcceleration, ref row); } @@ -209,6 +264,34 @@ public sealed class LongitudinalConstraintBuilder } } + internal static void CalculateLowSpeedDecelerationReleaseTangent( + double anchorAcceleration, double maximumJerk, + out double accelerationCoefficient, out double lowerBound) + { + if (!IsFinite(anchorAcceleration) || !IsFinite(maximumJerk) || maximumJerk <= 0d) + throw new ArgumentOutOfRangeException(nameof(anchorAcceleration)); + double a0 = Math.Min(0d, anchorAcceleration); + accelerationCoefficient = -a0 / maximumJerk; + lowerBound = -(a0 * a0) / (2d * maximumJerk); + } + + private static void AddLowSpeedDecelerationReleaseEnvelope(LongitudinalVariableLayout layout, + LongitudinalCandidate iterate, double maximumJerk, SparseTripletBuilder constraints, + IList lower, IList upper, ref int row) + { + for (int index = 0; index < layout.KnotCount; index++) + { + CalculateLowSpeedDecelerationReleaseTangent(iterate.A[index], maximumJerk, + out double accelerationCoefficient, out double lowerBound); + AddRow(constraints, lower, upper, row, new[] + { + new Coefficient(layout.U(index), 1d), + new Coefficient(layout.A(index), accelerationCoefficient), + }, lowerBound, QuadraticProgram.MaximumFiniteBound); + row++; + } + } + private static void AddExactDynamics(IReadOnlyList times, LongitudinalVariableLayout layout, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalContinuousProfileValidator.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalContinuousProfileValidator.cs new file mode 100644 index 0000000..621cbbb --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalContinuousProfileValidator.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; + +/// Proves the constant-jerk profile used by publication is continuously forward-progressing. +internal static class LongitudinalContinuousProfileValidator +{ + public static bool TryValidate(LongitudinalCandidate candidate, double tolerance, out string failureReason) + { + failureReason = string.Empty; + if (candidate == null) + { + failureReason = "A longitudinal candidate is required."; + return false; + } + if (!IsFinite(tolerance) || tolerance < 0d) + { + failureReason = "A finite nonnegative continuous-profile tolerance is required."; + return false; + } + + double highWater = candidate.S[0]; + for (int interval = 0; interval < candidate.J.Count; interval++) + { + double duration = candidate.KnotTimes[interval + 1] - candidate.KnotTimes[interval]; + double initialS = candidate.S[interval]; + double initialU = candidate.U[interval]; + double initialA = candidate.A[interval]; + double jerk = candidate.J[interval]; + var evaluationTimes = new List(5) { 0d, duration }; + if (jerk != 0d) + AddIfInside(evaluationTimes, -initialA / jerk, duration); + AddSpeedRoots(evaluationTimes, initialU, initialA, jerk, duration); + evaluationTimes.Sort(); + + double previousTime = double.NegativeInfinity; + for (int point = 0; point < evaluationTimes.Count; point++) + { + double localTime = evaluationTimes[point]; + if (localTime == previousTime) + continue; + previousTime = localTime; + Evaluate(initialS, initialU, initialA, jerk, localTime, + out double progress, out double speed, out double acceleration); + double regression = highWater - progress; + if (!IsFinite(progress) || !IsFinite(speed) || !IsFinite(acceleration) || + speed < -tolerance || regression > tolerance) + { + string kind = !IsFinite(progress) || !IsFinite(speed) || !IsFinite(acceleration) + ? "non-finite" + : speed < -tolerance ? "negative-speed" : "PathS-regression"; + failureReason = FormatFailure(kind, interval, localTime, progress, speed, + acceleration, jerk, regression, string.Empty); + return false; + } + if (progress > highWater) + highWater = progress; + } + + Evaluate(initialS, initialU, initialA, jerk, duration, + out double integratedS, out double integratedU, out double integratedA); + double sMismatch = Math.Abs(integratedS - candidate.S[interval + 1]); + double uMismatch = Math.Abs(integratedU - candidate.U[interval + 1]); + double aMismatch = Math.Abs(integratedA - candidate.A[interval + 1]); + if (!IsFinite(sMismatch) || !IsFinite(uMismatch) || !IsFinite(aMismatch) || + sMismatch > tolerance || uMismatch > tolerance || aMismatch > tolerance) + { + string detail = ";endpointMismatchS=" + Invariant(sMismatch) + + ";endpointMismatchU=" + Invariant(uMismatch) + + ";endpointMismatchA=" + Invariant(aMismatch); + failureReason = FormatFailure("endpoint-mismatch", interval, duration, integratedS, + integratedU, integratedA, jerk, highWater - integratedS, detail); + return false; + } + } + return true; + } + + private static void AddSpeedRoots(ICollection times, double initialU, double initialA, + double jerk, double duration) + { + if (jerk == 0d) + { + if (initialA != 0d) + AddIfInside(times, -initialU / initialA, duration); + return; + } + + double discriminant = initialA * initialA - 2d * jerk * initialU; + if (!IsFinite(discriminant) || discriminant < 0d) + return; + double rootTerm = Math.Sqrt(discriminant); + AddIfInside(times, (-initialA - rootTerm) / jerk, duration); + AddIfInside(times, (-initialA + rootTerm) / jerk, duration); + } + + private static void AddIfInside(ICollection times, double localTime, double duration) + { + if (IsFinite(localTime) && localTime >= 0d && localTime <= duration) + times.Add(localTime); + } + + private static void Evaluate(double initialS, double initialU, double initialA, double jerk, + double localTime, out double progress, out double speed, out double acceleration) + { + acceleration = initialA + jerk * localTime; + speed = initialU + initialA * localTime + 0.5d * jerk * localTime * localTime; + progress = initialS + initialU * localTime + 0.5d * initialA * localTime * localTime + + jerk * localTime * localTime * localTime / 6d; + } + + private static string FormatFailure(string kind, int interval, double localTime, double progress, + double speed, double acceleration, double jerk, double regression, string detail) + { + return "Continuous ST profile rejected: kind=" + kind + + ";interval=" + interval.ToString(CultureInfo.InvariantCulture) + + ";localTime=" + Invariant(localTime) + + ";S=" + Invariant(progress) + + ";U=" + Invariant(speed) + + ";A=" + Invariant(acceleration) + + ";J=" + Invariant(jerk) + + ";regression=" + Invariant(regression) + detail + "."; + } + + private static string Invariant(double value) + { + return value.ToString("R", CultureInfo.InvariantCulture); + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalEnvelopeTrustRegion.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalEnvelopeTrustRegion.cs new file mode 100644 index 0000000..55f3935 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalEnvelopeTrustRegion.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; + +internal sealed class LongitudinalEnvelopeTrustRegion +{ + internal LongitudinalEnvelopeTrustRegion(IReadOnlyList minimumPathS, IReadOnlyList maximumPathS, + IReadOnlyList speedSlope, IReadOnlyList speedIntercept, + IReadOnlyList envelopeSegmentIndex, double scale) + { + MinimumPathS = Copy(minimumPathS, nameof(minimumPathS)); + MaximumPathS = Copy(maximumPathS, nameof(maximumPathS)); + SpeedSlope = Copy(speedSlope, nameof(speedSlope)); + SpeedIntercept = Copy(speedIntercept, nameof(speedIntercept)); + EnvelopeSegmentIndex = Copy(envelopeSegmentIndex, nameof(envelopeSegmentIndex)); + Scale = scale; + } + + internal IReadOnlyList MinimumPathS { get; } + + internal IReadOnlyList MaximumPathS { get; } + + internal IReadOnlyList SpeedSlope { get; } + + internal IReadOnlyList SpeedIntercept { get; } + + internal IReadOnlyList EnvelopeSegmentIndex { get; } + + internal double Scale { get; } + + internal bool CanShrinkTo(double nextScale, double minimumActiveWidthMeters, out string failureReason) + { + failureReason = string.Empty; + if (!IsFinite(nextScale) || nextScale <= 0d || nextScale >= Scale) + { + failureReason = "Next trust-region scale must be finite, positive, and smaller than the current scale."; + return false; + } + if (!IsFinite(minimumActiveWidthMeters) || minimumActiveWidthMeters <= 0d) + { + failureReason = "Minimum active width must be finite and positive."; + return false; + } + double ratio = nextScale / Scale; + for (int index = 0; index < MinimumPathS.Count; index++) + { + double currentWidth = MaximumPathS[index] - MinimumPathS[index]; + if (currentWidth > 0d && currentWidth * ratio < minimumActiveWidthMeters) + { + failureReason = "The next trust-region scale would fall below the minimum width at knot " + index + "."; + return false; + } + } + return true; + } + + private static IReadOnlyList Copy(IReadOnlyList source, string parameterName) + { + if (source == null) + throw new ArgumentNullException(parameterName); + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList Copy(IReadOnlyList source, string parameterName) + { + if (source == null) + throw new ArgumentNullException(parameterName); + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} + +internal sealed class LongitudinalEnvelopeTrustRegionBuilder +{ + private const double ScheduleProgressTolerance = 1e-12d; + private const double StationSelectionTolerance = 1e-12d; + + internal bool TryBuild(PathSpeedLimit speedLimit, LongitudinalCandidate anchor, + IReadOnlyList referencePathS, int terminalHoldStartIndex, double scale, + double minimumActiveWidthMeters, out LongitudinalEnvelopeTrustRegion region, + out string failureReason) + { + region = null; + failureReason = string.Empty; + if (speedLimit == null || anchor == null || referencePathS == null) + { + failureReason = "Trust region inputs must be present."; + return false; + } + if (!IsSupportedScale(scale)) + { + failureReason = "Trust-region scale must be one of 1, 0.5, 0.25, or 0.125."; + return false; + } + if (!IsFinite(minimumActiveWidthMeters) || minimumActiveWidthMeters <= 0d) + { + failureReason = "Minimum active width must be finite and positive."; + return false; + } + if (referencePathS.Count != anchor.S.Count) + { + failureReason = "Reference PathS count must match the anchor knot count."; + return false; + } + if (terminalHoldStartIndex == -1) + terminalHoldStartIndex = anchor.S.Count; + if (terminalHoldStartIndex < 1 || terminalHoldStartIndex > anchor.S.Count) + { + failureReason = "Terminal-hold start index is outside the anchor knot range."; + return false; + } + if (!TryValidateAnchorAndReference(speedLimit, anchor, referencePathS, out failureReason)) + return false; + + int knotCount = anchor.S.Count; + var minimumPathS = new double[knotCount]; + var maximumPathS = new double[knotCount]; + var speedSlope = new double[knotCount]; + var speedIntercept = new double[knotCount]; + var segmentIndex = new int[knotCount]; + for (int index = 0; index < knotCount; index++) + { + double anchorS = anchor.S[index]; + bool fixedKnot = index == 0 || index >= terminalHoldStartIndex; + int segment = SelectSegment(speedLimit, anchorS, referencePathS, index, fixedKnot); + FindMaximalExactAffineRun(speedLimit, segment, out int firstSegment, out int lastSegment, + out double slope, out double intercept); + double lower = speedLimit.PathS[firstSegment]; + double upper = speedLimit.PathS[lastSegment + 1]; + if (anchorS < lower && lower - anchorS <= StationSelectionTolerance) + lower = anchorS; + if (anchorS > upper && anchorS - upper <= StationSelectionTolerance) + upper = anchorS; + double trustedLower = fixedKnot ? anchorS : anchorS - scale * (anchorS - lower); + double trustedUpper = fixedKnot ? anchorS : anchorS + scale * (upper - anchorS); + if (!fixedKnot && scale < 1d && trustedUpper - trustedLower < minimumActiveWidthMeters) + { + failureReason = "Active trust-region interval is narrower than the minimum width at knot " + index + "."; + return false; + } + + minimumPathS[index] = trustedLower; + maximumPathS[index] = trustedUpper; + speedSlope[index] = slope; + speedIntercept[index] = intercept; + segmentIndex[index] = segment; + } + + region = new LongitudinalEnvelopeTrustRegion(minimumPathS, maximumPathS, speedSlope, speedIntercept, + segmentIndex, scale); + return true; + } + + private static bool TryValidateAnchorAndReference(PathSpeedLimit speedLimit, LongitudinalCandidate anchor, + IReadOnlyList referencePathS, out string failureReason) + { + if (anchor.S[0] != 0d) + { + failureReason = "The anchor must begin at exact PathS zero."; + return false; + } + + double minimumPathS = speedLimit.PathS[0]; + double maximumPathS = speedLimit.PathS[speedLimit.PathS.Count - 1]; + double previousAnchorS = double.NegativeInfinity; + for (int index = 0; index < anchor.S.Count; index++) + { + double anchorS = anchor.S[index]; + double referenceS = referencePathS[index]; + if (!IsFinite(anchorS) || anchorS < minimumPathS || anchorS > maximumPathS) + { + failureReason = "Anchor PathS is outside the speed-limit range at knot " + index + "."; + return false; + } + if (anchorS < previousAnchorS) + { + failureReason = "Anchor PathS must be nondecreasing."; + return false; + } + if (!IsFinite(referenceS)) + { + failureReason = "Reference PathS must be finite."; + return false; + } + previousAnchorS = anchorS; + } + + failureReason = string.Empty; + return true; + } + + private static int SelectSegment(PathSpeedLimit speedLimit, double anchorS, IReadOnlyList referencePathS, + int knotIndex, bool terminalHold) + { + int lastSegment = speedLimit.PathS.Count - 2; + if (Math.Abs(anchorS - speedLimit.PathS[0]) <= StationSelectionTolerance) + return 0; + if (Math.Abs(anchorS - speedLimit.PathS[speedLimit.PathS.Count - 1]) <= StationSelectionTolerance) + return lastSegment; + for (int index = 1; index < speedLimit.PathS.Count - 1; index++) + { + if (Math.Abs(anchorS - speedLimit.PathS[index]) <= StationSelectionTolerance) + { + if (terminalHold) + return index; + double scheduleDelta = referencePathS[knotIndex] - referencePathS[knotIndex - 1]; + if (scheduleDelta > ScheduleProgressTolerance) + return index; + if (scheduleDelta < -ScheduleProgressTolerance) + return index - 1; + double leftWidth = speedLimit.PathS[index] - speedLimit.PathS[index - 1]; + double rightWidth = speedLimit.PathS[index + 1] - speedLimit.PathS[index]; + return rightWidth >= leftWidth ? index : index - 1; + } + if (anchorS < speedLimit.PathS[index]) + return index - 1; + } + return lastSegment; + } + + private static void GetAffineLine(PathSpeedLimit limit, int segment, + out double slope, out double intercept) + { + double lower = limit.PathS[segment]; + double upper = limit.PathS[segment + 1]; + slope = (limit.MaximumSpeedMetersPerSecond[segment + 1] - + limit.MaximumSpeedMetersPerSecond[segment]) / (upper - lower); + intercept = limit.MaximumSpeedMetersPerSecond[segment] - slope * lower; + } + + private static void FindMaximalExactAffineRun(PathSpeedLimit limit, int selectedSegment, + out int firstSegment, out int lastSegment, out double slope, out double intercept) + { + GetAffineLine(limit, selectedSegment, out slope, out intercept); + firstSegment = selectedSegment; + while (firstSegment > 0) + { + GetAffineLine(limit, firstSegment - 1, out double candidateSlope, out double candidateIntercept); + if (candidateSlope != slope || candidateIntercept != intercept) + break; + firstSegment--; + } + lastSegment = selectedSegment; + while (lastSegment < limit.PathS.Count - 2) + { + GetAffineLine(limit, lastSegment + 1, out double candidateSlope, out double candidateIntercept); + if (candidateSlope != slope || candidateIntercept != intercept) + break; + lastSegment++; + } + } + + private static bool IsSupportedScale(double scale) + { + return scale == 1d || scale == 0.5d || scale == 0.25d || scale == 0.125d; + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalQpFeasibilityAudit.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalQpFeasibilityAudit.cs new file mode 100644 index 0000000..db53896 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalQpFeasibilityAudit.cs @@ -0,0 +1,217 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; + +internal sealed class LongitudinalQpAuditResult +{ + internal LongitudinalQpAuditResult(bool isFeasible, double maximumResidual, int worstRow, + string category, string unit) + { + IsFeasible = isFeasible; + MaximumResidual = maximumResidual; + WorstRow = worstRow; + Category = category; + Unit = unit; + } + + internal bool IsFeasible { get; } + + internal double MaximumResidual { get; } + + internal int WorstRow { get; } + + internal string Category { get; } + + internal string Unit { get; } +} + +internal static class LongitudinalQpFeasibilityAudit +{ + internal static LongitudinalQpAuditResult Evaluate(QuadraticProgram problem, + LongitudinalCandidate candidate, double tolerance, LongitudinalVariableLayout layout, + int stabilizationStart) + { + if (problem == null) + throw new ArgumentNullException(nameof(problem)); + if (candidate == null) + throw new ArgumentNullException(nameof(candidate)); + if (layout == null) + throw new ArgumentNullException(nameof(layout)); + if (problem.VariableCount != layout.VariableCount || candidate.S.Count != layout.KnotCount || + candidate.U.Count != layout.KnotCount || candidate.A.Count != layout.KnotCount || + candidate.J.Count != layout.KnotCount - 1) + { + throw new ArgumentException("The QP, candidate, and longitudinal layout must have matching dimensions."); + } + + double[] primal = ToPrimal(candidate, layout); + var activity = new double[problem.ConstraintCount]; + bool allFinite = IsFinite(tolerance); + SparseCscMatrix matrix = problem.ConstraintMatrix; + for (int column = 0; column < matrix.ColumnCount; column++) + { + double value = primal[column]; + allFinite &= IsFinite(value); + for (int entry = matrix.ColumnPointers[column]; entry < matrix.ColumnPointers[column + 1]; entry++) + activity[matrix.RowIndices[entry]] += matrix.Values[entry] * value; + } + + double maximumResidual = 0d; + int worstRow = problem.ConstraintCount == 0 ? -1 : 0; + for (int row = 0; row < problem.ConstraintCount; row++) + { + double residual; + if (!IsFinite(activity[row])) + { + allFinite = false; + residual = double.PositiveInfinity; + } + else + { + residual = Math.Max(0d, Math.Max( + problem.LowerBounds[row] - activity[row], + activity[row] - problem.UpperBounds[row])); + } + if (row == 0 || residual > maximumResidual) + { + maximumResidual = residual; + worstRow = row; + } + } + + DescribeRow(worstRow, layout.KnotCount, stabilizationStart, out string category, out string unit); + return new LongitudinalQpAuditResult(allFinite && maximumResidual <= tolerance, + maximumResidual, worstRow, category, unit); + } + + private static double[] ToPrimal(LongitudinalCandidate candidate, LongitudinalVariableLayout layout) + { + var primal = new double[layout.VariableCount]; + for (int index = 0; index < layout.KnotCount; index++) + { + primal[layout.S(index)] = candidate.S[index]; + primal[layout.U(index)] = candidate.U[index]; + primal[layout.A(index)] = candidate.A[index]; + } + for (int index = 0; index < layout.KnotCount - 1; index++) + primal[layout.J(index)] = candidate.J[index]; + return primal; + } + + private static void DescribeRow(int targetRow, int knotCount, int stabilizationStart, + out string category, out string unit) + { + int row = 0; + for (int index = 0; index < knotCount; index++) + { + if (targetRow == row++) + { + category = "PathS trust"; + unit = "m"; + return; + } + if (targetRow == row++) + { + category = "speed"; + unit = "m/s"; + return; + } + if (index > 0 && targetRow == row++) + { + category = "speed envelope"; + unit = "m/s"; + return; + } + if (targetRow == row++) + { + category = "acceleration"; + unit = "m/s^2"; + return; + } + } + + for (int index = 0; index < knotCount - 1; index++) + { + if (targetRow == row++) + { + category = "jerk"; + unit = "m/s^3"; + return; + } + } + for (int index = 0; index < knotCount; index++) + { + if (targetRow == row++) + { + category = "low-speed deceleration release"; + unit = "m/s"; + return; + } + } + for (int index = 0; index < knotCount - 1; index++) + { + if (targetRow == row++) + { + category = "monotonic progress"; + unit = "m"; + return; + } + } + for (int index = 0; index < knotCount - 1; index++) + { + if (targetRow == row++) + { + category = "exact dynamics"; + unit = "m/s^2"; + return; + } + if (targetRow == row++) + { + category = "exact dynamics"; + unit = "m/s"; + return; + } + if (targetRow == row++) + { + category = "exact dynamics"; + unit = "m"; + return; + } + } + + string[] stateUnits = { "m", "m/s", "m/s^2" }; + for (int state = 0; state < stateUnits.Length; state++) + { + if (targetRow == row++) + { + category = "exact start"; + unit = stateUnits[state]; + return; + } + } + + int stopKnotCount = stabilizationStart >= 0 && stabilizationStart < knotCount + ? knotCount - stabilizationStart + : 0; + for (int index = 0; index < stopKnotCount; index++) + { + for (int state = 0; state < stateUnits.Length; state++) + { + if (targetRow == row++) + { + category = "exact stop"; + unit = stateUnits[state]; + return; + } + } + } + + category = "row accounting"; + unit = string.Empty; + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs index fc363fa..5069575 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; @@ -48,6 +49,8 @@ public sealed class LongitudinalSolutionValidator failureReason = "ST candidate violates exact constant-jerk dynamics."; return false; } + if (!LongitudinalContinuousProfileValidator.TryValidate(candidate, tolerance, out failureReason)) + return false; if (!AreClose(candidate.S[0], 0d, tolerance) || !AreClose(candidate.U[0], input.InitialProgressSpeedMetersPerSecond, tolerance) || !AreClose(candidate.A[0], input.InitialAccelerationMetersPerSecondSquared, tolerance)) @@ -56,6 +59,9 @@ public sealed class LongitudinalSolutionValidator return false; } + var canonicalS = new double[candidate.S.Count]; + var canonicalU = new double[candidate.U.Count]; + var canonicalA = new double[candidate.A.Count]; for (int index = 0; index < candidate.S.Count; index++) { double progress = candidate.S[index]; @@ -83,6 +89,11 @@ public sealed class LongitudinalSolutionValidator failureReason = "ST candidate PathS decreases at knot " + index + "."; return false; } + canonicalS[index] = progress; + canonicalU[index] = speed < 0d ? 0d : speed; + canonicalA[index] = speed < 0d && acceleration < 0d && acceleration >= -tolerance + ? 0d + : acceleration; } for (int index = 0; index < candidate.J.Count; index++) { @@ -122,25 +133,29 @@ public sealed class LongitudinalSolutionValidator { for (int index = 0; index < candidate.S.Count; index++) { - if (!JerkLimitedStoppingMath.TryCalculate(candidate.U[index], candidate.A[index], - maximumDeceleration, maximumJerk, out JerkLimitedStoppingProfile stop, out _) || - candidate.S[index] + stop.DistanceMeters > input.StopBoundaryPathS + tolerance) + bool hasStop = JerkLimitedStoppingMath.TryCalculate(canonicalU[index], canonicalA[index], + maximumDeceleration, maximumJerk, out JerkLimitedStoppingProfile stop, + out string stoppingFailure); + double stopDistance = hasStop ? stop.DistanceMeters : double.NaN; + double margin = hasStop + ? input.StopBoundaryPathS - canonicalS[index] - stopDistance + : double.NaN; + if (!hasStop || canonicalS[index] + stopDistance > input.StopBoundaryPathS + tolerance) { - failureReason = "ST candidate leaves the jerk-limited stoppable set at knot " + index + "."; + failureReason = "ST candidate leaves the jerk-limited stoppable set: knot=" + + index.ToString(CultureInfo.InvariantCulture) + + ";S=" + Invariant(canonicalS[index]) + + ";U=" + Invariant(canonicalU[index]) + + ";A=" + Invariant(canonicalA[index]) + + ";stopDistance=" + Invariant(stopDistance) + + ";stopBoundary=" + Invariant(input.StopBoundaryPathS) + + ";margin=" + Invariant(margin) + + (hasStop ? string.Empty : ";stoppingReason=" + stoppingFailure) + "."; return false; } } } - var canonicalS = new double[candidate.S.Count]; - var canonicalU = new double[candidate.U.Count]; - var canonicalA = new double[candidate.A.Count]; - for (int index = 0; index < candidate.S.Count; index++) - { - canonicalS[index] = candidate.S[index]; - canonicalU[index] = candidate.U[index]; - canonicalA[index] = candidate.A[index]; - } canonicalS[0] = 0d; canonicalU[0] = input.InitialProgressSpeedMetersPerSecond; canonicalA[0] = input.InitialAccelerationMetersPerSecondSquared; @@ -202,6 +217,11 @@ public sealed class LongitudinalSolutionValidator return Math.Abs(actual - expected) <= tolerance; } + private static string Invariant(double value) + { + return value.ToString("R", CultureInfo.InvariantCulture); + } + private static double RequireNonnegative(double value, string parameterName) { if (!IsFinite(value) || value < 0d) diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolveTrace.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolveTrace.cs new file mode 100644 index 0000000..1bf61d0 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolveTrace.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; + +internal sealed class LongitudinalSolveTrace +{ + private const int MaximumEntries = 12; + private readonly List _entries = new List(MaximumEntries); + + internal void Add(string phase, int callOrdinal, int anchorUpdateIndex, double trustScale, + TimeSpan remainingBudget, TimeSpan remainingAfterReserve, TimeSpan elapsed, QpSolveResult result, + double anchorObjective, double candidateObjective, string rejection) + { + if (_entries.Count >= MaximumEntries) + throw new InvalidOperationException("Longitudinal solve trace exceeded the twelve-call cap."); + string status = result == null ? "null" : result.Status.ToString(); + int iterations = result == null ? -1 : result.Iterations; + double primal = result == null ? double.NaN : result.PrimalResidual; + double dual = result == null ? double.NaN : result.DualResidual; + _entries.Add("phase:" + phase + + ",call:" + callOrdinal.ToString(CultureInfo.InvariantCulture) + + ",anchor:" + anchorUpdateIndex.ToString(CultureInfo.InvariantCulture) + + ",scale:" + trustScale.ToString("R", CultureInfo.InvariantCulture) + + ",budgetMs:" + remainingBudget.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture) + + ",remainingAfterReserveMs:" + remainingAfterReserve.TotalMilliseconds.ToString( + "F3", CultureInfo.InvariantCulture) + + ",elapsedMs:" + elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture) + + ",status:" + status + + ",iterations:" + iterations.ToString(CultureInfo.InvariantCulture) + + ",primal:" + primal.ToString("R", CultureInfo.InvariantCulture) + + ",dual:" + dual.ToString("R", CultureInfo.InvariantCulture) + + ",anchorObj:" + anchorObjective.ToString("R", CultureInfo.InvariantCulture) + + ",candidateObj:" + candidateObjective.ToString("R", CultureInfo.InvariantCulture) + + ",rejection:" + Sanitize(rejection)); + } + + internal string Format() => string.Join("|", _entries); + + private static string Sanitize(string text) + { + if (string.IsNullOrEmpty(text)) + return "none"; + return text.Replace(';', '/').Replace('|', '/').Replace('\r', '/').Replace('\n', '/'); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs index 349dc8d..cb26a84 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs @@ -10,13 +10,21 @@ namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// Bounded ST envelope iteration retaining only independently validated physical candidates. public sealed class SequentialLongitudinalOptimizer { - private const int MaximumEnvelopeIterations = 5; - private const double OrdinaryEnvelopeProbeLookaheadSteps = 1d; - private const double OrdinaryTerminalProbeFraction = 0.5d; + private const int MaximumAcceptedAnchorUpdates = 5; + private const int MaximumQpSolveCalls = 12; + private const double MinimumTrustRegionWidthMeters = 0.001d; + private const double ObjectiveAcceptanceRelativeTolerance = 1e-9d; + private const double HighPrecisionRetryTolerance = 1e-7d; + private const double StaticStartSeedBudgetFraction = 0.10d; + private static readonly TimeSpan MaximumStaticStartSeedBudget = TimeSpan.FromMilliseconds(250d); + private static readonly TimeSpan PublicationReserve = TimeSpan.FromMilliseconds(250d); + private static readonly double[] TrustRegionScales = { 1d, 0.5d, 0.25d, 0.125d }; private readonly IQpSolver _qpSolver; private readonly PathSpeedLimitBuilder _speedLimitBuilder; private readonly LongitudinalConstraintBuilder _constraintBuilder; private readonly LongitudinalSolutionValidator _solutionValidator; + private readonly LongitudinalEnvelopeTrustRegionBuilder _trustRegionBuilder = + new LongitudinalEnvelopeTrustRegionBuilder(); public SequentialLongitudinalOptimizer(IQpSolver qpSolver) : this(qpSolver, new PathSpeedLimitBuilder(), new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()), @@ -65,133 +73,418 @@ public sealed class SequentialLongitudinalOptimizer return Failed(speedStatus, speedFailure); var stopwatch = Stopwatch.StartNew(); - LongitudinalCandidate iterate; - LongitudinalCandidate lastStrictCandidate = null; - int remainingObjectiveIterations = iterationLimit; + var solveTrace = new LongitudinalSolveTrace(); + LongitudinalCandidate initialCandidate; + int projectionSolveCount = 0; if (input.PlanningScope == EmPlanningScope.FullDirectionSegment && input.Mode == EmLongitudinalMode.ExactStopAtBoundary) { if (!TryCreateInitialFeasibleCandidate(input, speedLimit, settings, totalBudget, convergenceTolerance, - iterationLimit, stopwatch, cancellationToken, out iterate, out int projectionSolveCount, - out EmPlanningStatus projectionStatus, out string projectionFailure)) + iterationLimit, stopwatch, solveTrace, cancellationToken, out initialCandidate, + out int usedProjectionSolveCount, out EmPlanningStatus projectionStatus, + out string projectionFailure)) { - return Failed(projectionStatus, projectionFailure); - } - lastStrictCandidate = CopyCandidate(iterate); - remainingObjectiveIterations -= projectionSolveCount; - if (remainingObjectiveIterations <= 0) - { - return new LongitudinalPlanningResult(EmPlanningStatus.SuccessWithFallback, lastStrictCandidate, - "The strict initial feasibility projection consumed the configured outer-iteration budget."); + string projectionTrace = solveTrace.Format(); + return Failed(projectionStatus, projectionFailure + + (string.IsNullOrEmpty(projectionTrace) ? string.Empty : ";solveTrace=" + projectionTrace)); } + projectionSolveCount = usedProjectionSolveCount; } else { - iterate = CreateInitialIterate(input, speedLimit); - if (!_solutionValidator.TryValidate(input, speedLimit, iterate, out lastStrictCandidate, out _)) - lastStrictCandidate = null; + LongitudinalCandidate seed = CreateInitialIterate(input, speedLimit); + if (!_solutionValidator.TryValidate(input, speedLimit, seed, out initialCandidate, + out EmPlanningStatus initializationStatus, out string initializationFailure)) + { + return Failed(initializationStatus, "No strictly validated longitudinal candidate was found. " + + initializationFailure); + } } - double[] warmStart = ToPrimal(iterate); - bool hasDynamicsConsistentInitialWarmStart = iterate.SatisfiesExactDiscreteDynamics(1e-12d); - string lastCandidateRejection = string.Empty; - bool hasPreviousObjective = false; - double previousObjective = 0d; - for (int iteration = 0; iteration < remainingObjectiveIterations; iteration++) + LongitudinalCandidate anchor = CopyCandidate(initialCandidate); + int stabilizationStart = GetStabilizationStart(input); + int qpSolveCount = projectionSolveCount; + int trustShrinkCount = 0; + int acceptedAnchorCount = 0; + int acceptedUpdateLimit = Math.Min(MaximumAcceptedAnchorUpdates, iterationLimit); + double finalTrustScale = 1d; + string lastRejection = string.Empty; + + while (acceptedAnchorCount < acceptedUpdateLimit && qpSolveCount < MaximumQpSolveCalls) { + bool promoted = false; + for (int scaleIndex = 0; scaleIndex < TrustRegionScales.Length; scaleIndex++) + { + double scale = TrustRegionScales[scaleIndex]; + finalTrustScale = scale; + if (cancellationToken.IsCancellationRequested || totalBudget - stopwatch.Elapsed <= TimeSpan.Zero) + { + return FinishFromAnchor(anchor, acceptedAnchorCount, qpSolveCount, trustShrinkCount, + finalTrustScale, cancellationToken.IsCancellationRequested, lastRejection, solveTrace); + } + if (!_trustRegionBuilder.TryBuild(speedLimit, anchor, input.KnotSchedule.ReferencePathS, + stabilizationStart, scale, MinimumTrustRegionWidthMeters, + out LongitudinalEnvelopeTrustRegion region, out string regionFailure)) + { + lastRejection = regionFailure; + break; + } + if (!_constraintBuilder.TryBuildTrusted(input, speedLimit, anchor, region, + convergenceTolerance, out QuadraticProgram problem, out string buildFailure)) + { + lastRejection = buildFailure; + break; + } + TrustedSolveAttempt attempt = SolveTrustedProblem(problem, input, speedLimit, anchor, settings, + totalBudget, stopwatch, convergenceTolerance, stabilizationStart, + MaximumQpSolveCalls - qpSolveCount, solveTrace, qpSolveCount, acceptedAnchorCount, + scale, acceptedAnchorCount > 0, cancellationToken); + qpSolveCount += attempt.SolveCount; + lastRejection = attempt.FailureReason; + if (attempt.Status == EmPlanningStatus.Cancelled) + { + return Failed(EmPlanningStatus.Cancelled, CreateRunDiagnostic(qpSolveCount, + trustShrinkCount, acceptedAnchorCount, finalTrustScale, lastRejection, solveTrace)); + } + if (attempt.Accepted) + { + anchor = CopyCandidate(attempt.Candidate); + acceptedAnchorCount++; + promoted = true; + break; + } + if (attempt.Status != EmPlanningStatus.SuccessWithFallback) + { + return FinishFromAnchor(anchor, acceptedAnchorCount, qpSolveCount, trustShrinkCount, + finalTrustScale, false, lastRejection, solveTrace); + } + if (scaleIndex + 1 < TrustRegionScales.Length) + { + double nextScale = TrustRegionScales[scaleIndex + 1]; + if (!region.CanShrinkTo(nextScale, MinimumTrustRegionWidthMeters, out string shrinkFailure)) + { + lastRejection = shrinkFailure; + break; + } + trustShrinkCount++; + } + } + if (!promoted) + break; + } + + return FinishFromAnchor(anchor, acceptedAnchorCount, qpSolveCount, trustShrinkCount, + finalTrustScale, cancellationToken.IsCancellationRequested, lastRejection, solveTrace); + } + + private TrustedSolveAttempt SolveTrustedProblem(QuadraticProgram problem, + LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate anchor, + QpSolverSettings settings, TimeSpan totalBudget, Stopwatch stopwatch, double strictTolerance, + int stabilizationStart, int remainingCallCount, LongitudinalSolveTrace solveTrace, + int solveOrdinalOffset, int anchorUpdateIndex, double trustScale, + bool optionalImprovement, CancellationToken cancellationToken) + { + int solveCount = 0; + double anchorObjective = EvaluateObjective(problem, ToPrimal(anchor)); + if (!IsFinite(anchorObjective)) + { + return new TrustedSolveAttempt(EmPlanningStatus.LongitudinalInfeasible, null, solveCount, false, + "The strict anchor objective is non-finite for the trusted QP."); + } + + IReadOnlyList warmStart = ToPrimal(anchor); + string lastFailure = string.Empty; + for (int attemptIndex = 0; attemptIndex < 2; attemptIndex++) + { + if (solveCount >= remainingCallCount) + { + return new TrustedSolveAttempt(EmPlanningStatus.SolverTimedOut, null, solveCount, false, + string.IsNullOrWhiteSpace(lastFailure) ? "The longitudinal QP solve-call cap was reached." : lastFailure); + } if (cancellationToken.IsCancellationRequested) - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled."); + { + return new TrustedSolveAttempt(EmPlanningStatus.Cancelled, null, solveCount, false, + "Longitudinal optimization was cancelled before the trusted QP solve."); + } TimeSpan remainingBudget = totalBudget - stopwatch.Elapsed; if (remainingBudget <= TimeSpan.Zero) { - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.SolverTimedOut, - "Longitudinal optimization exhausted its solve budget."); + return new TrustedSolveAttempt(EmPlanningStatus.SolverTimedOut, null, solveCount, false, + "Longitudinal optimization exhausted its shared solve budget."); } - if (!_constraintBuilder.TryBuild(input, speedLimit, iterate, out QuadraticProgram problem, out string buildFailure)) + bool hasOptionalSolveBudget = TryGetOptionalSolveBudget(remainingBudget, + out TimeSpan remainingAfterReserve); + if (optionalImprovement && !hasOptionalSolveBudget) { - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.LongitudinalInfeasible, - "Longitudinal constraints are infeasible: " + buildFailure); + return new TrustedSolveAttempt(EmPlanningStatus.SolverTimedOut, null, solveCount, false, + CreatePublicationReserveSkipDiagnostic(remainingAfterReserve)); + } + TimeSpan solveBudget = optionalImprovement ? remainingAfterReserve : remainingBudget; + + bool highPrecision = attemptIndex == 1; + double absoluteTolerance = highPrecision + ? Math.Min(settings.AbsoluteTolerance, HighPrecisionRetryTolerance) + : settings.AbsoluteTolerance; + double relativeTolerance = highPrecision + ? Math.Min(settings.AbsoluteTolerance, HighPrecisionRetryTolerance) + : settings.RelativeTolerance; + var solveStopwatch = Stopwatch.StartNew(); + QpSolveResult solved = _qpSolver.Solve(problem, + new QpSolverSettings(settings.MaximumIterations, absoluteTolerance, relativeTolerance, + solveBudget, settings.EnableWarmStart, settings.EnablePolishing, + settings.EnableNativeVerboseOutput), warmStart, cancellationToken); + solveStopwatch.Stop(); + solveCount++; + double candidateObjective = double.NaN; + void AddSolveTrace(string rejection) + { + solveTrace.Add(attemptIndex == 0 ? "normal" : "retry", + solveOrdinalOffset + solveCount, anchorUpdateIndex, trustScale, + solveBudget, remainingAfterReserve, solveStopwatch.Elapsed, solved, anchorObjective, + candidateObjective, rejection); } - QpSolveResult solved = _qpSolver.Solve(problem, - new QpSolverSettings(settings.MaximumIterations, settings.AbsoluteTolerance, settings.RelativeTolerance, - remainingBudget, settings.EnableWarmStart && (iteration > 0 || hasDynamicsConsistentInitialWarmStart), - settings.EnablePolishing, - settings.EnableNativeVerboseOutput), - warmStart, cancellationToken); if (cancellationToken.IsCancellationRequested) - return Failed(EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled after the QP solve."); - if (solved == null) - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Failed, "The longitudinal QP solver returned no result."); - if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations) { - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.SolverTimedOut, - "The longitudinal QP solver timed out (status=" + solved.NativeStatus + - ", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual + - ", dual=" + solved.DualResidual + "): " + solved.Diagnostic); + const string rejection = "Longitudinal optimization was cancelled after the trusted QP solve."; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.Cancelled, null, solveCount, false, + rejection); + } + if (solved == null) + { + const string rejection = "The longitudinal QP solver returned no result."; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.Failed, null, solveCount, false, + rejection); } if (solved.Status == QpSolveStatus.Cancelled) - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Cancelled, - "The longitudinal QP solver was cancelled: " + solved.Diagnostic); + { + string rejection = "The longitudinal QP solver was cancelled: " + solved.Diagnostic; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.Cancelled, null, solveCount, false, + rejection); + } + if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations) + { + string rejection = "The longitudinal QP solver timed out (status=" + solved.NativeStatus + + ", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual + + ", dual=" + solved.DualResidual + "): " + solved.Diagnostic; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.SolverTimedOut, null, solveCount, false, + rejection); + } if (solved.Status == QpSolveStatus.PrimalInfeasible || solved.Status == QpSolveStatus.DualInfeasible) { - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.LongitudinalInfeasible, - "The longitudinal QP solver reported infeasibility: " + solved.Diagnostic); + string rejection = "The preflight-feasible longitudinal QP solver reported infeasibility;" + + "solverNumericalAnomaly=true;status=" + solved.NativeStatus + ": " + solved.Diagnostic; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.LongitudinalInfeasible, null, solveCount, false, + rejection); } if (solved.Status == QpSolveStatus.SolverUnavailable) - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.SolverUnavailable, - "The longitudinal QP solver is unavailable: " + solved.Diagnostic); + { + string rejection = "The longitudinal QP solver is unavailable: " + solved.Diagnostic; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.SolverUnavailable, null, solveCount, false, + rejection); + } if (solved.Status != QpSolveStatus.Solved && solved.Status != QpSolveStatus.SolvedInaccurate) { - return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Failed, - "The longitudinal QP solver failed: " + solved.Diagnostic); + string rejection = "The longitudinal QP solver failed (status=" + solved.NativeStatus + "): " + + solved.Diagnostic; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.Failed, null, solveCount, false, + rejection); } - if (solved.Status == QpSolveStatus.SolvedInaccurate && !HasStrictResiduals(solved, convergenceTolerance)) + + if (!TryCreateCandidate(anchor.KnotTimes, solved.Primal, out LongitudinalCandidate candidate)) { - lastCandidateRejection = "SolvedInaccurate residuals exceed the strict acceptance tolerance" + + const string rejection = + "The solver primal does not match the ST variable layout or contains non-finite values."; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.SuccessWithFallback, null, solveCount, false, + rejection); + } + + bool accepted = true; + if (solved.Status == QpSolveStatus.SolvedInaccurate && !HasStrictResiduals(solved, strictTolerance)) + { + accepted = false; + lastFailure = "SolvedInaccurate residuals exceed the strict acceptance tolerance" + " (primal=" + solved.PrimalResidual + ", dual=" + solved.DualResidual + ")."; - if (TryCreateCandidate(iterate.KnotTimes, solved.Primal, out LongitudinalCandidate inaccurateCandidate)) - warmStart = ToPrimal(inaccurateCandidate); - continue; } - if (!TryCreateCandidate(iterate.KnotTimes, solved.Primal, out LongitudinalCandidate candidate)) + if (accepted && !TryFastValidateCandidate(problem, candidate, strictTolerance, + stabilizationStart, out lastFailure)) { - lastCandidateRejection = "The solver primal does not match the ST variable layout."; - continue; + accepted = false; } - if (!_solutionValidator.TryValidate(input, speedLimit, candidate, out LongitudinalCandidate validated, + LongitudinalCandidate validated = null; + if (accepted && !_solutionValidator.TryValidate(input, speedLimit, candidate, out validated, out string validationFailure)) { - string rejection = validationFailure + CreateEnvelopeDiagnostic(speedLimit, iterate, candidate, - iteration + 1); - lastCandidateRejection = string.IsNullOrEmpty(lastCandidateRejection) - ? rejection - : lastCandidateRejection + " | " + rejection; - if (TryCreateEnvelopeIterate(input, iterate, candidate, out LongitudinalCandidate nextIterate)) + accepted = false; + lastFailure = validationFailure; + } + if (accepted) + { + candidateObjective = EvaluateObjective(problem, ToPrimal(validated)); + if (!IsObjectiveAccepted(anchorObjective, candidateObjective)) { - iterate = nextIterate; - warmStart = ToPrimal(candidate); + accepted = false; + lastFailure = "The strictly valid candidate worsens the current trusted-QP objective" + + " (anchor=" + anchorObjective.ToString("R", CultureInfo.InvariantCulture) + + ", candidate=" + candidateObjective.ToString("R", CultureInfo.InvariantCulture) + ")."; } - continue; + } + if (accepted && cancellationToken.IsCancellationRequested) + { + const string rejection = "Longitudinal optimization was cancelled before strict-anchor promotion."; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.Cancelled, null, solveCount, false, + rejection); + } + if (accepted && totalBudget - stopwatch.Elapsed <= TimeSpan.Zero) + { + const string rejection = + "Longitudinal optimization exhausted its shared solve budget before strict-anchor promotion."; + AddSolveTrace(rejection); + return new TrustedSolveAttempt(EmPlanningStatus.SolverTimedOut, null, solveCount, false, + rejection); + } + if (accepted) + { + AddSolveTrace(string.Empty); + return new TrustedSolveAttempt(EmPlanningStatus.Success, validated, solveCount, true, string.Empty); + } + if (highPrecision) + { + AddSolveTrace(lastFailure); + return new TrustedSolveAttempt(EmPlanningStatus.SuccessWithFallback, null, solveCount, false, + lastFailure); } - double maximumChange = MaximumProgressOrSpeedChange(iterate, validated); - double relativeObjectiveImprovement = hasPreviousObjective - ? RelativeObjectiveImprovement(previousObjective, solved.Objective) - : double.PositiveInfinity; - lastStrictCandidate = CopyCandidate(validated); - iterate = validated; - warmStart = ToPrimal(validated); - previousObjective = solved.Objective; - hasPreviousObjective = true; - if (maximumChange <= convergenceTolerance && relativeObjectiveImprovement <= convergenceTolerance) - return new LongitudinalPlanningResult(EmPlanningStatus.Success, lastStrictCandidate, string.Empty); + AddSolveTrace(lastFailure); + warmStart = ToPrimal(candidate); } - return lastStrictCandidate == null - ? Failed(EmPlanningStatus.LongitudinalInfeasible, "No strictly validated longitudinal candidate was found. " + - lastCandidateRejection) - : new LongitudinalPlanningResult(EmPlanningStatus.Success, lastStrictCandidate, string.Empty); + return new TrustedSolveAttempt(EmPlanningStatus.SuccessWithFallback, null, solveCount, false, + lastFailure); + } + + private static double EvaluateObjective(QuadraticProgram problem, IReadOnlyList primal) + { + if (problem == null || primal == null || primal.Count != problem.VariableCount) + return double.NaN; + double objective = 0d; + SparseCscMatrix hessian = problem.UpperTriangularP; + for (int column = 0; column < hessian.ColumnCount; column++) + { + double columnValue = primal[column]; + if (!IsFinite(columnValue)) + return double.NaN; + for (int entry = hessian.ColumnPointers[column]; entry < hessian.ColumnPointers[column + 1]; entry++) + { + int row = hessian.RowIndices[entry]; + double term = hessian.Values[entry] * primal[row] * columnValue; + objective += row == column ? 0.5d * term : term; + if (!IsFinite(objective)) + return double.NaN; + } + objective += problem.LinearCost[column] * columnValue; + if (!IsFinite(objective)) + return double.NaN; + } + return objective; + } + + private static bool IsObjectiveAccepted(double anchorObjective, double candidateObjective) + { + if (!IsFinite(anchorObjective) || !IsFinite(candidateObjective)) + return false; + double tolerance = ObjectiveAcceptanceRelativeTolerance * Math.Max(1d, Math.Abs(anchorObjective)); + return candidateObjective <= anchorObjective + tolerance; + } + + private static int GetStabilizationStart(LongitudinalPlanningInput input) + { + if (input.Mode != EmLongitudinalMode.ExactStopAtBoundary) + return input.KnotSchedule.KnotTimes.Count; + if (input.PlanningScope == EmPlanningScope.FullDirectionSegment) + return input.KnotSchedule.TerminalHoldStartIndex; + return LongitudinalTerminalSchedule.GetStabilizationStartIndex(input.KnotSchedule.KnotTimes, + input.Configuration.Scheduling.OutputTimeStepSeconds); + } + + private static bool TryFastValidateCandidate(QuadraticProgram problem, LongitudinalCandidate candidate, + double strictTolerance, int stabilizationStart, out string failureReason) + { + failureReason = string.Empty; + var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count); + LongitudinalQpAuditResult audit; + try + { + audit = LongitudinalQpFeasibilityAudit.Evaluate(problem, candidate, strictTolerance, + layout, stabilizationStart); + } + catch (ArgumentException exception) + { + failureReason = "The candidate cannot be audited against the current trusted QP: " + exception.Message; + return false; + } + if (audit.IsFeasible) + return true; + failureReason = "The candidate violates the current trusted QP" + + ";row=" + audit.WorstRow + ";category=" + audit.Category + + ";residual=" + audit.MaximumResidual.ToString("R", CultureInfo.InvariantCulture) + audit.Unit + + ";tolerance=" + strictTolerance.ToString("R", CultureInfo.InvariantCulture); + return false; + } + + private static string CreateRunDiagnostic(int qpSolveCount, int trustShrinkCount, + int acceptedAnchorCount, double finalTrustScale, string lastRejection, + LongitudinalSolveTrace solveTrace) + { + return "qpSolves=" + qpSolveCount + + ",trustShrinks=" + trustShrinkCount + + ",acceptedAnchors=" + acceptedAnchorCount + + ",trustScale=" + finalTrustScale.ToString("R", CultureInfo.InvariantCulture) + + (string.IsNullOrWhiteSpace(lastRejection) ? string.Empty : ";lastRejection=" + lastRejection) + + (string.IsNullOrEmpty(solveTrace.Format()) ? string.Empty : ";solveTrace=" + solveTrace.Format()); + } + + internal static bool TryGetOptionalSolveBudget(TimeSpan remaining, out TimeSpan solveBudget) + { + solveBudget = remaining - PublicationReserve; + if (solveBudget <= TimeSpan.Zero) + { + solveBudget = TimeSpan.Zero; + return false; + } + return true; + } + + private static string CreatePublicationReserveSkipDiagnostic(TimeSpan remainingAfterReserve) + { + return "remainingAfterReserveMs=" + remainingAfterReserve.TotalMilliseconds.ToString( + "F3", CultureInfo.InvariantCulture) + + ";publicationReserveMs=" + PublicationReserve.TotalMilliseconds.ToString( + "F0", CultureInfo.InvariantCulture) + + ";optionalImprovement=skipped"; + } + + private static LongitudinalPlanningResult FinishFromAnchor(LongitudinalCandidate anchor, + int acceptedAnchorCount, int qpSolveCount, int trustShrinkCount, double finalTrustScale, + bool cancelled, string lastRejection, LongitudinalSolveTrace solveTrace) + { + string diagnostic = CreateRunDiagnostic(qpSolveCount, trustShrinkCount, acceptedAnchorCount, + finalTrustScale, lastRejection, solveTrace); + if (cancelled) + return Failed(EmPlanningStatus.Cancelled, diagnostic); + EmPlanningStatus status = acceptedAnchorCount > 0 + ? EmPlanningStatus.Success + : EmPlanningStatus.SuccessWithFallback; + return new LongitudinalPlanningResult(status, CopyCandidate(anchor), diagnostic); } private static bool TryCreateSettings(LongitudinalPlanningInput input, out QpSolverSettings settings, @@ -222,7 +515,7 @@ public sealed class SequentialLongitudinalOptimizer settings = new QpSolverSettings(solver.MaximumOsqpIterations, solver.AbsoluteTolerance, solver.RelativeTolerance, totalBudget, solver.WarmStart, solver.Polish, solver.NativeVerbose); convergenceTolerance = solver.StrictResidualTolerance; - iterationLimit = Math.Min(MaximumEnvelopeIterations, solver.MaximumOuterIterations); + iterationLimit = Math.Min(MaximumAcceptedAnchorUpdates, solver.MaximumOuterIterations); return true; } catch (ArgumentException exception) @@ -234,8 +527,9 @@ public sealed class SequentialLongitudinalOptimizer private bool TryCreateInitialFeasibleCandidate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, QpSolverSettings settings, TimeSpan totalBudget, double convergenceTolerance, int iterationLimit, - Stopwatch stopwatch, CancellationToken cancellationToken, out LongitudinalCandidate candidate, - out int projectionSolveCount, out EmPlanningStatus failureStatus, out string failureReason) + Stopwatch stopwatch, LongitudinalSolveTrace solveTrace, CancellationToken cancellationToken, + out LongitudinalCandidate candidate, out int projectionSolveCount, + out EmPlanningStatus failureStatus, out string failureReason) { candidate = null; projectionSolveCount = 0; @@ -248,8 +542,9 @@ public sealed class SequentialLongitudinalOptimizer input.Configuration.Validation.KinematicTolerance; bool staticStartSeedUsed = false; string staticStartSeedFailure = string.Empty; - if (staticStartEligible && TryCreateStaticStartSeed(input, speedLimit, out LongitudinalCandidate staticStartSeed, - out staticStartSeedFailure)) + TimeSpan staticStartSeedDeadline = stopwatch.Elapsed + GetStaticStartSeedBudget(totalBudget); + if (staticStartEligible && TryCreateStaticStartSeed(input, speedLimit, stopwatch, staticStartSeedDeadline, + out LongitudinalCandidate staticStartSeed, out staticStartSeedFailure)) { staticStartSeedUsed = true; candidate = staticStartSeed; @@ -289,60 +584,93 @@ public sealed class SequentialLongitudinalOptimizer double projectionTolerance = Math.Min(settings.AbsoluteTolerance, input.Configuration.Validation.KinematicTolerance * 0.1d); + double anchorObjective = double.NaN; + var solveStopwatch = Stopwatch.StartNew(); QpSolveResult solved = _qpSolver.Solve(problem, new QpSolverSettings(settings.MaximumIterations, projectionTolerance, projectionTolerance, remainingBudget, settings.EnableWarmStart && linearizationIterate.SatisfiesExactDiscreteDynamics(1e-12d), settings.EnablePolishing, settings.EnableNativeVerboseOutput), ToPrimal(linearizationIterate), cancellationToken); + solveStopwatch.Stop(); projectionSolveCount++; + int projectionCallOrdinal = projectionSolveCount; + double candidateObjective = double.NaN; + void AddProjectionTrace(string rejection) + { + TryGetOptionalSolveBudget(remainingBudget, out TimeSpan remainingAfterReserve); + solveTrace.Add("projection", projectionCallOrdinal, 0, 1d, remainingBudget, + remainingAfterReserve, solveStopwatch.Elapsed, solved, anchorObjective, + candidateObjective, rejection); + } if (cancellationToken.IsCancellationRequested) { + const string rejection = + "Initial full-direction feasibility projection was cancelled after the QP solve."; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.Cancelled; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection was cancelled after the QP solve."); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved == null) { + const string rejection = "The initial full-direction feasibility solver returned no result."; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.Failed; - failureReason = WithStaticSeedDiagnostic("The initial full-direction feasibility solver returned no result."); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations) { - failureStatus = EmPlanningStatus.SolverTimedOut; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection timed out (status=" + solved.NativeStatus + + string rejection = "Initial full-direction feasibility projection timed out (status=" + solved.NativeStatus + ", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual + ", dual=" + - solved.DualResidual + "): " + solved.Diagnostic); + solved.DualResidual + "): " + solved.Diagnostic; + AddProjectionTrace(rejection); + failureStatus = EmPlanningStatus.SolverTimedOut; + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved.Status == QpSolveStatus.Cancelled) { + string rejection = + "Initial full-direction feasibility projection was cancelled: " + solved.Diagnostic; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.Cancelled; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection was cancelled: " + solved.Diagnostic); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved.Status == QpSolveStatus.PrimalInfeasible || solved.Status == QpSolveStatus.DualInfeasible) { + string rejection = + "Initial full-direction feasibility projection is infeasible: " + solved.Diagnostic; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.LongitudinalInfeasible; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection is infeasible: " + solved.Diagnostic); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved.Status == QpSolveStatus.SolverUnavailable) { + string rejection = + "Initial full-direction feasibility solver is unavailable: " + solved.Diagnostic; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.SolverUnavailable; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility solver is unavailable: " + solved.Diagnostic); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved.Status != QpSolveStatus.Solved && solved.Status != QpSolveStatus.SolvedInaccurate) { + string rejection = "Initial full-direction feasibility solver failed: " + solved.Diagnostic; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.Failed; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility solver failed: " + solved.Diagnostic); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (!TryCreateCandidate(input.KnotSchedule.KnotTimes, solved.Primal, out LongitudinalCandidate projected)) { + const string rejection = + "Initial full-direction feasibility solver primal does not match the ST layout."; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.LongitudinalInfeasible; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility solver primal does not match the ST layout."); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } if (solved.Status == QpSolveStatus.Solved || HasStrictResiduals(solved, convergenceTolerance)) @@ -350,11 +678,13 @@ public sealed class SequentialLongitudinalOptimizer if (_solutionValidator.TryValidate(input, speedLimit, projected, out LongitudinalCandidate strict, out EmPlanningStatus validationStatus, out string validationFailure)) { + AddProjectionTrace(string.Empty); candidate = strict; return true; } if (validationStatus == EmPlanningStatus.NoProgress) { + AddProjectionTrace(validationFailure); failureStatus = validationStatus; failureReason = WithStaticSeedDiagnostic(validationFailure); return false; @@ -365,8 +695,11 @@ public sealed class SequentialLongitudinalOptimizer if (!TryCreateFeasibilityEnvelopeIterate(input, projected, out LongitudinalCandidate nextLinearization)) { + const string rejection = + "Initial full-direction feasibility candidate could not be relinearized against the PathS envelope."; + AddProjectionTrace(rejection); failureStatus = EmPlanningStatus.LongitudinalInfeasible; - failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility candidate could not be relinearized against the PathS envelope."); + failureReason = WithStaticSeedDiagnostic(rejection); return false; } linearizationIterate = nextLinearization; @@ -374,6 +707,7 @@ public sealed class SequentialLongitudinalOptimizer lastRejection = "Initial feasibility projection residuals exceed the strict acceptance tolerance."; else if (string.IsNullOrEmpty(lastRejection)) lastRejection = "Initial feasibility projection violated the strict physical validator."; + AddProjectionTrace(lastRejection); } failureStatus = EmPlanningStatus.LongitudinalInfeasible; failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection exhausted the configured outer iterations. " + @@ -503,7 +837,7 @@ public sealed class SequentialLongitudinalOptimizer } private bool TryCreateStaticStartSeed(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, - out LongitudinalCandidate candidate, out string failureReason) + Stopwatch stopwatch, TimeSpan deadline, out LongitudinalCandidate candidate, out string failureReason) { candidate = null; failureReason = "unknown"; @@ -515,9 +849,11 @@ public sealed class SequentialLongitudinalOptimizer } IReadOnlyList times = input.KnotSchedule.KnotTimes; - if (TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit, out candidate)) + if (TryCreateStaticStartScurveSeed(input, times, stabilizationStart, speedLimit, stopwatch, deadline, out candidate)) return true; - failureReason = "exactJerkSeed=failed"; + if (TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit, stopwatch, deadline, out candidate)) + return true; + failureReason = "scurveSeed=failed; exactJerkSeed=failed"; double firstDuration = times[1] - times[0]; double secondDuration = times[2] - times[1]; LongitudinalConfiguration configuration = input.Configuration.Longitudinal; @@ -526,6 +862,8 @@ public sealed class SequentialLongitudinalOptimizer configuration.MaximumJerkMetersPerSecondCubed * secondDuration / firstDuration)); for (int sample = -256; sample <= 256; sample++) { + if (HasReachedDeadline(stopwatch, deadline)) + return false; if (sample == 0) continue; double firstJerk = maximumFirstJerk * sample / 256d; @@ -545,18 +883,13 @@ public sealed class SequentialLongitudinalOptimizer return true; } } - if (TryCreateStaticStartScurveSeed(input, times, stabilizationStart, speedLimit, out candidate)) - { - failureReason = string.Empty; - return true; - } - failureReason = "exactJerkSeed=failed; sampledSeeds=failed; scurveSeeds=failed"; + failureReason = "scurveSeed=failed; exactJerkSeed=failed; sampledSeeds=failed"; return false; } private bool TryCreateStaticStartScurveSeed(LongitudinalPlanningInput input, IReadOnlyList times, int stabilizationStart, PathSpeedLimit speedLimit, - out LongitudinalCandidate candidate) + Stopwatch stopwatch, TimeSpan deadline, out LongitudinalCandidate candidate) { candidate = null; int intervalCount = stabilizationStart; @@ -569,6 +902,8 @@ public sealed class SequentialLongitudinalOptimizer { for (int plateau = 0; 4 * ramp + 2 * plateau <= intervalCount; plateau++) { + if (HasReachedDeadline(stopwatch, deadline)) + return false; int cruise = intervalCount - 4 * ramp - 2 * plateau; var basisAccel = new double[intervalCount]; var basisBrake = new double[intervalCount]; @@ -688,6 +1023,14 @@ public sealed class SequentialLongitudinalOptimizer private bool TryCreateExactJerkSeed(LongitudinalPlanningInput input, IReadOnlyList times, int stabilizationStart, PathSpeedLimit speedLimit, out LongitudinalCandidate candidate) + { + return TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit, Stopwatch.StartNew(), + TimeSpan.MaxValue, out candidate); + } + + private bool TryCreateExactJerkSeed(LongitudinalPlanningInput input, IReadOnlyList times, + int stabilizationStart, PathSpeedLimit speedLimit, Stopwatch stopwatch, TimeSpan deadline, + out LongitudinalCandidate candidate) { candidate = null; int intervalCount = stabilizationStart; @@ -747,6 +1090,8 @@ public sealed class SequentialLongitudinalOptimizer { for (int basisIndex = 0; basisIndex < intervalCount; basisIndex++) { + if (HasReachedDeadline(stopwatch, deadline)) + return false; double[] direction = CreateEndpointNullspaceDirection(influence, gram, basisIndex); if (direction == null) continue; @@ -755,6 +1100,8 @@ public sealed class SequentialLongitudinalOptimizer double bestViolation = currentViolation; for (int sample = -256; sample <= 256; sample++) { + if (HasReachedDeadline(stopwatch, deadline)) + return false; double scale = maximumJerk * sample / 256d; var probeJerk = new double[intervalCount]; for (int interval = 0; interval < intervalCount; interval++) @@ -776,6 +1123,18 @@ public sealed class SequentialLongitudinalOptimizer return false; } + private static TimeSpan GetStaticStartSeedBudget(TimeSpan totalBudget) + { + double milliseconds = Math.Min(MaximumStaticStartSeedBudget.TotalMilliseconds, + Math.Max(1d, totalBudget.TotalMilliseconds * StaticStartSeedBudgetFraction)); + return TimeSpan.FromMilliseconds(milliseconds); + } + + private static bool HasReachedDeadline(Stopwatch stopwatch, TimeSpan deadline) + { + return stopwatch.Elapsed >= deadline; + } + private bool TryValidateExactSeed(LongitudinalPlanningInput input, IReadOnlyList times, int stabilizationStart, PathSpeedLimit speedLimit, IReadOnlyList jerk, out LongitudinalCandidate candidate) @@ -1065,72 +1424,6 @@ public sealed class SequentialLongitudinalOptimizer return primal; } - private static bool TryCreateEnvelopeIterate(LongitudinalPlanningInput input, LongitudinalCandidate previous, - LongitudinalCandidate candidate, out LongitudinalCandidate nextIterate) - { - nextIterate = null; - if (candidate.S.Count != previous.S.Count) - return false; - int stabilizationStart = input.Mode != EmLongitudinalMode.ExactStopAtBoundary - ? candidate.S.Count - : input.PlanningScope == EmPlanningScope.FullDirectionSegment - ? input.KnotSchedule.TerminalHoldStartIndex - : LongitudinalTerminalSchedule.GetStabilizationStartIndex(candidate.KnotTimes, - input.Configuration.Scheduling.OutputTimeStepSeconds); - var candidateProgressSamples = new double[candidate.S.Count]; - double priorProgress = double.NegativeInfinity; - double priorPreviousProgress = double.NegativeInfinity; - for (int index = 0; index < candidate.S.Count; index++) - { - double candidateProgress = candidate.S[index]; - double previousProgress = previous.S[index]; - if (!IsFinite(previousProgress) || previousProgress < 0d || previousProgress > input.PathUpperBoundS || - previousProgress < priorPreviousProgress) - { - return false; - } - if (!IsFinite(candidateProgress)) - { - candidateProgress = previousProgress; - } - candidateProgress = Math.Max(0d, Math.Min(input.PathUpperBoundS, candidateProgress)); - if (input.Mode == EmLongitudinalMode.ExactStopAtBoundary && index >= stabilizationStart) - candidateProgress = input.StopBoundaryPathS; - candidateProgress = Math.Max(priorProgress, candidateProgress); - candidateProgressSamples[index] = candidateProgress; - priorProgress = candidateProgress; - priorPreviousProgress = previousProgress; - } - var progress = new double[candidate.S.Count]; - double previousNextProgress = 0d; - for (int index = 0; index < progress.Length; index++) - { - double candidateProgress = candidateProgressSamples[index]; - if (index == 0 || index == progress.Length - 1 || (input.Mode == EmLongitudinalMode.ExactStopAtBoundary && - index >= stabilizationStart) || candidateProgress >= input.PathUpperBoundS) - { - progress[index] = candidateProgress; - } - else - { - double timeStep = candidate.KnotTimes[index + 1] - candidate.KnotTimes[index]; - double iterationAdvance = Math.Max(0d, candidateProgress - previous.S[index]); - double candidateSpeed = IsFinite(candidate.U[index]) ? Math.Max(0d, candidate.U[index]) : 0d; - double lookaheadAdvance = IsFinite(candidate.U[index]) - ? OrdinaryEnvelopeProbeLookaheadSteps * candidateSpeed * timeStep - : 0d; - double terminalLimitedAdvance = OrdinaryTerminalProbeFraction * - (input.PathUpperBoundS - candidateProgress); - double advance = Math.Min(Math.Max(iterationAdvance, lookaheadAdvance), terminalLimitedAdvance); - progress[index] = candidateProgress + advance; - } - progress[index] = Math.Max(previousNextProgress, progress[index]); - previousNextProgress = progress[index]; - } - nextIterate = new LongitudinalCandidate(candidate.KnotTimes, progress, previous.U, previous.A, previous.J); - return true; - } - private static bool TryCreateFeasibilityEnvelopeIterate(LongitudinalPlanningInput input, LongitudinalCandidate candidate, out LongitudinalCandidate nextIterate) { @@ -1161,52 +1454,27 @@ public sealed class SequentialLongitudinalOptimizer result.PrimalResidual <= tolerance && result.DualResidual <= tolerance; } - private static string CreateEnvelopeDiagnostic(PathSpeedLimit speedLimit, LongitudinalCandidate iterate, - LongitudinalCandidate candidate, int iteration) + private readonly struct TrustedSolveAttempt { - int worstIndex = -1; - double worstExcess = double.NegativeInfinity; - for (int index = 0; index < candidate.S.Count; index++) + internal TrustedSolveAttempt(EmPlanningStatus status, LongitudinalCandidate candidate, + int solveCount, bool accepted, string failureReason) { - if (!IsFinite(candidate.S[index]) || !IsFinite(candidate.U[index])) - continue; - double candidateProgress = Math.Max(0d, Math.Min(speedLimit.PathUpperBoundS, candidate.S[index])); - double limit = speedLimit.MaximumSpeedAt(candidateProgress); - double excess = candidate.U[index] - limit; - if (excess > worstExcess) - { - worstExcess = excess; - worstIndex = index; - } + Status = status; + Candidate = candidate; + SolveCount = solveCount; + Accepted = accepted; + FailureReason = failureReason ?? string.Empty; } - if (worstIndex < 0) - return ""; - return " Envelope iteration " + iteration + " used PathS=" + iterate.S[worstIndex] + - " and produced PathS=" + candidate.S[worstIndex] + " at its largest speed-envelope excess."; - } - private static double MaximumProgressOrSpeedChange(LongitudinalCandidate previous, LongitudinalCandidate current) - { - double maximum = 0d; - for (int index = 0; index < previous.S.Count; index++) - { - maximum = Math.Max(maximum, Math.Abs(current.S[index] - previous.S[index])); - maximum = Math.Max(maximum, Math.Abs(current.U[index] - previous.U[index])); - } - return maximum; - } + internal EmPlanningStatus Status { get; } - private static double RelativeObjectiveImprovement(double previous, double current) - { - return Math.Abs(previous - current) / Math.Max(1d, Math.Abs(previous)); - } + internal LongitudinalCandidate Candidate { get; } - private static LongitudinalPlanningResult FallbackOrFailure(LongitudinalCandidate candidate, - EmPlanningStatus failureStatus, string failureReason) - { - return failureStatus == EmPlanningStatus.Cancelled || candidate == null - ? Failed(failureStatus, failureReason) - : new LongitudinalPlanningResult(EmPlanningStatus.SuccessWithFallback, candidate, failureReason); + internal int SolveCount { get; } + + internal bool Accepted { get; } + + internal string FailureReason { get; } } private static LongitudinalPlanningResult Failed(EmPlanningStatus status, string reason) diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/IQpSolver.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/IQpSolver.cs index e160826..fffff78 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/IQpSolver.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/IQpSolver.cs @@ -3,6 +3,9 @@ using System.Threading; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 求解器中立的二次规划边界。实现必须将数值状态转换为 ,而不能把原生求解器对象泄漏给规划层。 +/// public interface IQpSolver { QpSolveResult Solve( diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveResult.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveResult.cs index 6dda032..457d328 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveResult.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveResult.cs @@ -5,6 +5,9 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// QP 求解后的数值和诊断快照。调用方必须结合状态判断解向量是否可用,不能将失败输出当作候选轨迹。 +/// public sealed class QpSolveResult { public QpSolveResult( diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QuadraticProgram.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QuadraticProgram.cs index 1b391ee..3e4b77d 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QuadraticProgram.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QuadraticProgram.cs @@ -5,6 +5,9 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 供横向或纵向规划器提交给 的二次规划快照;矩阵维度与变量布局必须一致。 +/// public sealed class QuadraticProgram { public const double MaximumFiniteBound = 1e30d; diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/DirectionSegmentView.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/DirectionSegmentView.cs index a9bbe5b..dc4d9dc 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/DirectionSegmentView.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/DirectionSegmentView.cs @@ -6,8 +6,16 @@ using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 平滑参考路径中单一前进或倒车方向段的只读视图;EM 规划和投影不得跨越该段的精确换向边界。 +/// public sealed class DirectionSegmentView { + /// + /// 创建一个已重基到局部 S=0 的方向段只读视图。 + /// 参数:points 的 ArcLength 为 m 且方向必须一致,边界与 sourceStartArcLength 为 m;首点与起边界必须位于局部 S=0,终边界须在 1e-12 m 内匹配末点。 + /// 返回:防御性复制的不可变段;索引、方向、边界或弧长不一致时引发异常。 + /// public DirectionSegmentView( int segmentIndex, TravelDirection direction, @@ -48,17 +56,38 @@ public sealed class DirectionSegmentView SourceStartArcLength = sourceStartArcLength; } + /// + /// 在原始平滑路径方向段序列中的零基索引。 + /// public int SegmentIndex { get; } + /// + /// 全段唯一的行驶方向;投影和优化不得在本视图内切换方向。 + /// public TravelDirection Direction { get; } + /// + /// 局部弧长已重基、按 S(m)非递减排列的平滑参考点只读副本。 + /// public IReadOnlyList Points { get; } + /// + /// 局部 S=0 的起始边界,含换向离开语义(若有)。 + /// public ReferenceBoundary StartBoundary { get; } + /// + /// 局部段末端的边界,含目标或换向接近语义(若有)。 + /// public ReferenceBoundary EndBoundary { get; } + /// + /// 本段局部 S=0 在原始平滑路径中的累计弧长,单位 m。 + /// public double SourceStartArcLength { get; } + /// + /// 本段从局部 S=0 到终边界的长度,单位 m。 + /// public double LengthMeters { get { return EndBoundary.SegmentLocalS; } } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs index 8415661..39bfb63 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs @@ -4,9 +4,16 @@ using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; -/// Reference-distance terminal chosen before LS without crossing the current direction segment. +/// +/// 在纵横向优化前选定的参考距离终端,保证不会跨越当前方向段。 +/// 所有参考站量以当前段局部 S(m)表示;边界类型和纵向模式明确滚动延续、接近停车或精确停车语义。 +/// public sealed class PlanningHorizonSelection { + /// + /// 创建仅供本程序集发布的窗口与停车边界选择。 + /// 参数:两个参考站均为局部 S(m),boundary/terminal/mode 为已判定枚举;hasStopBoundary 表示终端是否受真实停车边界约束。 + /// internal PlanningHorizonSelection(double windowEndReferenceS, EmBoundaryType windowEndBoundaryType, EmTerminalType terminalType, EmLongitudinalMode longitudinalMode, double stopBoundaryReferenceS, bool hasStopBoundary) @@ -19,25 +26,58 @@ public sealed class PlanningHorizonSelection HasStopBoundary = hasStopBoundary; } + /// + /// 本次优化窗口末端的局部参考弧长 S,单位 m。 + /// public double WindowEndReferenceS { get; } + /// + /// 规划输出的终端参考弧长 S,当前与窗口末端相同,单位 m。 + /// public double TerminalReferenceS => WindowEndReferenceS; + /// + /// 窗口末端的边界语义;中途截断时为滚动安全停车边界。 + /// public EmBoundaryType WindowEndBoundaryType { get; } + /// + /// 供终端约束使用的目标、换向或滚动终端类型。 + /// public EmTerminalType TerminalType { get; } + /// + /// 纵向候选应滚动延续、接近停车边界还是在边界精确停车的模式。 + /// public EmLongitudinalMode LongitudinalMode { get; } + /// + /// 实际目标或换向停车边界的局部参考弧长 S,单位 m;无此边界时仍保存段末。 + /// public double StopBoundaryReferenceS { get; } + /// + /// 是否存在必须在当前方向段内处理的目标或换向停车边界。 + /// public bool HasStopBoundary { get; } } +/// +/// 根据段边界、初始纵向状态和调度窗口选择 EM 优化地平线。 +/// 输入速度为 m/s、加速度为 m/s²、距离为 m;无效输入、越界初态或不足以停车的段会以明确状态拒绝。 +/// public sealed class PlanningHorizonSelector { + /// + /// 段终点、初态限值和停车距离比较使用的局部 S/距离容差,单位 m。 + /// private const double BoundaryTolerance = 1e-8d; + /// + /// 为当前方向段选择不跨界的参考距离窗口和纵向终端模式。 + /// 参数:currentSegmentReferenceS 为 m,初速为 m/s,初加速度为 m/s²,configuration 提供 m、s 制限值;planningScope 必须为定义的范围枚举。 + /// 返回:成功时给出 selection;配置/初态非法返回 InvalidInput,停车距离越段返回 StoppingDistanceInsufficient,非法全段终边界返回 InvalidReferencePath。 + /// public EmPlanningStatus Select(DirectionSegmentView segment, double currentSegmentReferenceS, double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared, EmPlanningScope planningScope, EmPlannerConfiguration configuration, out PlanningHorizonSelection selection, @@ -138,11 +178,19 @@ public sealed class PlanningHorizonSelector return EmPlanningStatus.Success; } + /// + /// 判断边界是否要求车辆在当前段内停止。 + /// 参数:boundaryType 为边界枚举;返回:Goal 或 GearSwitchApproach 时为 true,其余边界允许滚动延续。 + /// private static bool IsStopBoundary(EmBoundaryType boundaryType) { return boundaryType == EmBoundaryType.Goal || boundaryType == EmBoundaryType.GearSwitchApproach; } + /// + /// 将参考边界语义映射为纵向终端类型。 + /// 参数:boundaryType 为边界枚举;返回:目标、任一换向边界分别映射到 Goal、GearSwitch,其余映射为 RollingSafetyStop。 + /// private static EmTerminalType ToTerminalType(EmBoundaryType boundaryType) { return boundaryType == EmBoundaryType.Goal @@ -152,11 +200,19 @@ public sealed class PlanningHorizonSelector : EmTerminalType.RollingSafetyStop; } + /// + /// 判断配置中的正量是否可用于时间、距离或动力学计算。 + /// 参数:value 为对应单位的标量;返回:仅有限且严格大于零时为 true。 + /// private static bool IsPositiveFinite(double value) { return IsFinite(value) && value > 0d; } + /// + /// 判断规划输入是否为有限实数。 + /// 参数:value 为任意标量;返回:NaN 与无穷均返回 false。 + /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceBoundary.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceBoundary.cs index 12f4268..67868c5 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceBoundary.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceBoundary.cs @@ -2,8 +2,16 @@ using System; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 方向段的不可变参考边界标识,携带局部与源路径弧长以及边界语义。 +/// 两个弧长均以 m 计;相等性仅代表同一段、同一局部位置和同一类型,不以源弧长参与判等。 +/// public sealed class ReferenceBoundary : IEquatable { + /// + /// 创建已验证的参考边界。 + /// 参数:segmentIndex 必须非负,segmentLocalS 与 sourceArcLength 为非负有限 m 制弧长,boundaryType 必须是已定义枚举;非法输入会引发异常。 + /// public ReferenceBoundary(int segmentIndex, double segmentLocalS, EmBoundaryType boundaryType, double sourceArcLength) { if (segmentIndex < 0) @@ -21,22 +29,46 @@ public sealed class ReferenceBoundary : IEquatable SourceArcLength = sourceArcLength; } + /// + /// 边界所属方向段在路径段序列中的零基索引。 + /// public int SegmentIndex { get; } + /// + /// 边界相对该方向段起点的局部参考弧长 S,单位 m。 + /// public double SegmentLocalS { get; } + /// + /// 边界的目标、换向或普通边界语义。 + /// public EmBoundaryType BoundaryType { get; } + /// + /// 边界在未重基原始平滑路径中的累计弧长,单位 m。 + /// public double SourceArcLength { get; } + /// + /// 比较两个边界在优化语义上是否相同。 + /// 参数:other 可为 null;返回:仅段索引、局部 S(精确 double 比较)和边界类型都相同时为 true,源弧长不参与比较。 + /// public bool Equals(ReferenceBoundary other) { return other != null && SegmentIndex == other.SegmentIndex && SegmentLocalS.Equals(other.SegmentLocalS) && BoundaryType == other.BoundaryType; } + /// + /// 比较任意对象是否为同一参考边界。 + /// 参数:obj 可为空或非边界对象;返回:仅可转换为 且满足强类型相等性时为 true。 + /// public override bool Equals(object obj) { return Equals(obj as ReferenceBoundary); } + /// + /// 计算与边界相等性一致的哈希值。 + /// 返回:由段索引、局部 S 与边界类型组成的哈希;源弧长不参与以保持与 一致。 + /// public override int GetHashCode() { unchecked diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceHorizonSlicer.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceHorizonSlicer.cs index cc55cc9..4820911 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceHorizonSlicer.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferenceHorizonSlicer.cs @@ -6,8 +6,15 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 当前方向段内供一次 EM 优化使用的参考路径切片,保留全局参考站与切片局部弧长的对应关系。 +/// public sealed class ReferenceHorizonSlice { + /// + /// 创建一个以当前段起点为基准的参考地平线切片。 + /// 参数:points 为局部 S(m)有序的参考点,terminalBoundary 必须属于 segment;返回:points 的防御性只读副本,空输入或边界不匹配会被拒绝。 + /// public ReferenceHorizonSlice(DirectionSegmentView segment, IReadOnlyList points, ReferenceBoundary terminalBoundary) { @@ -25,17 +32,38 @@ public sealed class ReferenceHorizonSlice TerminalBoundary = terminalBoundary; } + /// + /// 切片所属且不会跨越的单一方向段。 + /// public DirectionSegmentView Segment { get; } + /// + /// 从段起点至精确终端点的参考点只读副本,局部 ArcLength 单位为 m。 + /// public IReadOnlyList Points { get; } + /// + /// 切片末端的精确参考边界;中途截断时具有滚动安全停车语义。 + /// public ReferenceBoundary TerminalBoundary { get; } } +/// +/// 按局部参考距离截取单一方向段,并在请求终端精确插值参考点。 +/// 切片只包含本段坐标;S、原始弧长与 X/Y 使用 m,航向使用 rad,终端附近以 1e-12 m 判定相等。 +/// public static class ReferenceHorizonSlicer { + /// + /// 端点复制、精确节点匹配和原段终点判定使用的局部 S 容差,单位 m。 + /// private const double Epsilon = 1e-12d; + /// + /// 将方向段截取到请求的局部终端 S,并追加该终端的精确点。 + /// 参数:requestedEndSegmentLocalS 为非负有限 m 制局部 S;超过段长的请求夹紧到段末。 + /// 返回:段末保留原边界,中途截断创建 RollingSafetyStop 边界;空段或非法 S 会引发异常。 + /// public static ReferenceHorizonSlice Slice(DirectionSegmentView segment, double requestedEndSegmentLocalS) { if (segment == null) @@ -60,6 +88,10 @@ public static class ReferenceHorizonSlicer return new ReferenceHorizonSlice(segment, points, terminal); } + /// + /// 取得给定局部 S 的精确已有点或其相邻点之间的插值点。 + /// 参数:terminalS 为已夹紧的 m 制局部 S;返回:与节点相差不超过 1e-12 m 时复用节点,否则在正跨度内线性插值。 + /// private static SmoothedPathPoint GetExactTerminalPoint(DirectionSegmentView segment, double terminalS) { for (int index = 0; index < segment.Points.Count; index++) @@ -76,6 +108,10 @@ public static class ReferenceHorizonSlicer return segment.Points[segment.Points.Count - 1]; } + /// + /// 在线性弧长区间内插值一个终端平滑路径点。 + /// 参数:lower/upper 的 ArcLength 为 m,localS 为其间的 m 制位置;返回:位置、展开航向、曲率和净空线性插值,标记为 Interpolated。 + /// private static SmoothedPathPoint Interpolate(SmoothedPathPoint lower, SmoothedPathPoint upper, double localS) { double interval = upper.ArcLength - lower.ArcLength; @@ -98,6 +134,10 @@ public static class ReferenceHorizonSlicer SmoothedPathPointSource.Interpolated); } + /// + /// 计算同单位标量的线性插值。 + /// 参数:lower、upper 为端点,fraction 为无单位比例;返回:未夹紧的线性结果。 + /// private static double Interpolate(double lower, double upper, double fraction) { return lower + (upper - lower) * fraction; diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferencePathSegmenter.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferencePathSegmenter.cs index 7fcc3a5..63e5c53 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferencePathSegmenter.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ReferencePathSegmenter.cs @@ -5,8 +5,16 @@ using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +/// +/// 将已发布的平滑参考路径拆分为不会跨越换向点的局部方向段视图。 +/// 每个输出段以局部 S=0 重基(m),同时保留原始累计弧长(m)及目标/换向边界语义;索引覆盖或弧长不连续会被拒绝。 +/// public static class ReferencePathSegmenter { + /// + /// 从已验证的平滑路径及其方向分段创建局部方向段视图。 + /// 参数:referencePath 必须含完整 Path 和 Segments,路径弧长为有限 m 制量;返回:按源段索引排列的只读视图,任何漏覆盖、方向或严格递增弧长错误均引发异常。 + /// public static IReadOnlyList Create(PathSmoothingResult referencePath) { if (referencePath == null || referencePath.Path == null || referencePath.Segments == null || @@ -65,6 +73,10 @@ public static class ReferencePathSegmenter return new ReadOnlyCollection(result); } + /// + /// 复制一个源参考点并将其弧长重基到当前方向段局部坐标。 + /// 参数:source 保存世界 X/Y(m)、航向(rad)和曲率量,localS 为非负段局部 m 制弧长;返回:除 ArcLength 外保持源点几何和来源不变的副本。 + /// internal static SmoothedPathPoint CloneAtLocalS(SmoothedPathPoint source, double localS) { return new SmoothedPathPoint( diff --git a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md b/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md deleted file mode 100644 index 9ee4f13..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md +++ /dev/null @@ -1,726 +0,0 @@ -# Hybrid A* 粗路径规划实施计划 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。 - -**目标:** 在 ClumsyPilot `netstandard2.0` 项目中交付可复用的静态规划地图和 Hybrid A* 粗路径规划模块,通过一次 `CoarsePathPlanningService.Plan(job)` 完成建图、快照复用、粗路径搜索和可选调试发布。 - -**架构:** `Map` 模块统一接收人工障碍、TwoLeg 及未来障碍来源,生成只含外部障碍物的不可变 `PlanningGridMap`;`HybridAStarPlanner` 只消费该快照并完成保守碰撞检查与搜索;`CoarsePathPlanningService` 负责编排。核心逻辑不读取传感器、UI 或系统时间,Clumsy `MovementTest` 和 PNG 导出只作为旁路适配器。 - -**技术栈:** C# 10、.NET Standard 2.0、PowerShell 反射契约测试、Clumsy `MovementTest`、StbImageWriteSharp 1.16.7。 - -**设计依据:** `docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md` - -## 全局约束 - -- 所有命令均从仓库根目录执行;项目文件为 `ClumsyPilot/ClumsyPilot.csproj`。 -- 目标框架保持 `netstandard2.0`,不得直接使用 `PriorityQueue`、`Math.Clamp`、`double.IsFinite` 或依赖 `record/init` 的实现。 -- 地图边界、人工障碍和 TwoLeg 快照使用世界坐标 mm;规划内部统一使用 m、rad、1/m。 -- 地图只保存外部障碍物,不写入 AMR 自身足迹,不在地图侧添加车辆安全膨胀。 -- 安全余量只在碰撞检查时扩大车体矩形,防止地图膨胀与车体膨胀重复计算。 -- `ResolutionMm` 必须在 20~200 mm;地图最多 4,000,000 格,分配前使用 `checked` 检查。 -- 地图坐标采用 `[XMin, XMax) × [YMin, YMax)`;地图外始终按占据处理。 -- `PrimitiveLengthMeters=0.50` 表示原语最大长度;`IntegrationStepMeters=0.05` 表示积分最大步长。 -- 碰撞采样中心位移不得超过 `min(0.025 m, Map.ResolutionMeters / 2)`。 -- 默认终点容差为 0.15 m 和 5°。每个原语在内部积分点逐点检查目标,第一次满足条件即截断为终点候选。 -- 生成终点候选时不能立即成功;候选必须进入 Open List,作为当前最佳有效条目出队时才成功。 -- 第一版只支持恒曲率前进、倒车及原语边界换向;不支持蟹行、横移、原地旋转、Reeds-Shepp 精确连接、平滑、速度规划或底盘控制。 -- PowerShell 脚本首行设置 `$ErrorActionPreference = 'Stop'`,统一加载 `bin/Debug/netstandard2.0/ClumsyPilot.dll`。 -- 用户已明确不需要 Git 自检。本计划不包含 `git diff`、`git add`、`git commit` 等步骤;版本管理由用户另行处理。 - -## P0 与 P1 完成定义 - -| 阶段 | 必须完成的结果 | 阶段出口 | -| --- | --- | --- | -| P0-MAP:当前首要工作 | Map 文件结构、统一障碍来源、TwoLeg 投影、栅格化、距离场、快照缓存、PNG 迁移、自动测试和 `MovementTest.MapTest` 实际调用 | Map 独立构建成功;3 个 Map 脚本通过;Clumsy MapTest 只通过 `PlanningMapFactory.Create` 完成建图与显示 | -| P0-PLAN:Map Gate 之后 | CoarsePath 契约、保守碰撞、原语截断、Open List、Hybrid A*、输出校验和一次调用门面 | 6 个非 UI P0 脚本全部通过;固定场景可由 `CoarsePathPlanningService` 返回经最终复核的粗路径 | -| P1:集成与优化 | CoarsePath MovementTest、性能基准、旧地图退役和 README | 7 个功能脚本及 Release 基准通过;UI 入口只做调试展示;旧地图类型不再成为规划运行时入口 | - -执行顺序固定为“P0-MAP 独立闭环 → P0-PLAN → P1”。除了 Map 自己的 `MovementTest.MapTest` 和 PNG 调试辅助外,在 Map Gate 通过前不编写粗路径搜索或 CoarsePath UI,避免旧 `MovementTest.Trapmaptest.cs` 的传感器和渲染依赖进入核心模块。 - -## 当前首要里程碑:P0-MAP 独立闭环 - -当前阶段只处理 `Utils` 中被 Map 使用的无状态工具、完整 `Map` 目录、Map 自动化测试和 Map 实际调用。不得提前创建 `CoarsePath/Search`、`CoarsePath/Vehicle` 或 `CoarsePath/Output` 运行时代码。 - -### Map 实施顺序 - -1. 建立 `Map/Core`、`Map/Obstacles`、`Map/Sources`、`Map/Planning`、`Map/Test/Visualization` 目录和命名空间。 -2. 完成 `MapBoundsMm`、连续行优先 `EnvironmentGridMap`、圆/矩形 DTO 和唯一栅格化器。 -3. 完成统一 `IMapObstacleSource`、人工来源、TwoLeg 快照来源和事务式 `EnvironmentMapBuilder`。 -4. 完成 `PlanningGridMap`、精确 EDT、保守距离场和 mm→m 适配。 -5. 完成 `PlanningMapFactory`、容量 4 的两级快照缓存和地图变化判断。 -6. 将旧 `TrapMapImageExporter.cs` 能力拆到 `Map/Test/Visualization`,只消费只读快照。 -7. 完成三个 Map PowerShell 脚本和 `MovementTest.MapTest`,用真实入口验证建图、复用和显示。 - -对应详细任务的执行次序为:`Task 1 → Task 3 → Task 4 → Task 5 → Task 6 → Task 13 → Task 14 的 MapTest 部分 → Map Gate`。`Task 2` 和 `Task 7~12` 在 Map Gate 之后执行。 - -### Map 实际调用契约 - -`PlanningMapFactory` 应由长期存在的服务或测试对象持有,不能每次调用都重新 `new`,否则容量 4 的快照缓存无法跨调用复用: - -```csharp -private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); - -public PlanningMapBuildResult CreateCurrentMap( - MapBoundsMm bounds, - long manualVersion, - IReadOnlyList manualObstacles, - long twoLegVersion, - TwoLegProjectionInput twoLegSnapshot) -{ - return _mapFactory.Create(new PlanningMapRequest - { - Bounds = bounds, - ResolutionMm = 50f, - ObstacleSources = new IMapObstacleSource[] - { - new ManualObstacleSource( - "manual", manualVersion, true, manualObstacles), - new TwoLegObstacleSource( - "two-leg", twoLegVersion, false, twoLegSnapshot), - }, - AllowExplicitEmptyMap = false, - }); -} -``` - -调用方只判断统一结果,不接触 builder、rasterizer、投影器或 adapter: - -```csharp -PlanningMapBuildResult result = CreateCurrentMap( - bounds, manualVersion, manualObstacles, twoLegVersion, twoLegSnapshot); - -if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) - return; - -PlanningGridMap map = result.Map; -bool worldOriginIsOccupied = map.IsOccupiedWorld(0d, 0d); -double worldOriginClearance = - map.GetConservativeObstacleDistanceMeters(0d, 0d); -``` - -### Map Gate - -以下条件必须全部满足,才开始 CoarsePath 契约和搜索: - -- [ ] 新 `Map` 运行时代码不读取 `TwoLegDetect`、定位、Painter、Toast、UI 或系统时间。 -- [ ] 人工、TwoLeg 以及未来来源全部经 `IMapObstacleSource.ProjectToWorld()` 输出世界 mm 几何,再由唯一 rasterizer 写图。 -- [ ] 地图不写 AMR 自身,不使用旧默认 300 mm 膨胀。 -- [ ] `PlanningMapFactory.Create` 对完整输入命中、占据命中和占据变化给出可区分结果。 -- [ ] `MovementTest.MapTest` 只调用 `PlanningMapFactory` 和可选 PNG exporter,不复制地图构造逻辑。 -- [ ] 下列命令全部通过: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -``` - -### P0-MAP 执行状态(2026-07-27) - -- [x] 已完成 Task 1、Task 3~6、Task 13,以及 Task 14 的 `MovementTest.MapTest` 部分。 -- [x] 已建立新的 `Map` 目录结构、统一障碍物来源与 TwoLeg 检测时位姿投影;Map 核心不读取传感器、UI 或系统时钟。 -- [x] 已完成只读规划快照、保守 EDT 距离场、容量 4 的 LRU 缓存、PNG 可视化拆分和实际 MapTest 调用。 -- [x] 已通过上述 Map Gate 命令;另外 `verify_planning_utils.ps1` 也已通过。 -- [x] 未执行 Git 自检、暂存或提交。 - -## 最终目录与文件职责 - -```text -ClumsyPilot/ParkrobTrajplanner/ -├── Initial_plan/ ------ 方案与实施文档,不放运行时代码 -├── Utils/ ------ 通用、无状态、确定性数值工具 -│ ├── AngleMath.cs ------ 角度归一化、最短角差和航向离散索引 -│ ├── UnitConverter.cs ------ mm/m、deg/rad 和半径/曲率转换 -│ ├── CoordinateTransform.cs ------ 车体坐标与世界坐标二维刚体变换 -│ ├── NumericGuard.cs ------ 有限值、正值和参数范围校验 -│ └── GridIndex.cs ------ 不可变行列索引值对象 -├── Map/ ------ MultiWheelC.TrajectoryPlanning.Mapping -│ ├── Core/ -│ │ ├── EnvironmentGridMap.cs ------ 只保存外部障碍物的 mm 占据栅格 -│ │ ├── MapBoundsMm.cs ------ 有限、非退化的 mm 地图边界 -│ │ ├── MapBuildRequest.cs ------ 内部环境图构建请求 -│ │ ├── EnvironmentMapBuildResult.cs ------ 环境图、来源摘要和失败原因 -│ │ └── EnvironmentMapBuilder.cs ------ 校验来源并事务式合并障碍图层 -│ ├── Obstacles/ -│ │ ├── IMapObstacle.cs ------ 世界坐标障碍物公共几何契约 -│ │ ├── AxisAlignedRectangleObstacle.cs ------ 轴对齐矩形障碍 DTO -│ │ ├── CircleObstacle.cs ------ 圆形障碍 DTO -│ │ └── MapObstacleRasterizer.cs ------ 唯一占据栅格写入器 -│ ├── Sources/ -│ │ ├── IMapObstacleSource.cs ------ 纯快照障碍来源统一接口 -│ │ ├── ObstacleSourceStatus.cs ------ Applied/Empty/Unavailable/Invalid -│ │ ├── ObstacleProjectionResult.cs ------ 来源版本、状态、诊断和障碍集合 -│ │ ├── ManualObstacleSource.cs ------ 输出人工圆和矩形 -│ │ ├── TwoLegProjectionInput.cs ------ 两腿端点、检测状态和检测位姿快照 -│ │ ├── TwoLegObstacleSource.cs ------ TwoLeg 统一来源适配器 -│ │ └── TwoLegObstacleProjector.cs ------ 车体系两腿端点投影到世界系 -│ ├── Planning/ -│ │ ├── PlanningGridMap.cs ------ 只读 m 占据图、距离场和快照元数据 -│ │ ├── EuclideanDistanceTransform.cs ------ 线性时间精确二维欧氏距离变换 -│ │ ├── ObstacleDistanceField.cs ------ 不高估真实净空的距离下界 -│ │ ├── PlanningMapCache.cs ------ 容量 4 的线程安全两级快照缓存 -│ │ └── PlanningMapAdapter.cs ------ 占据深拷贝、mm→m 和距离场生成 -│ ├── PlanningMapRequest.cs ------ Map 模块统一输入 -│ ├── PlanningMapBuildResult.cs ------ Map 模块统一输出和缓存命中类型 -│ ├── PlanningMapFactory.cs ------ Map 模块唯一公共创建入口 -│ └── Test/ -│ ├── MovementTest.MapTest.cs ------ Clumsy UI 地图构建/显示入口 -│ └── Visualization/ -│ ├── PlanningMapImageExportRequest.cs ------ 快照、叠加层和输出选项 -│ ├── PlanningMapImageExportResult.cs ------ PNG 状态、路径、尺寸和诊断 -│ ├── PlanningMapImageExporter.cs ------ 校验、渲染编排和原子发布 -│ ├── PlanningMapImageRenderer.cs ------ 地图/车辆/路径绘制到 RGBA -│ └── ValidatedPngWriter.cs ------ Stb 编码、PNG 结构和 CRC 校验 -├── CoarsePath/ ------ MultiWheelC.TrajectoryPlanning.CoarsePath -│ ├── Contracts/ -│ │ ├── Pose2D.cs ------ m/rad 不可变二维位姿 -│ │ ├── TravelDirection.cs ------ Forward/Reverse -│ │ ├── GoalDirectionConstraint.cs ------ Any/Forward/Reverse -│ │ ├── VehicleParameters.cs ------ 尺寸、安全余量和最大曲率 -│ │ ├── PlanningRequest.cs ------ 纯规划器输入 -│ │ ├── HybridAStarConfiguration.cs ------ 原语、离散、代价、限额和容差 -│ │ ├── PlanningResult.cs ------ 状态、诊断、稠密路径和分段 -│ │ ├── PlanningStatus.cs ------ 输入、碰撞、搜索和验证状态 -│ │ ├── PlanningDiagnostics.cs ------ 节点、堆、耗时、路径和终止统计 -│ │ ├── CoarsePathPoint.cs ------ 位姿、弧长、方向、曲率和净空 -│ │ ├── CoarsePathPointSource.cs ------ Start/MotionPrimitive/GoalTruncation -│ │ └── PathSegment.cs ------ 包含式方向分段索引 -│ ├── Vehicle/ -│ │ ├── VehicleKinematics.cs ------ 解析保守最大曲率 -│ │ ├── VehicleFootprint.cs ------ 扩大矩形、AABB 和外接圆 -│ │ ├── OrientedRectangleCellIntersection.cs ------ 旋转矩形与格矩形精确相交 -│ │ └── FootprintCollisionChecker.cs ------ 边界、快速放行、精确和扫掠检查 -│ ├── Search/ -│ │ ├── MotionPrimitive.cs ------ 恒曲率原语和实际截断长度 -│ │ ├── MotionPrimitiveGenerator.cs ------ 解析积分并保留内部采样点 -│ │ ├── BinaryMinHeap.cs ------ netstandard2.0 确定性 Open List -│ │ ├── SearchCostCalculator.cs ------ 统一计算等效米搜索代价 -│ │ ├── HybridAStarNode.cs ------ 连续状态、代价、父索引和原语 -│ │ ├── HybridAStarNodeKey.cs ------ 位置/航向/方向/曲率离散键 -│ │ ├── GoalToleranceChecker.cs ------ 位置、航向和进入方向判断 -│ │ ├── GridDijkstraHeuristic.cs ------ 八邻域、禁止切角的二维启发 -│ │ └── HybridAStarSearch.cs ------ 扩展、重开、限额和候选管理 -│ ├── Output/ -│ │ ├── PathBacktracker.cs ------ 根据父索引重建原语内部点 -│ │ ├── CoarsePathAssembler.cs ------ 弧长、换向点和方向分段 -│ │ └── CoarsePathValidator.cs ------ 数值、碰撞、曲率、终点和分段复核 -│ ├── HybridAStarPlanner.cs ------ 只消费 PlanningGridMap 的下层门面 -│ ├── Facade/ -│ │ ├── CoarsePathPlanningJob.cs ------ 一次调用所需全部输入 -│ │ ├── CoarsePathPlanningJobResult.cs ------ 地图结果与粗路径结果 -│ │ ├── PlanningDebugOptions.cs ------ 地图、路径、碰撞调试开关 -│ │ ├── IPlanningDebugSink.cs ------ 不改变规划状态的调试消费接口 -│ │ └── CoarsePathPlanningService.cs ------ 建图、搜索和调试的一次调用入口 -│ └── Test/ -│ ├── CoarsePathScenarioFactory.cs ------ 固定、可复现的地图与规划场景 -│ └── MovementTest.CoarsePathTest.cs ------ 后台规划并在 Clumsy UI 显示 -│ └── README.md ------ 粗规划模块边界、调用示例和文档链接 - -ClumsyPilot/tests/ -├── verify_planning_utils.ps1 ------ 工具与公共契约 -├── verify_planning_map_factory.ps1 ------ 来源、事务、缓存与快照 -├── verify_planning_map_adapter.ps1 ------ 栅格、适配和距离场 -├── verify_planning_map_image.ps1 ------ PNG 渲染与原子发布 -├── verify_coarse_path_collision.ps1 ------ 亚栅格、擦边和扫掠碰撞 -├── verify_coarse_path_search.ps1 ------ 原语截断、堆、代价和搜索 -├── verify_coarse_path_integration.ps1 ------ 一次调用端到端验证 -└── benchmark_coarse_path.ps1 ------ Release 性能与内存门槛 -``` - -## 固定公共调用方式 - -常规业务代码只保留一个长期存在的服务实例: - -```csharp -var result = planningService.Plan(new CoarsePathPlanningJob -{ - MapRequest = new PlanningMapRequest - { - Bounds = bounds, - ResolutionMm = 50f, - ObstacleSources = new IMapObstacleSource[] - { - new ManualObstacleSource( - "manual", manualVersion, true, manualObstacles), - new TwoLegObstacleSource("two-leg", twoLegVersion, false, twoLegSnapshot), - }, - AllowExplicitEmptyMap = true, - }, - Start = startPose, - Goal = goalPose, - Vehicle = vehicle, - Configuration = configuration, - StartVehicleCurvature = 0d, - StartDirection = null, - GoalDirection = GoalDirectionConstraint.Any, - Debug = new PlanningDebugOptions - { - VisualizeMap = true, - VisualizePath = true, - VisualizeCollisionChecks = false, - }, -}, cancellationToken); -``` - -已有地图快照复用或纯搜索测试才直接调用 `HybridAStarPlanner.Plan(PlanningRequest, CancellationToken)`。业务调用方不得直接拼装 builder、rasterizer、碰撞器、运动原语或搜索节点。 - -## P0:正确可用 - -### Task 1:建立 Utils 与测试基座 - -**文件:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs` -- Create: `ClumsyPilot/tests/verify_planning_utils.ps1` - -**产出接口:** - -```csharp -double AngleMath.NormalizeRadians(double radians); -double AngleMath.ShortestSignedDifference(double from, double to); -int AngleMath.ToHeadingIndex(double heading, double resolution, int binCount); -double UnitConverter.MillimetersToMeters(double value); -double UnitConverter.DegreesToRadians(double value); -bool NumericGuard.IsFinite(double value); -readonly struct GridIndex { int Row; int Col; } -``` - -- [ ] 写失败测试:断言 `2π→0`、`179°→-179°` 最短角差为 `2°`、`1250 mm→1.25 m`、车体系 `(1000,0)` 在世界位姿 `(2000,3000,90°)` 后得到 `(2,4) m`,并拒绝 NaN/Infinity。 -- [ ] 执行以下命令,预期构建成功、脚本因新类型不存在返回非零。 - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -``` - -- [ ] 实现:角度统一到 `[-π,π)`;航向索引先归一化再 `floor`;刚体变换使用 `world=origin+R(heading)×local`;`GridIndex` 实现值相等和稳定哈希。 -- [ ] 重跑脚本,预期输出 `Planning utility checks passed.`。 - -### Task 2:锁定 CoarsePath 公共契约与默认参数 - -**文件:** - -- Create: `CoarsePath/Contracts/` 下目录树列出的 12 个契约文件 -- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1` - -**固定默认值:** - -```csharp -PrimitiveLengthMeters = 0.50; -IntegrationStepMeters = 0.05; -MaximumCollisionCheckStepMeters = 0.025; -HeadingResolutionRadians = Math.PI / 36d; -CurvatureLevelCount = 5; -GoalPositionToleranceMeters = 0.15; -GoalHeadingToleranceRadians = Math.PI / 36d; -MaximumExpandedNodes = 200000; -SearchTimeout = TimeSpan.FromSeconds(5); -HeuristicWeight = 1.0; -ReverseCostMultiplier = 1.5; -GearSwitchPenaltyMeters = 1.0; -CurvatureMagnitudeWeight = 0.10; -CurvatureChangePenaltyMetersPerLevel = 0.05; -ClearanceCostWeight = 0.20; -ClearanceCostDistanceMeters = 0.50; -``` - -- [ ] 扩展失败测试:断言上述默认值,三个方向/来源枚举,以及 `PlanningRequest` 的起始曲率、起始方向、目标进入方向。 -- [ ] 断言 `PlanningStatus` 包含:`Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed`、`InternalError`。 -- [ ] 实现不可变 `Pose2D`;结果工厂只允许成功结果携带路径,失败结果路径为空。 -- [ ] `PlanningDiagnostics` 固定记录扩展节点数、生成节点数、重新打开节点数、陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因。 -- [ ] `CoarsePathPoint` 固定包含 `X/Y`、`Heading/UnwrappedHeading`、`ArcLength`、`Direction`、`VehicleCurvature`、`BodyClearance`、`IsGearSwitchPoint` 和 `Source`;`PathSegment` 固定包含方向和包含式 `StartIndex/EndIndex`。 -- [ ] 重跑工具脚本,预期契约和默认值全部通过。 - -### Task 3:实现环境栅格、边界与统一栅格化 - -**文件:** - -- Create: `Map/Core/MapBoundsMm.cs` -- Create: `Map/Core/EnvironmentGridMap.cs` -- Create: `Map/Obstacles/IMapObstacle.cs` -- Create: `Map/Obstacles/AxisAlignedRectangleObstacle.cs` -- Create: `Map/Obstacles/CircleObstacle.cs` -- Create: `Map/Obstacles/MapObstacleRasterizer.cs` -- Create: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` - -上述 Map/CoarsePath 相对路径均位于 `ClumsyPilot/ParkrobTrajplanner/`。 - -- [ ] 写边界和栅格失败测试:20/200 mm 合法,范围外失败;4,000,001 格分配前失败;`XMax/YMax` 排他;非完整末格裁剪;地图外占据;圆/矩形与格边或格角接触时保守占据;完全在地图外的合法障碍不写格。 -- [ ] 运行脚本,预期新 Map 类型不存在。 -- [ ] 实现私有行优先 `byte[]`,索引固定为 `row*Cols+col`;世界转格使用 `floor((value-min)/resolution)`;不得返回内部缓冲区。 -- [ ] 实现唯一栅格化器:圆使用“圆心到格矩形最近点距离”,轴对齐矩形使用闭区间相交,只遍历裁剪后的候选包围盒。 -- [ ] 确认不存在 `MarkVehicleFootprint` 或安全距离参数,重跑脚本通过。 - -### Task 4:统一人工与 TwoLeg 障碍来源 - -**文件:** - -- Create: `Map/Sources/` 下目录树列出的 7 个文件 -- Create: `Map/Core/MapBuildRequest.cs` -- Create: `Map/Core/EnvironmentMapBuildResult.cs` -- Create: `Map/Core/EnvironmentMapBuilder.cs` -- Create: `ClumsyPilot/tests/verify_planning_map_factory.ps1` - -**固定接口:** - -```csharp -public interface IMapObstacleSource -{ - string SourceId { get; } - long SourceVersion { get; } - bool IsRequired { get; } - ObstacleProjectionResult ProjectToWorld(); -} -``` - -- [ ] 构造函数统一为 `ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles)` 和 `TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input)`;计划内所有实际调用均使用这两个签名。 -- [ ] 写来源事务失败测试:人工 `Applied/Empty`、TwoLeg 两个圆、可选来源 `Unavailable/Invalid` 保留人工图层、必需来源失败导致整图失败、重复 `SourceId` 失败、输入顺序不同但结果相同。 -- [ ] 校验 `SourceId` 非空且一次请求内唯一,`SourceVersion` 非负;来源快照内容发生变化时,上层必须递增版本。 -- [ ] 写 TwoLeg 坐标测试:使用检测时车辆位姿完成车体系 mm 到世界系 mm 变换,不得使用规划开始时位姿。 -- [ ] 实现纯快照投影:`ProjectToWorld()` 不能调用 `TwoLegDetect`、定位、UI 或系统时间;过期判断由上层采集适配器在构造 DTO 前完成。 -- [ ] builder 按 `SourceId` 排序;可选失败只记诊断;必需失败不发布地图;全部成功几何交给唯一 rasterizer。 -- [ ] 重跑工厂脚本,预期来源状态、投影和事务断言通过。 - -### Task 5:生成不可变 PlanningGridMap 和保守距离场 - -**文件:** - -- Create: `Map/Planning/EuclideanDistanceTransform.cs` -- Create: `Map/Planning/ObstacleDistanceField.cs` -- Create: `Map/Planning/PlanningGridMap.cs` -- Create: `Map/Planning/PlanningMapAdapter.cs` -- Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` - -- [ ] 扩展失败测试:mm→m、占据深拷贝、地图外距离为零、障碍格距离为零、空图距离正无穷、末格裁剪,以及所有样本距离不高于暴力几何距离。 -- [ ] 实现两次一维平方距离变换,复杂度 `O(Rows×Cols)`;不得逐自由格遍历全部障碍格。 -- [ ] 对精确栅格中心距离应用: - -```csharp -Math.Max(0d, centerDistanceMeters - Math.Sqrt(2d) * resolutionMeters) -``` - -- [ ] 查询使用点所在格的保守值,不做可能抬高结果的插值;空图跳过 EDT;边界检查独立执行。 -- [ ] `PlanningGridMap` 私有保存连续占据/距离数组,不提供写入口和缓冲区引用。 -- [ ] 重跑地图适配脚本,预期栅格、深拷贝、空图和保守距离全部通过。 - -### Task 6:实现 PlanningMapFactory 与两级快照缓存 - -**文件:** - -- Create: `Map/Planning/PlanningMapCache.cs` -- Create: `Map/PlanningMapRequest.cs` -- Create: `Map/PlanningMapBuildResult.cs` -- Create: `Map/PlanningMapFactory.cs` -- Modify: `Map/Planning/PlanningGridMap.cs` -- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` - -```csharp -public sealed class PlanningMapFactory -{ - public PlanningMapBuildResult Create(PlanningMapRequest request); -} -``` - -`PlanningMapRequest` 固定包含 `MapBoundsMm Bounds`、`float ResolutionMm`、`IReadOnlyList ObstacleSources`、`bool AllowExplicitEmptyMap`。 - -- [ ] 写缓存测试:完全相同请求返回同一 `PlanningGridMap`;来源版本变化但最终占据未变化时产生新 `SnapshotId` 并复用占据/距离缓冲;占据变化时重建距离场。 -- [ ] 写规划可用性测试:至少一个成功来源提供有效障碍语义时 `PlanningReady=true`;所有来源均为空时仅 `AllowExplicitEmptyMap=true` 可用;必需来源失败或未明确空图语义时 `PlanningReady=false` 并填写 `PlanningBlockReason`,规划器随后返回 `MapNotReady`。 -- [ ] 验证失效边界:起终点、车辆、规划参数和可视化开关不进入地图指纹;边界、分辨率、空图策略、来源状态/版本或规范化几何变化必须进入。 -- [ ] 实现确定性 `InputFingerprint`:来源先按 ID 排序,字段固定顺序,浮点按 IEEE 位模式;命中后仍比较规范化结构。 -- [ ] 栅格化后计算 `OccupancyHash`;命中后仍比较地图几何、数组长度和逐字节内容,不能只信任哈希。 -- [ ] 实现容量 4 的线程安全 LRU;完整命中返回同一快照,占据命中共享不可变缓冲但生成新元数据,只有占据变化才运行 EDT。 -- [ ] `PlanningMapBuildResult` 固定返回 `Succeeded`、失败原因、来源摘要、缓存命中类型和成功时的 `PlanningGridMap`;快照固定保存 `PlanningReady`、`PlanningBlockReason`、来源版本摘要、`InputFingerprint`、`OccupancyHash`、单调 `SnapshotId`。 -- [ ] 增加 16 个并发相同请求测试和 LRU 淘汰测试,重跑脚本通过。 - -### Task 7:实现连续车体足迹和保守碰撞检查 - -**文件:** - -- Create: `CoarsePath/Vehicle/` 下目录树列出的 4 个文件 -- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1` - -**固定接口:** - -```csharp -bool IsPoseCollisionFree( - Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, - double additionalMarginMeters, out double bodyClearanceMeters); - -bool IsSweptMotionCollisionFree( - Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, - double maximumCenterStepMeters, out double minimumBodyClearanceMeters); -``` - -- [ ] 写失败测试:正交、45°、任意航向、栅格中心/亚栅格中心、边角接触、薄障碍、地图边界、距离场快速放行,以及两个无碰撞端点之间有障碍的扫掠场景。 -- [ ] 实现以几何中心为参考的扩大车体;安全余量加到长度和宽度两侧;最大曲率与最小转弯半径并存时取更保守限制。 -- [ ] 使用分离轴定理精确判断连续旋转矩形与占据格矩形相交,接触视为碰撞;不得使用离散航向模板。 -- [ ] 检查顺序:扩大车体边界 → 保守距离严格大于外接圆时快速放行 → AABB 内占据格 SAT。 -- [ ] 扫掠采样中心步长不超过 `min(configuredStep,map.Resolution/2)`;每段临时附加余量为: - -```text -0.5 × (centerDisplacement - + circumscribedRadius × abs(headingDelta)) -``` - -- [ ] 重跑碰撞脚本,预期所有亚栅格、擦边和扫掠案例通过。 - -### Task 8:实现解析恒曲率原语和内部终点截断 - -**文件:** - -- Create: `CoarsePath/Search/MotionPrimitive.cs` -- Create: `CoarsePath/Search/MotionPrimitiveGenerator.cs` -- Create: `CoarsePath/Search/GoalToleranceChecker.cs` -- Create/Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**解析积分:** - -```csharp -double signedDistance = direction == TravelDirection.Forward ? step : -step; -double nextHeading = AngleMath.NormalizeRadians( - heading + curvature * signedDistance); - -if (Math.Abs(curvature) < 1e-12) -{ - nextX = x + signedDistance * Math.Cos(heading); - nextY = y + signedDistance * Math.Sin(heading); -} -else -{ - nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature; - nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature; -} -``` - -- [ ] 写原语测试:直行、圆弧、倒车、五级曲率、相邻曲率最多变化一级、最大长度 0.50 m、实际采样步长不超过 `min(IntegrationStep,CollisionStep,MapResolution/2)`。 -- [ ] 写用户提出的案例:目标距起点 0.30 m、原语最大 0.50 m、收紧容差;断言第一个满足目标的内部点截断,后续点不生成,来源为 `GoalTruncation`。 -- [ ] 每个内部点严格按“有限值 → 扫掠碰撞 → 目标条件”检查;碰撞必须先于目标。 -- [ ] 起点已满足目标时创建零长度候选,不生成原语。 -- [ ] 重跑搜索脚本,预期几何、碰撞采样和 0.30 m 截断案例通过。 - -### Task 9:实现 Open List、统一代价和二维启发 - -**文件:** - -- Create: `CoarsePath/Search/BinaryMinHeap.cs` -- Create: `CoarsePath/Search/SearchCostCalculator.cs` -- Create: `CoarsePath/Search/GridDijkstraHeuristic.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**固定代价:** - -```text -primitiveCost = - lengthMeters - × directionMultiplier - × (1 - + CurvatureMagnitudeWeight × abs(curvature / maximumCurvature) - + ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters)) - + gearSwitchPenalty - + CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta) -``` - -- [ ] 写堆顺序测试:较小 `F`、较小 `H`、较大 `G`、较小插入序号;相同输入重复运行顺序一致。 -- [ ] 写代价测试:前进、倒车、换向、曲率幅值、曲率变化和净空项;拒绝负数及非有限权重。 -- [ ] 写 Dijkstra 测试:八邻域直/斜代价;两个正交邻格任一占据时禁止对角切角;二维不可达返回明确状态。 -- [ ] 实现专用二叉最小堆,不依赖 `PriorityQueue`;允许旧条目由搜索层惰性丢弃。 -- [ ] `HeuristicWeight=1` 使用 `F=G+H`;大于 1 时只承诺可行性。 -- [ ] 重跑搜索脚本,预期顺序、公式和切角限制通过。 - -### Task 10:实现 Hybrid A* 节点、重开和终点候选管理 - -**文件:** - -- Create: `CoarsePath/Search/HybridAStarNode.cs` -- Create: `CoarsePath/Search/HybridAStarNodeKey.cs` -- Create: `CoarsePath/Search/HybridAStarSearch.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -- [ ] 写搜索测试:空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、`±π` 容差、无解、取消、超时、节点上限、重开和确定性。 -- [ ] 写候选排序测试:先生成较大 `F` 的终点候选时不得结束;较小 `F` 普通节点先出队;候选成为最佳有效条目后才成功。 -- [ ] 离散键固定为位置格、航向格、方向和曲率等级;连续位姿保留在节点中。 -- [ ] `Dictionary` 保存普通状态最佳 `G`;更小 `G` 允许重开;旧普通堆条目惰性丢弃。 -- [ ] 终点候选放入同一堆,但不能仅因另一个连续位姿落入相同离散键且 `G` 更低而被删除;候选出队时重新验证目标和末段碰撞。 -- [ ] 搜索循环在每次扩展前按顺序检查取消、5 s 超时、200,000 节点上限,再弹出有效条目;Open List 为空返回 `NoFeasiblePath`。 -- [ ] 重跑搜索脚本,预期候选顺序、重开、限额和所有场景通过。 - -### Task 11:回溯、装配、最终复核和 HybridAStarPlanner - -**文件:** - -- Create: `CoarsePath/Output/PathBacktracker.cs` -- Create: `CoarsePath/Output/CoarsePathAssembler.cs` -- Create: `CoarsePath/Output/CoarsePathValidator.cs` -- Create: `CoarsePath/HybridAStarPlanner.cs` -- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -```csharp -public sealed class HybridAStarPlanner -{ - public PlanningResult Plan( - PlanningRequest request, - CancellationToken cancellationToken = default(CancellationToken)); -} -``` - -- [ ] 写失败状态测试:请求、地图就绪、车辆参数、曲率配置、起终点越界/碰撞,以及搜索失败到结果的无异常映射。 -- [ ] 写输出测试:首点弧长零;弧长不递减;`UnwrappedHeading` 连续;终点截断来源正确;除换向对外无相邻重复点。 -- [ ] 写换向分段测试:换向处保留两个坐标/航向/弧长相同而方向不同的点;新方向点标记 `IsGearSwitchPoint`;包含式分段完整覆盖路径。 -- [ ] 搜索节点只存父索引、方向、曲率和实际原语长度;成功后使用相同解析积分和有效步长重建内部点。 -- [ ] 最终复核有限数值、曲率、扫掠碰撞、目标容差/方向、弧长、换向对和分段;失败返回 `FinalValidationFailed` 且不发布部分路径。 -- [ ] 重跑碰撞、搜索和集成脚本,预期全部通过。 - -### Task 12:实现一次调用 CoarsePathPlanningService - -**文件:** - -- Create: `CoarsePath/Facade/` 下目录树列出的 5 个文件 -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -```csharp -public sealed class CoarsePathPlanningService -{ - public CoarsePathPlanningJobResult Plan( - CoarsePathPlanningJob job, - CancellationToken cancellationToken = default(CancellationToken)); -} -``` - -- [ ] 写一次调用测试:固定执行 `PlanningMapFactory.Create → HybridAStarPlanner.Plan → debug sink`;地图失败不启动搜索;结果同时保留地图和规划结果。 -- [ ] 写旁路隔离测试:可视化开关不改变地图指纹、占据哈希、规划状态或路径;debug sink 异常只写调试诊断。 -- [ ] 服务持有同一 `PlanningMapFactory`,多次调用共享容量 4 缓存;默认 debug sink 为空行为。 -- [ ] 重跑以下 P0 验收,预期构建成功且 6 个脚本退出码为 0。 - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -## P0-MAP 收尾与 P1:集成、迁移和性能 - -### Task 13(P0-MAP):拆分迁移 TrapMapImageExporter - -**现有来源:** - -- Read/Migrate: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` - -**新文件:** - -- Create: `Map/Test/Visualization/PlanningMapImageExportRequest.cs` -- Create: `Map/Test/Visualization/PlanningMapImageExportResult.cs` -- Create: `Map/Test/Visualization/PlanningMapImageExporter.cs` -- Create: `Map/Test/Visualization/PlanningMapImageRenderer.cs` -- Create: `Map/Test/Visualization/ValidatedPngWriter.cs` -- Create: `ClumsyPilot/tests/verify_planning_map_image.ps1` - -- [ ] 写新图片测试:输入只允许 `PlanningGridMap`;覆盖关闭导出、非法尺寸、唯一命名、临时文件清理、PNG 签名/IHDR/IEND 和每个 chunk CRC。 -- [ ] 保留现有有效限制:`PixelsPerCell=4`、`MaximumImageEdgePixels=4000`、`MaximumFileSizeBytes=50 MiB`、`OutputDpi=300`、最多 1024 次重名重试、StbImageWriteSharp 1.16.7。 -- [ ] 按职责迁移:Request/Result 只放 DTO;Renderer 生成 RGBA;Writer 负责编码/CRC;Exporter 校验、独占临时文件和原子发布。 -- [ ] 新导出器不得依赖 `GridMapData`、`TrapMapVehiclePose`、TwoLeg 状态、传感器或地图构造器;车辆、起终点和路径只作可选叠加层。 -- [ ] 运行图片脚本通过后先保留旧文件,Task 16 确认等价覆盖后再退役。 - -### Task 14:增加地图与粗路径 MovementTest - -**文件:** - -- Create: `Map/Test/MovementTest.MapTest.cs` -- Create: `CoarsePath/Test/CoarsePathScenarioFactory.cs` -- Create: `CoarsePath/Test/MovementTest.CoarsePathTest.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -- [ ] **P0-MAP 部分:** 先创建 `MovementTest.MapTest.cs`。它只调用长期持有的 `PlanningMapFactory`,显示来源状态、栅格、快照 ID 和缓存命中,并可选调用新 PNG 导出器;通过 Map Gate 后即可结束当前首要里程碑。 -- [ ] **P1 部分:** Map Gate 和 P0-PLAN 均通过后,再创建 `CoarsePathScenarioFactory.cs` 与 `MovementTest.CoarsePathTest.cs`。 -- [ ] 场景工厂提供显式空图、单矩形绕行、人工圆+矩形+TwoLeg、相同地图缓存命中、倒车换向和无解案例。 -- [ ] CoarsePath 案例使用同一 `CoarsePathPlanningService`;测试类不得直接实例化 rasterizer、碰撞器、原语生成器或搜索节点。 -- [ ] `MovementTest.CoarsePathTest` 在后台任务调用同步 `Plan`,绘制起点、目标、路径、换向点和扩大车体检查点。 -- [ ] `TestStop` 先取消专用 `CancellationTokenSource`,再清理任务和 Painter;两个入口不得发送底盘运动命令。 -- [ ] Clumsy UI 手动运行时不阻塞界面,停止后无后台规划残留,调试开关不改变结果。 - -### Task 15(P1):性能、资源和确定性验收 - -**文件:** - -- Create: `ClumsyPilot/tests/benchmark_coarse_path.ps1` -- Modify: `Map/Planning/PlanningMapCache.cs` -- Modify: `Map/Planning/EuclideanDistanceTransform.cs` -- Modify: `CoarsePath/Search/BinaryMinHeap.cs` -- Modify: `CoarsePath/Search/HybridAStarSearch.cs` -- Modify: `CoarsePath/Contracts/PlanningDiagnostics.cs` - -- [ ] 基准脚本输出地图规模、缓存命中、状态、耗时、扩展/生成/重开节点、陈旧堆条目、Open List 峰值和托管内存增量,超限返回非零。 -- [ ] 地图参考:20 m×20 m、0.05 m、160,000 格、100 障碍;20 次后完整构建 P95≤200 ms,完整缓存命中 P95≤5 ms。 -- [ ] 地图极限:4,000,000 格、100 障碍;3 s 内成功或明确失败;成功时内存增量≤160 MB,无溢出和部分快照。 -- [ ] 规划参考:12 m×8 m、0.05 m、矩形阻断直线、距离≥8 m;20 次 P95≤2 s,内存增量≤256 MB。 -- [ ] 规划压力:20 m×20 m、0.05 m;成功或无解均在 5 s、200,000 节点和 512 MB 增量内返回。 -- [ ] 优化只针对查询分配、重复 EDT、堆扩容和稠密点保存;不得降低碰撞保守性、跳过最终复核或放宽失败状态。 -- [ ] 运行: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj -c Release --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\benchmark_coarse_path.ps1 -Configuration Release -``` - -预期:全部指标达标,脚本退出码为 0。 - -### Task 16(P1):退役旧地图入口并完成文档 - -**文件:** - -- Modify/Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/MovementTest.Trapmaptest.cs` -- Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_grid.ps1` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_lifecycle.ps1` -- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_image.ps1` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - -- [ ] 建立覆盖表:`TrapMapBounds→MapBoundsMm`、`GridMapData→EnvironmentGridMap/PlanningGridMap`、`TrapMapLayerComposer→EnvironmentMapBuilder`、`TrapMapBuilder.Get→上层采集+CoarsePathPlanningService`、旧 exporter→五个 Visualization 文件。 -- [ ] 等价脚本全部通过后再删除旧实现;不保留车辆写图、默认 300 mm 膨胀或地图构建器直接调用 `TwoLegDetect` 的兼容开关。 -- [ ] README 写明一次调用、下层门面、mm/m-rad 边界、快照变化判断、调试开关不参与指纹、P0/P1 命令和非目标。 -- [ ] 执行旧引用搜索: - -```powershell -rg -n "GridMapData|TrapMapBuilder|TrapMapLayerComposer|TrapMapImageExporter|MarkVehicleFootprint" .\ClumsyPilot -``` - -预期:仅迁移说明或历史文档可命中;运行时代码和新测试不得命中旧类型。 - -- [ ] 运行最终回归: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -预期:Debug 构建成功,7 个功能脚本退出码均为 0;随后重跑 Task 15 Release 基准并通过。 - -## 最终验收清单 - -- [ ] `CoarsePathPlanningService.Plan(job)` 是推荐的一次调用入口。 -- [ ] `PlanningMapFactory` 和 `HybridAStarPlanner` 仅作为可独立测试的下层门面。 -- [ ] 人工、TwoLeg 和未来障碍通过同一个 `IMapObstacleSource` 进入唯一栅格化器。 -- [ ] 地图未变化时复用完整快照或占据/距离缓冲;可视化、起终点和车辆参数不参与地图变化判断。 -- [ ] 地图不包含车辆自身和安全膨胀,碰撞检查使用扩大车辆矩形。 -- [ ] 距离场是净空下界,不能因高估而跳过精确碰撞。 -- [ ] 0.50 m 原语可在任意内部采样点截断;终点候选按 Open List 顺序出队后才终止。 -- [ ] best-G、重开、陈旧条目、候选保护和堆排序均有自动化测试。 -- [ ] 输出包含稠密点、保守净空、实际曲率、换向点和完整方向分段。 -- [ ] PNG 和 MovementTest 只消费只读快照,不成为核心规划依赖。 -- [ ] 失败、取消、超时和限额均返回空路径及明确状态,不发布部分结果。 -- [ ] Debug 与 Release 验收全部通过,且未执行 Git 自检或提交。 diff --git a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md b/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md deleted file mode 100644 index 0d05929..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md +++ /dev/null @@ -1,1194 +0,0 @@ -# AMR 非结构化道路轨迹规划技术路线(面向 Agent) - -## 1. 项目目标 - -开发一套面向非结构化道路四舵轮 AMR 的轨迹规划系统。 - -第一阶段采用车式运动模型,具体边界如下: - -- 支持前进、倒车和换向; -- 暂不支持蟹行、纯横移和原地旋转; -- 车体航向始终表示车头朝向; -- 规划结果必须满足完整车体无碰撞、最大曲率约束和目标位姿要求; -- 最终输出可供底盘控制器跟踪的带时间轨迹。 - -整体技术路线分为四个主要阶段: - -```mermaid -flowchart LR - A[输入与地图构建] --> B[Hybrid A* 粗路径规划] - B --> C[路径平滑与可行性处理] - C --> D[SQP 轨迹优化] - D --> E[输出可跟踪轨迹] -``` - ---- - -## 2. 总体模块划分 - -```text -Module 1:输入与地图构建 -Module 2:Hybrid A* 粗路径规划 -Module 3:路径平滑与可行性处理 -Module 4:SQP 轨迹优化 -Module 5:轨迹输出与底盘接口 -``` - -各模块必须通过明确的数据结构传递结果,不允许后级模块直接读取前级模块内部状态。 - ---- - -# 3. Module 1:输入与地图构建 - -## 3.1 模块目标 - -根据起点、终点、车辆参数和环境障碍物信息,构建一张供全局搜索使用的任务级二维栅格地图。 - -地图范围至少覆盖: - -- AMR 起点; -- 目标位置; -- 起点与目标之间的规划区域; -- 周围必要的安全边界。 - -当前阶段继续使用已有 `GridMapData` 作为地图数据来源,并通过适配器转换为 Hybrid A* 使用的规划地图。 - ---- - -## 3.2 输入数据 - -### 3.2.1 起点位姿 - -```text -StartPose: - X - Y - Heading - StartDirection - StartVehicleCurvature -``` - -字段定义: - -- `X、Y`:AMR 几何中心在世界坐标系中的位置; -- `Heading`:车头航向; -- `StartDirection`:`Forward`、`Reverse` 或 `Both`; -- `StartVehicleCurvature`:车辆当前转向曲率,未提供时默认为 0。 - ---- - -### 3.2.2 终点位姿 - -```text -GoalPose: - X - Y - Heading - GoalDirectionConstraint -``` - -目标进入方向约束: - -```text -Any -Forward -Reverse -``` - -其中: - -- `GoalHeading` 始终表示目标车头朝向; -- `GoalDirectionConstraint` 只约束最后一个连续运动段的运动方向。 - ---- - -### 3.2.3 车辆参数 - -```text -VehicleParameters: - VehicleLength - VehicleWidth - MaxVehicleCurvature - MinTurningRadius -``` - -第一阶段车辆参考点统一采用 AMR 几何中心。 - ---- - -### 3.2.4 环境障碍物 - -当前已接入: - -```text -TwoLegDetect 两腿检测结果 -``` - -后续扩展: - -```text -点云障碍物 -激光雷达障碍物 -静态障碍物 -动态障碍物 -禁行区域 -``` - ---- - -### 3.2.5 地图配置 - -```text -MapConfiguration: - GridResolution - MapMarginX - MapMarginY - ObstacleInflation - MaximumCellCount -``` - ---- - -## 3.3 当前地图构建逻辑 - -```mermaid -flowchart TD - A[读取 AMR 位姿与车身尺寸] --> B[读取目标位置与地图参数] - B --> C[计算任务级地图边界] - C --> D[创建 GridMapData] - D --> E[读取 TwoLegDetect 障碍物] - E --> F[车体系坐标转世界坐标] - F --> G[圆形障碍物栅格化] - G --> H[输出环境占据栅格] -``` - -当前地图边界由起点与目标位置共同决定: - -```text -XMin = min(StartX, GoalX) - MapMarginX -XMax = max(StartX, GoalX) + MapMarginX - -YMin = min(StartY, GoalY) - MapMarginY -YMax = max(StartY, GoalY) + MapMarginY -``` - -栅格尺寸: - -```text -Cols = ceil((XMax - XMin) / Resolution) -Rows = ceil((YMax - YMin) / Resolution) -``` - ---- - -## 3.4 当前地图适配要求 - -现有地图格式为: - -```text -单位:mm -数组:Cells[row, col] -状态:0 = 未标记占据,1 = Occupied -``` - -Hybrid A* 内部统一使用: - -```text -单位:m -角度:rad -曲率:1/m -``` - -因此需要增加: - -```text -PlanningMapAdapter -``` - -适配流程: - -```mermaid -flowchart TD - A[GridMapData] --> B[校验边界、尺寸与分辨率] - B --> C[深拷贝 Occupancy] - C --> D[毫米转换为米] - D --> E[生成障碍物距离场] - E --> F[输出 PlanningGridMap] -``` - ---- - -## 3.5 地图构建阶段的关键规则 - -### 3.5.1 规划地图不得包含 AMR 自身占据层 - -AMR 自身不能写入环境占据地图,否则 Hybrid A* 起点会直接被判定为碰撞。 - -正确关系: - -```text -环境地图: - 只包含外部障碍物 - -AMR 自身: - 通过 VehiclePose、VehicleLength、VehicleWidth 单独描述 -``` - ---- - -### 3.5.2 障碍物膨胀量必须明确 - -建议将: - -```text -SafetyMarginMm -``` - -改为: - -```text -ObstacleInflationMm -``` - -其唯一含义是: - -> 障碍物写入地图前向外膨胀的距离。 - -必须避免: - -```text -障碍物已经膨胀 -+ -车辆碰撞矩形再次增加同样余量 -``` - ---- - -### 3.5.3 区分地图创建成功与地图可用于规划 - -```text -Succeeded: - 地图数据结构是否创建成功 - -PlanningReady: - 当前地图是否满足规划输入要求 - -PlanningBlockReason: - 地图为何不能用于规划 -``` - -例如: - -```text -TwoLegDetect 正常: - Succeeded = true - PlanningReady = true - -TwoLegDetect 无结果: - Succeeded = true - PlanningReady = false -``` - ---- - -## 3.6 地图输出 - -```text -PlanningGridMap: - XMin - XMax - YMin - YMax - Resolution - Rows - Cols - Occupancy[row, col] - ObstacleDistance[row, col] - ObstacleInflation - InputSource - PlanningReady - MapVersion -``` - -当前第一版状态定义: - -```text -Occupancy = 0: - 当前地图层未标记为占据 - -Occupancy = 1: - 占据 - -地图外部: - 按占据处理 -``` - ---- - -# 4. Module 2:Hybrid A* 粗路径规划 - -## 4.1 模块目标 - -在任务级栅格地图中搜索一条: - -- 从起点到目标附近; -- 完整车体无碰撞; -- 满足最大曲率限制; -- 支持前进、倒车和换向; -- 具有正确绕障拓扑; -- 可作为后续平滑和 SQP 优化初值; - -的空间粗路径。 - -Hybrid A* 负责决定: - -```text -从哪里走 -绕障碍物哪一侧走 -什么时候前进 -什么时候倒车 -在哪里换向 -以什么车头航向接近目标 -``` - -Hybrid A* 不负责: - -```text -速度 -加速度 -角速度 -时间戳 -舵轮角度 -轮速 -``` - ---- - -## 4.2 搜索状态 - -连续状态: - -```text -State: - x - y - theta - direction - curvatureIndex -``` - -字段定义: - -- `x、y`:AMR 几何中心位置; -- `theta`:车头航向; -- `direction`:`Forward` 或 `Reverse`; -- `curvatureIndex`:当前离散车辆曲率等级。 - -Closed Set 键: - -```text -GridKey: - ix - iy - iHeading - direction - curvatureIndex -``` - ---- - -## 4.3 运动模型 - -定义: - -```text -directionSign = +1:Forward -directionSign = -1:Reverse -``` - -运动积分: - -```text -dx/ds = directionSign × cos(theta) -dy/ds = directionSign × sin(theta) -dtheta/ds = directionSign × VehicleCurvature -``` - -其中: - -```text -Heading: - 始终表示车头朝向 - -ArcLength: - 始终非负并单调增加 -``` - ---- - -## 4.4 运动原语 - -第一版离散曲率集合: - -```text --kappaMax --0.5 × kappaMax -0 -+0.5 × kappaMax -+kappaMax -``` - -每个曲率均支持: - -```text -Forward -Reverse -``` - -第一版建议参数: - -```text -PrimitiveLength = 0.50 m -IntegrationStep = 0.05 m -``` - -每条原语按 0.05 m 步长积分,并在每个内部积分点执行完整车体碰撞检测。 - ---- - -## 4.5 搜索约束 - -每个候选原语必须检查: - -```text -地图边界 -Occupied 栅格 -完整车辆矩形碰撞 -安全净空 -最大车辆曲率 -允许的运动方向 -曲率等级切换规则 -目标进入方向 -``` - -第一阶段暂不直接约束: - -```text -速度 -加速度 -曲率时间变化率 -舵轮转向速度 -``` - -但需要通过代价项减少极端曲率跳变。 - ---- - -## 4.6 搜索代价 - -```text -g = - 路径长度代价 - + 倒车代价 - + 换向代价 - + 曲率代价 - + 曲率变化代价 - + 障碍物距离代价 -``` - -启发函数: - -```text -h = max(h2D, hRS) -``` - -其中: - -```text -h2D: - 基于二维 Dijkstra 的绕障距离 - -hRS: - 基于 Reeds–Shepp 模型的非完整约束距离 -``` - -节点排序: - -```text -f = g + HeuristicWeight × h -``` - ---- - -## 4.7 终点附近 Reeds–Shepp 连接 - -```mermaid -flowchart TD - A[当前节点接近目标] --> B[生成 Reeds-Shepp 候选] - B --> C[按 0.05 m 采样] - C --> D[完整车体碰撞检查] - D --> E[最大曲率与目标方向检查] - E -->|成功| F[精确连接目标] - E -->|失败| G[继续 Hybrid A* 搜索] -``` - -支持模式: - -```text -Disabled -Opportunistic -RequiredNearGoal -``` - -停车、钻入和精确泊入任务优先使用: - -```text -RequiredNearGoal -``` - ---- - -## 4.8 Hybrid A* 输出 - -```text -CoarsePathPoint: - X - Y - Heading - UnwrappedHeading - ArcLength - Direction - VehicleCurvature - GeometricCurvature - BodyClearance - IsGearSwitchPoint - Source -``` - -路径按运动方向拆分: - -```text -PathSegment: - SegmentIndex - Direction - StartIndex - EndIndex - StartsAtGearSwitch - EndsAtGearSwitch -``` - ---- - -# 5. Module 3:路径平滑与可行性处理 - -## 5.1 模块目标 - -消除 Hybrid A* 粗路径中的离散折线和不连续曲率,生成更平滑的空间参考路径。 - -该阶段只优化几何形状,不进行时间参数化。 - -不得改变: - -```text -绕障侧 -前进与倒车顺序 -换向次数 -方向段拓扑 -目标进入方向 -``` - ---- - -## 5.2 处理流程 - -```mermaid -flowchart TD - A[Hybrid A* 稠密粗路径] --> B[删除重复点] - B --> C[按 Forward/Reverse 分段] - C --> D[保留关键点] - D --> E[按弧长重采样] - E --> F[几何平滑] - F --> G[完整可行性检查] - G --> H[平滑空间路径] -``` - ---- - -## 5.3 必须保留的关键点 - -```text -起点 -终点 -换向点 -曲率等级切换点 -直线与弯道切换点 -Reeds–Shepp 连接点 -局部最小净空点 -明显航向变化点 -``` - -禁止跨换向点进行单条曲线拟合。 - ---- - -## 5.4 平滑方法 - -### 5.4.1 贝塞尔曲线 - -适用于: - -```text -局部短转角 -折线拐点快速平滑 -局部连接 -``` - -特点: - -```text -实现简单 -计算速度快 -局部形状易控制 -``` - ---- - -### 5.4.2 五次多项式 - -适用于: - -```text -末端短距离连接 -具有明确起终点位置、航向和曲率的局部段 -``` - -特点: - -```text -可同时约束位置、航向和曲率 -适合末端姿态调整 -``` - ---- - -### 5.4.3 B 样条 - -适用于: - -```text -较长的普通路径段 -多点连续平滑 -降低局部折线感 -``` - -建议采用三次 B 样条作为第一版普通路径段的主平滑方法。 - ---- - -## 5.5 可行性检查 - -平滑后的路径必须重新检查: - -```text -完整车体碰撞 -地图边界 -Unknown / Occupied -最小安全净空 -最大车辆曲率 -路径位置连续性 -路径切线连续性 -换向结构 -目标进入方向 -``` - -若平滑失败: - -```text -局部重新调整 - ↓ -局部 QP 兜底 - ↓ -仍失败则回退原始可行粗路径 -``` - ---- - -## 5.6 输出 - -```text -SmoothedPathPoint: - X - Y - Heading - ArcLength - Direction - VehicleCurvature - GeometricCurvature - BodyClearance - Source -``` - -该输出仍然不包含: - -```text -速度 -加速度 -时间戳 -``` - ---- - -# 6. Module 4:SQP 轨迹优化 - -## 6.1 模块目标 - -以平滑空间路径作为参考线,通过 Frenet 坐标系和 SQP 优化,联合完成: - -- 横向几何调整; -- 曲率优化; -- 纵向速度规划; -- 加速度约束; -- 换向停车约束; -- 终点精确收敛; -- 时间参数化。 - -最终生成可供车辆控制器跟踪的带时间轨迹。 - ---- - -## 6.2 Frenet 坐标转换 - -将平滑路径作为参考线,建立 Frenet 坐标系: - -```text -s: - 沿参考线的累计弧长 - -l: - 相对于参考线的横向偏移 -``` - -转换后,将原二维路径问题分解为: - -```text -横向 l-s 问题 -纵向 s-t 问题 -``` - ---- - -## 6.3 横向 l-s 优化 - -主要优化变量: - -```text -横向偏移 l -横向一阶导数 dl/ds -横向二阶导数 d²l/ds² -曲率 -曲率变化 -``` - -优化目标: - -```text -尽量贴近参考线 -避开障碍物 -保持曲率平滑 -满足最大曲率 -满足安全走廊约束 -精确到达目标横向位置 -``` - ---- - -## 6.4 纵向 s-t 优化 - -主要优化变量: - -```text -s(t) -速度 v(t) -加速度 a(t) -加加速度 jerk(t) -``` - -优化目标: - -```text -满足速度上限 -满足加速度约束 -满足减速度约束 -提高运动平顺性 -减少总时间 -在换向点停车 -在终点停车 -``` - ---- - -## 6.5 SQP 联合优化 - -SQP 负责迭代求解: - -```text -路径位置 -航向 -曲率 -速度 -加速度 -角速度 -时间戳 -``` - -约束至少包括: - -```text -障碍物安全约束 -车辆曲率约束 -速度约束 -加速度约束 -角速度约束 -换向点速度为 0 -终点位置约束 -终点航向约束 -终点速度约束 -``` - -SQP 不负责: - -```text -重新决定绕障侧 -凭空新增换向 -改变 Hybrid A* 已确定的拓扑结构 -``` - ---- - -## 6.6 SQP 输出 - -```text -TrajectoryPoint: - X - Y - Heading - Time - ArcLength - Direction - Velocity - Acceleration - AngularVelocity - VehicleCurvature - GeometricCurvature -``` - -该轨迹可以直接提供给后续底盘控制与轨迹跟踪模块。 - ---- - -# 7. Module 5:轨迹输出与底盘接口 - -## 7.1 输出内容 - -```text -TrajectoryResult: - PlanningStatus - GoalReachType - PathProcessingResult - TrajectoryPoints - DirectionSegments - Diagnostics -``` - ---- - -## 7.2 底盘控制前的处理 - -轨迹规划器不直接输出四轮舵角和轮速。 - -底盘控制层负责: - -```text -轨迹点 - ↓ -车辆状态误差计算 - ↓ -轨迹跟踪控制 - ↓ -底盘运动学逆解 - ↓ -四轮舵角与轮速 -``` - ---- - -# 8. Agent 开发顺序 - -## 阶段 M0:数据结构与公共工具 - -实现: - -```text -Pose2D -VehicleParameters -GoalPose -GridMapData Adapter -角度归一化 -坐标变换 -单位转换 -``` - -完成标准: - -```text -单元测试通过 -毫米与米转换无误 -航向角定义统一 -``` - ---- - -## 阶段 M1:地图适配与碰撞检测 - -实现: - -```text -PlanningMapAdapter -PlanningGridMap -距离场 -旋转矩形碰撞检测 -地图边界检测 -``` - -完成标准: - -```text -AMR 自身不会出现在环境占据图中 -不同航向下完整车体碰撞检测正确 -``` - ---- - -## 阶段 M2:最小 Hybrid A* - -实现: - -```text -只前进 -无障碍 -恒曲率运动原语 -基础启发函数 -``` - -完成标准: - -```text -稳定从起点搜索到目标 -``` - ---- - -## 阶段 M3:障碍物、倒车和换向 - -实现: - -```text -Dijkstra 启发 -Forward / Reverse -换向代价 -方向分段 -``` - -完成标准: - -```text -单障碍绕行成功 -需要倒车的场景成功 -换向点正确输出 -``` - ---- - -## 阶段 M4:目标终止与 Reeds–Shepp - -实现: - -```text -目标位置容差 -目标航向容差 -目标进入方向 -Reeds–Shepp 三种模式 -``` - -完成标准: - -```text -普通目标和狭窄目标场景均能正确终止 -``` - ---- - -## 阶段 M5:路径后处理 - -实现: - -```text -父节点回溯 -内部积分点恢复 -关键点保留 -弧长重采样 -``` - -完成标准: - -```text -路径无断点 -换向结构明确 -方向段正确 -``` - ---- - -## 阶段 M6:平滑与可行性检查 - -实现: - -```text -B 样条 -贝塞尔曲线 -五次多项式 -完整碰撞复核 -失败回退 -``` - -完成标准: - -```text -路径曲率明显改善 -不改变原有拓扑 -平滑失败可回退 -``` - ---- - -## 阶段 M7:SQP 轨迹优化 - -实现: - -```text -Frenet 坐标转换 -l-s 优化 -s-t 优化 -速度和加速度约束 -换向停车约束 -终点精确约束 -``` - -完成标准: - -```text -输出带时间的可跟踪轨迹 -终点位置与航向满足要求 -换向点速度为 0 -``` - ---- - -## 阶段 M8:性能优化 - -在前述流程完整跑通后再开展: - -```text -性能剖析 -Open List 优化 -Closed Set 优化 -节点内存优化 -地图裁剪 -预计算运动原语 -预计算碰撞模板 -启发图复用 -路径热启动 -增量规划 -``` - ---- - -# 9. 核心技术边界 - -## Hybrid A* 负责 - -```text -拓扑 -绕障侧 -前进与倒车结构 -换向位置 -基本运动学可行性 -粗目标位姿接近 -``` - -## 路径平滑负责 - -```text -消除折线 -改善局部曲率 -保持拓扑不变 -输出连续空间参考路径 -``` - -## SQP 负责 - -```text -终点精确收敛 -速度规划 -加速度规划 -角速度规划 -时间参数化 -动态可跟踪性 -``` - -## 底盘控制负责 - -```text -四轮舵角 -四轮轮速 -轨迹跟踪 -执行器控制 -``` - ---- - -# 10. 最终技术路线总结 - -```text -起点位姿 + 目标位姿 + 车辆参数 + 障碍物数据 - ↓ -构建任务级环境占据地图 - ↓ -通过 PlanningMapAdapter 转换为规划地图 - ↓ -Hybrid A* 搜索满足运动学约束的粗路径 - ↓ -Reeds–Shepp 处理终点位姿连接 - ↓ -回溯、重采样和路径方向分段 - ↓ -B 样条 / 贝塞尔 / 五次多项式进行几何平滑 - ↓ -重新执行完整车体碰撞和曲率检查 - ↓ -建立 Frenet 参考坐标系 - ↓ -横向 l-s 优化路径形状和曲率 - ↓ -纵向 s-t 优化速度、加速度和停车决策 - ↓ -SQP 联合求解可跟踪轨迹 - ↓ -输出位置、航向、速度、加速度、角速度和时间戳 - ↓ -底盘控制器进行四轮舵角和轮速逆解 -``` - -该技术路线的核心原则是: - -> Hybrid A* 解决“怎么走”,路径平滑解决“走得是否顺”,SQP 解决“以什么速度和时间走”,底盘控制解决“四个舵轮和车轮如何执行”。 diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs index 813e584..3e910a6 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs @@ -159,6 +159,11 @@ internal sealed class LocalG2WindowPlanner return; } + // The tolerance above deliberately accepts boundary-sized round-off. Clamp that + // round-off before constructing the strict, non-negative window contract. + left = Math.Min(left, availableLeft); + right = Math.Min(right, availableRight); + double start = anchor - left; double end = anchor + right; if (start > firstEvent + MergeToleranceMeters || end + MergeToleranceMeters < lastEvent || end - start + MergeToleranceMeters < target) diff --git a/ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs b/ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs index 98f0ac3..434d8cd 100644 --- a/ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs @@ -58,6 +58,12 @@ public sealed class EmPlanningCoordinator /// 调用方取消令牌;会与本周期内部令牌链接。 /// 完成后给出不可变 ;只有版本和身份仍当前且结果成功完整时其 Published public Task PlanLatestAsync(PlanningCycleInput input, CancellationToken cancellationToken) + { + return PlanLatestAsync(input, cancellationToken, null); + } + + internal Task PlanLatestAsync(PlanningCycleInput input, + CancellationToken cancellationToken, Action publicationCommit) { if (input == null) throw new ArgumentNullException(nameof(input)); @@ -75,7 +81,8 @@ public sealed class EmPlanningCoordinator replanPeriodSeconds = input.ReplanPeriodSeconds; } - return Task.Run(() => CompleteCycle(version, input, cycleCancellation)); + return Task.Run(() => CompleteCycle(version, input, cycleCancellation, cancellationToken, + publicationCommit)); } /// 从协调器当前已发布的不可变轨迹构建下一周期的安全交接决策。 @@ -100,7 +107,8 @@ public sealed class EmPlanningCoordinator /// 本周期与调用方取消状态链接的令牌源。 /// 包含原始或 Superseded 规划结果、发布标记和观察器诊断的周期记录。 private PlanningCycleResult CompleteCycle(long version, PlanningCycleInput input, - CancellationTokenSource cycleCancellation) + CancellationTokenSource cycleCancellation, CancellationToken callerCancellationToken, + Action publicationCommit) { EmPlanningResult planned; try @@ -130,11 +138,57 @@ public sealed class EmPlanningCoordinator } else { - bool publishable = (planned.Status == EmPlanningStatus.Success || planned.Status == EmPlanningStatus.SuccessWithFallback) && - planned.Trajectory != null; + bool publishable = (planned.Status == EmPlanningStatus.Success || + planned.Status == EmPlanningStatus.SuccessWithFallback) && planned.Trajectory != null; if (publishable) - publishedTrajectory = planned.Trajectory; - result = new PlanningCycleResult(version, input.Identity, planned, publishable, planned.FailureReason); + { + EmPlanningPublicationDecision decision = input.Request.TryAuthorizePublication( + callerCancellationToken, () => + { + publicationCommit?.Invoke(planned.Trajectory); + publishedTrajectory = planned.Trajectory; + }); + if (decision == EmPlanningPublicationDecision.CallerCancelled) + { + var cancelled = new EmPlanningResult(EmPlanningStatus.Cancelled, null, + "callerCancellation=true;phase=publication"); + result = new PlanningCycleResult(version, input.Identity, cancelled, false, + cancelled.FailureReason); + } + else if (decision == EmPlanningPublicationDecision.DeadlineExpired) + { + var expired = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null, + "cycleDeadlineExpired=true;phase=publication;remainingMs=0.000"); + result = new PlanningCycleResult(version, input.Identity, expired, false, + expired.FailureReason); + } + else + { + result = new PlanningCycleResult(version, input.Identity, planned, true, + planned.FailureReason); + } + } + else if (input.Request.CallerCancellationToken.IsCancellationRequested || + callerCancellationToken.IsCancellationRequested) + { + var cancelled = new EmPlanningResult(EmPlanningStatus.Cancelled, null, + "callerCancellation=true;phase=publication"); + result = new PlanningCycleResult(version, input.Identity, cancelled, false, + cancelled.FailureReason); + } + else if (input.Request.CycleDeadlineToken.IsCancellationRequested || + input.Request.IsCycleDeadlineExpired()) + { + var expired = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null, + "cycleDeadlineExpired=true;phase=publication;remainingMs=0.000"); + result = new PlanningCycleResult(version, input.Identity, expired, false, + expired.FailureReason); + } + else + { + result = new PlanningCycleResult(version, input.Identity, planned, false, + planned.FailureReason); + } } } diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/trajectory-planning-flow-demo.html similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html rename to ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/trajectory-planning-flow-demo.html diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/ControlModuleTrajectoryAdapter.cs b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/ControlModuleTrajectoryAdapter.cs deleted file mode 100644 index eafe00a..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/ControlModuleTrajectoryAdapter.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.EMPlanner; - -namespace TrajectoryOutputDemo; - -/// 供外部控制模块消费的单个只读轨迹点;全部字段直接来自已验证的 -public sealed class ControlTrajectoryPoint -{ - internal ControlTrajectoryPoint(EmTrajectoryPoint source) - { - TimeFromStartSeconds = source.TimeFromStart; - XMeters = source.X; - YMeters = source.Y; - YawRadians = source.Yaw; - SignedLongitudinalVelocityMetersPerSecond = source.SignedLongitudinalVelocity; - YawRateRadiansPerSecond = source.YawRate; - CurvaturePerMeter = source.VehicleCurvature; - Direction = source.Direction; - SegmentIndex = source.SegmentIndex; - PathSMeters = source.PathS; - BoundaryType = source.BoundaryType; - } - - public double TimeFromStartSeconds { get; } - public double XMeters { get; } - public double YMeters { get; } - public double YawRadians { get; } - public double SignedLongitudinalVelocityMetersPerSecond { get; } - public double YawRateRadiansPerSecond { get; } - public double CurvaturePerMeter { get; } - public TravelDirection Direction { get; } - public int SegmentIndex { get; } - public double PathSMeters { get; } - public EmBoundaryType BoundaryType { get; } -} - -/// 轨迹元数据与控制点序列的不可变组合;它不包含也不发送任何硬件命令。 -public sealed class ControlTrajectorySequence -{ - internal ControlTrajectorySequence(EmTrajectoryMetadata metadata, IReadOnlyList points) - { - Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); - Points = points ?? throw new ArgumentNullException(nameof(points)); - } - - public EmTrajectoryMetadata Metadata { get; } - public IReadOnlyList Points { get; } -} - -/// 将不可变 EM 轨迹投影为控制模块可引用的只读序列,不进行采样、插值或底盘协议转换。 -public sealed class ControlModuleTrajectoryAdapter -{ - /// 逐点复制公开控制字段;调用方只能在完整非空轨迹上调用此方法。 - public ControlTrajectorySequence Create(EmTrajectory trajectory) - { - if (trajectory == null || trajectory.Points == null || trajectory.Points.Count == 0) - throw new ArgumentException("必须提供完整非空的 EM 轨迹。", nameof(trajectory)); - - var points = new List(trajectory.Points.Count); - for (int index = 0; index < trajectory.Points.Count; index++) - points.Add(new ControlTrajectoryPoint(trajectory.Points[index])); - return new ControlTrajectorySequence(trajectory.Metadata, - new ReadOnlyCollection(points)); - } -} diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Program.cs b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Program.cs deleted file mode 100644 index 2fd1bba..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; - -namespace TrajectoryOutputDemo; - -/// Trajplanner_output 控制台入口;只输出规划诊断、轨迹摘要和 CSV 路径。 -internal static class Program -{ - private static int Main(string[] args) - { - if (args.Length != 0) - { - Console.Error.WriteLine("用法:dotnet run --project TrajectoryOutputDemo.csproj"); - return 2; - } - - string csvPath = Path.Combine(AppContext.BaseDirectory, "output", "trajectory.csv"); - try - { - TrajectoryOutputDemoResult result = new TrajectoryOutputDemoRunner().Run( - TrajectoryOutputDemoConfiguration.CreateDefault(csvPath)); - if (!result.Succeeded || result.Trajectory == null || result.ControlTrajectory == null || result.CsvPath == null) - { - Console.Error.WriteLine(result.Diagnostic); - return 1; - } - - ControlTrajectoryPoint first = result.ControlTrajectory.Points[0]; - ControlTrajectoryPoint last = result.ControlTrajectory.Points[result.ControlTrajectory.Points.Count - 1]; - Console.WriteLine("轨迹 ID:" + result.Trajectory.Metadata.TrajectoryId); - Console.WriteLine("生效时间:" + result.Trajectory.Metadata.EffectiveAtUtc.ToString("O")); - Console.WriteLine("方向段:" + result.Trajectory.Metadata.SegmentIndex + ",终端类型:" + result.Trajectory.Metadata.TerminalType); - Console.WriteLine("轨迹点数:" + result.ControlTrajectory.Points.Count); - Console.WriteLine("首点:t=" + first.TimeFromStartSeconds + "s, X=" + first.XMeters + "m, Y=" + first.YMeters + "m"); - Console.WriteLine("末点:t=" + last.TimeFromStartSeconds + "s, X=" + last.XMeters + "m, Y=" + last.YMeters + "m"); - Console.WriteLine("CSV:" + result.CsvPath); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine("轨迹 Demo 运行异常:" + exception.Message); - return 1; - } - } -} diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md deleted file mode 100644 index efdd6b7..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md +++ /dev/null @@ -1,164 +0,0 @@ -# Trajplanner_output 真实轨迹序列 Demo - -`Trajplanner_output` 是面向学习、调参和控制模块对接的独立控制台示例。它在一个明确允许的空地图演示场景中依次执行粗路径规划、Local G2 平滑和真实 OSQP EM 规划,最终得到不可变 `EmTrajectory`,导出 CSV,并投影为控制模块可读取的只读轨迹序列。 - -它不读取真实定位、传感器或底盘状态,不驱动、转向、制动或换向车辆。演示空地图只用于理解接口与算法链路,不能替代真实作业地图。 - -## 模块说明(Module Overview) - -| 模块 | 负责内容 | 不负责内容 | -| --- | --- | --- | -| `TrajectoryOutputDemoConfiguration` | 集中保存地图、起终点、车辆、曲率、初速和输出路径 | 运行时读取 UI 或硬件参数 | -| `TrajectoryOutputDemoRunner` | 串联 CoarsePath、PathSmoothing 与真实 OSQP EM 规划 | 发布硬件命令或伪造失败轨迹 | -| `ControlModuleTrajectoryAdapter` | 将 `EmTrajectory` 转为只读控制序列 | 插值、采样或底盘协议转换 | -| `TrajectorySequenceExporter` | 原子导出稳定字段顺序的 UTF-8 CSV | 将 CSV 当作车辆命令发送 | -| `Program` | 打印轨迹摘要、诊断和 CSV 路径 | 逐行打印轨迹或修改规划结果 | - -唯一的轨迹生成入口是: - -```csharp -TrajectoryOutputDemoResult result = - new TrajectoryOutputDemoRunner().Run(configuration); -``` - -## 文件结构(File Structure) - -```text -Trajplanner_output/ -├── TrajectoryOutputDemo.csproj # 独立 net10.0-windows 控制台项目 -├── Program.cs # 运行入口和轨迹摘要 -├── TrajectoryOutputDemoConfiguration.cs # 唯一调参位置 -├── TrajectoryOutputDemoRunner.cs # 粗路径、平滑和真实 EM 规划编排 -├── ControlModuleTrajectoryAdapter.cs # EM 轨迹到控制只读序列的映射 -├── TrajectorySequenceExporter.cs # 原子 CSV 导出 -├── README.md # 本说明 -└── Tests/ - ├── TrajectoryOutputDemo.Tests.csproj # 真实 OSQP 自检项目 - └── Program.cs # CSV 和逐点映射契约验证 -``` - -## 轨迹数据流(Trajectory Data Flow) - -```text -TrajectoryOutputDemoConfiguration - ▼ -PlanningMapRequest(显式允许的空地图) - ▼ -CoarsePathPlanningService - ▼ -PathSmoothingService(Local G2) - ▼ -EmPlanningService(new OsqpNativeSolver()) - ▼ -EmPlanningResult - │ 仅 Success / SuccessWithFallback 且 Trajectory 非空 - ▼ -EmTrajectory.Points + Metadata - ├── ControlModuleTrajectoryAdapter - │ └── ControlTrajectorySequence - └── TrajectorySequenceExporter - └── output/trajectory.csv -``` - -中间任一阶段失败都会立刻停止,打印状态与诊断,且不会导出部分或伪造的 CSV。 - -## 全链路可视化(Offline Flow Demo) - -双击打开 [trajectory-planning-flow-demo.html](trajectory-planning-flow-demo.html),可在同一米制地图中按阶段查看地图、Hybrid A* 粗路径、Local G2 平滑路径、`EmTrajectory` 和控制层采样命令。该页面使用内置说明数据,帮助理解版本身份与数据边界;它不调用 OSQP、定位、硬件或真实控制器。 - -控制器只消费已验证完整轨迹的采样结果或执行层导出的 `TrajectoryControlCommand`。粗路径、平滑路径、地图和规划诊断属于观测、回放和问题定位证据链,不是逐周期的底盘命令。 - -## 运行(Run) - -在仓库根目录执行: - -```powershell -dotnet run --project ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj -``` - -成功时控制台会输出轨迹 ID、生效时间、方向段、终端类型、轨迹点数量、首末点和 CSV 绝对路径。默认 CSV 位于 Demo 程序输出目录下的 `output/trajectory.csv`。 - -运行自检: - -```powershell -dotnet run --project ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/TrajectoryOutputDemo.Tests.csproj -``` - -自检使用真实 OSQP,不使用假求解器;它验证成功轨迹存在、CSV 存在、CSV 表头稳定,且控制序列点数与 `EmTrajectory.Points` 相同。 - -## 调参(Configuration) - -所有 Demo 参数集中在 `TrajectoryOutputDemoConfiguration.CreateDefault(csvOutputPath)`: - -| 参数 | 单位 | 默认值 | 作用 | -| --- | --- | ---: | --- | -| `MapBounds` | mm | `0..6000 × 0..4000` | 演示地图范围 | -| `MapResolutionMillimeters` | mm | `50` | 占据栅格分辨率 | -| `Start` / `Goal` | m, rad | `(1,1,0)` / `(3,1,0)` | 车辆几何中心位姿 | -| `VehicleLengthMeters` / `VehicleWidthMeters` | m | `0.80 / 0.60` | 车辆矩形尺寸 | -| `SafetyMarginMeters` | m | `0.05` | 车辆外扩安全余量 | -| `MaximumCurvaturePerMeter` | 1/m | `1 / 1.20` | 最大允许曲率 | -| `InitialSignedSpeedMetersPerSecond` | m/s | `0` | 初始带符号纵向速度 | -| `CsvOutputPath` | 文件路径 | 运行时指定 | 完整 CSV 输出位置 | - -调整参数后应重新运行自检。若更换为真实作业场景,必须将 `ObstacleSources` 替换为有效地图来源,并取消演示空地图策略。 - -## CSV 契约(CSV Contract) - -首行固定为: - -```text -time_s,x_m,y_m,yaw_rad,signed_velocity_mps,yaw_rate_radps,curvature_per_m,direction,segment_index,path_s_m,boundary_type -``` - -每一行与一个不可变 `EmTrajectoryPoint` 一一对应。CSV 使用 UTF-8 无 BOM 和不受系统区域设置影响的小数点格式;导出过程先写临时文件,再替换最终文件,避免读取方获得半写入内容。 - -| 字段 | 单位 / 语义 | -| --- | --- | -| `time_s` | 自轨迹生效时刻起的秒数,严格递增 | -| `x_m`, `y_m`, `yaw_rad` | 世界位置和航向 | -| `signed_velocity_mps` | 带符号纵向速度;前进为正、倒车为负 | -| `yaw_rate_radps` | 世界航向角速度,不是转向角 | -| `curvature_per_m` | 车辆曲率 | -| `direction`, `segment_index`, `boundary_type` | 方向段与终端边界语义 | - -## 控制模块对接(Control Module Integration) - -控制模块优先以项目引用依赖规划库: - -```xml - - - -``` - -对接方应接收上层规划服务提供的完整 `EmTrajectory`,再使用适配器读取不可变序列: - -```csharp -var sequence = new ControlModuleTrajectoryAdapter().Create(trajectory); - -foreach (ControlTrajectoryPoint point in sequence.Points) -{ - SendReference( - point.TimeFromStartSeconds, - point.XMeters, - point.YMeters, - point.YawRadians, - point.SignedLongitudinalVelocityMetersPerSecond, - point.YawRateRadiansPerSecond); -} -``` - -`SendReference` 是控制模块自己的协议适配函数,不是本项目 API。控制模块必须保留 `sequence.Metadata`,并依据方向、换向边界和生效时间实施自己的安全策略。不要把 `VelocityX`、`VelocityY` 当作底盘命令;本 Demo 也不实现换向确认、制动和硬件通信。 - -## 失败与限制(Failures and Limits) - -| 现象 | 原因 | 处理 | -| --- | --- | --- | -| 没有 CSV 输出 | 粗路径、平滑、OSQP 或 EM 验证未成功 | 阅读控制台诊断;不能将失败当作部分轨迹 | -| OSQP 加载失败 | `osqp.dll` 未随运行输出部署,或 Windows x64 运行时不匹配 | 使用项目引用构建,检查输出目录的 OSQP 文件 | -| 控制模块轨迹跳变 | 忽略轨迹 ID、生效时间或方向段 | 持有 `EmTrajectoryMetadata` 并按控制周期安全接管 | -| 想用于真实车辆 | Demo 仍使用显式空地图 | 先接入真实障碍物来源、状态快照和硬件安全审查 | - -本项目是“如何得到并交给他人轨迹序列”的学习/接口示例,不是经过现场认证的车辆控制器。 diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/Program.cs b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/Program.cs deleted file mode 100644 index f225b67..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; -using TrajectoryOutputDemo; - -namespace TrajectoryOutputDemoTests; - -internal static class Program -{ - private static int Main() - { - string temporaryDirectory = Path.Combine(Path.GetTempPath(), "trajectory-output-demo-tests", Guid.NewGuid().ToString("N")); - try - { - Directory.CreateDirectory(temporaryDirectory); - string csvPath = Path.Combine(temporaryDirectory, "trajectory.csv"); - var configuration = TrajectoryOutputDemoConfiguration.CreateDefault(csvPath); - TrajectoryOutputDemoResult result = new TrajectoryOutputDemoRunner().Run(configuration); - - if (!result.Succeeded) - throw new InvalidOperationException("真实 OSQP 规划未成功:" + result.Diagnostic); - if (!File.Exists(csvPath)) - throw new InvalidOperationException("成功规划必须导出 CSV。"); - ControlTrajectorySequence sequence = result.ControlTrajectory ?? - throw new InvalidOperationException("成功规划必须提供控制模块序列。"); - var trajectory = result.Trajectory ?? - throw new InvalidOperationException("成功规划必须提供 EM 轨迹。"); - if (sequence.Points.Count != trajectory.Points.Count) - throw new InvalidOperationException("控制序列必须逐点对应 EM 轨迹。"); - - string[] lines = File.ReadAllLines(csvPath); - if (lines.Length < 2) - throw new InvalidOperationException("CSV 必须包含表头和至少一个轨迹点。"); - if (lines[0] != "time_s,x_m,y_m,yaw_rad,signed_velocity_mps,yaw_rate_radps,curvature_per_m,direction,segment_index,path_s_m,boundary_type") - throw new InvalidOperationException("CSV 表头不符合控制模块契约。"); - - Console.WriteLine("PASS trajectory-output-demo"); - return 0; - } - finally - { - if (Directory.Exists(temporaryDirectory)) - Directory.Delete(temporaryDirectory, true); - } - } -} diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/TrajectoryOutputDemo.Tests.csproj b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/TrajectoryOutputDemo.Tests.csproj deleted file mode 100644 index aaa8470..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/TrajectoryOutputDemo.Tests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - Exe - net10.0-windows - disable - enable - - - - - diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj deleted file mode 100644 index 7a68944..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - Exe - net10.0-windows - disable - enable - - - - - - - - diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoConfiguration.cs deleted file mode 100644 index 6cae332..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoConfiguration.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace TrajectoryOutputDemo; - -/// -/// Trajplanner_output 的唯一演示调参入口。 -/// 地图边界与分辨率使用 mm;车辆、起终点位置和安全余量使用 m;航向使用 rad;输出路径为 CSV 文件位置。 -/// -public sealed class TrajectoryOutputDemoConfiguration -{ - /// 演示地图的世界边界;空地图只允许用于算法学习,不能替代真实障碍物来源。 - public MapBoundsMm MapBounds { get; init; } = new MapBoundsMm(0f, 6000f, 0f, 4000f); - - /// 演示占据栅格分辨率,单位 mm。 - public float MapResolutionMillimeters { get; init; } - - /// 车辆几何中心起点,位置单位 m、航向单位 rad。 - public Pose2D Start { get; init; } = new Pose2D(1d, 1d, 0d); - - /// 车辆几何中心目标点,位置单位 m、航向单位 rad。 - public Pose2D Goal { get; init; } = new Pose2D(3d, 1d, 0d); - - /// 车辆长度,单位 m。 - public double VehicleLengthMeters { get; init; } - - /// 车辆宽度,单位 m。 - public double VehicleWidthMeters { get; init; } - - /// 车辆矩形外的附加安全余量,单位 m。 - public double SafetyMarginMeters { get; init; } - - /// 车辆最大曲率,单位 1/m。 - public double MaximumCurvaturePerMeter { get; init; } - - /// 轨迹开始时的带符号纵向速度,单位 m/s;前进为正、倒车为负。 - public double InitialSignedSpeedMetersPerSecond { get; init; } - - /// 输出 CSV 的绝对或相对路径。 - public string CsvOutputPath { get; init; } = string.Empty; - - /// 创建可直线通行的最小真实 EM 演示配置。 - public static TrajectoryOutputDemoConfiguration CreateDefault(string csvOutputPath) - { - if (string.IsNullOrWhiteSpace(csvOutputPath)) - throw new ArgumentException("CSV 输出路径不能为空。", nameof(csvOutputPath)); - - return new TrajectoryOutputDemoConfiguration - { - MapBounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), - MapResolutionMillimeters = 50f, - Start = new Pose2D(1d, 1d, 0d), - Goal = new Pose2D(3d, 1d, 0d), - VehicleLengthMeters = 0.80d, - VehicleWidthMeters = 0.60d, - SafetyMarginMeters = 0.05d, - MaximumCurvaturePerMeter = 1d / 1.20d, - InitialSignedSpeedMetersPerSecond = 0d, - CsvOutputPath = csvOutputPath, - }; - } -} diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoRunner.cs b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoRunner.cs deleted file mode 100644 index 6fae1d3..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoRunner.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.EMPlanner; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; - -namespace TrajectoryOutputDemo; - -/// 一次 Demo 运行的结果;成功时同时提供原始 EM 轨迹、控制只读序列和已完成的 CSV 路径。 -public sealed class TrajectoryOutputDemoResult -{ - private TrajectoryOutputDemoResult(bool succeeded, string diagnostic, EmTrajectory? trajectory, - ControlTrajectorySequence? controlTrajectory, string? csvPath) - { - Succeeded = succeeded; - Diagnostic = diagnostic ?? string.Empty; - Trajectory = trajectory; - ControlTrajectory = controlTrajectory; - CsvPath = csvPath; - } - - public bool Succeeded { get; } - public string Diagnostic { get; } - public EmTrajectory? Trajectory { get; } - public ControlTrajectorySequence? ControlTrajectory { get; } - public string? CsvPath { get; } - - internal static TrajectoryOutputDemoResult Failure(string diagnostic) => new(false, diagnostic, null, null, null); - internal static TrajectoryOutputDemoResult Success(EmTrajectory trajectory, ControlTrajectorySequence sequence, string csvPath) => - new(true, string.Empty, trajectory, sequence, csvPath); -} - -/// -/// 编排一次真实轨迹输出:粗路径、Local G2 平滑、真实 OSQP EM 规划、控制序列投影和 CSV 导出。 -/// 它只处理冻结的演示输入,绝不读取硬件状态或发送控制命令。 -/// -public sealed class TrajectoryOutputDemoRunner -{ - /// 在配置定义的演示场景运行一次完整规划;失败时不导出任何轨迹文件。 - public TrajectoryOutputDemoResult Run(TrajectoryOutputDemoConfiguration configuration) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - CoarsePathPlanningJob job = CreateCoarseJob(configuration); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper().Bootstrap(job, CancellationToken.None); - if (!bootstrap.Succeeded) - return TrajectoryOutputDemoResult.Failure("粗路径或平滑阶段失败:" + bootstrap.FailureReason); - if (bootstrap.Segments.Count == 0) - return TrajectoryOutputDemoResult.Failure("平滑路径没有可供 EM 规划的方向段。"); - - DateTimeOffset now = DateTimeOffset.UtcNow; - DirectionSegmentView segment = bootstrap.Segments[0]; - var state = new VehicleMotionState(configuration.Start, configuration.InitialSignedSpeedMetersPerSecond, - 0d, now, 1L); - var request = new EmPlanningRequest(bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, - EmPlannerConfiguration.CreateDefault(), segment.SegmentIndex, null, now, now, - "trajectory-output-demo-" + now.ToUnixTimeMilliseconds(), "trajectory-output-demo-reference", string.Empty, - EmMotionModel.NonholonomicForwardReverse, EmPlanningScope.FullDirectionSegment); - EmPlanningResult result = new EmPlanningService(new OsqpNativeSolver()).Plan(request, CancellationToken.None); - bool accepted = (result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback) && - result.Trajectory != null && result.Trajectory.Points.Count > 0; - if (!accepted) - return TrajectoryOutputDemoResult.Failure("EM 规划失败:" + result.Status + ";" + result.FailureReason); - - EmTrajectory trajectory = result.Trajectory!; - ControlTrajectorySequence sequence = new ControlModuleTrajectoryAdapter().Create(trajectory); - string csvPath = new TrajectorySequenceExporter().Export(sequence, configuration.CsvOutputPath); - return TrajectoryOutputDemoResult.Success(trajectory, sequence, csvPath); - } - - private static CoarsePathPlanningJob CreateCoarseJob(TrajectoryOutputDemoConfiguration configuration) - { - return new CoarsePathPlanningJob - { - MapRequest = new PlanningMapRequest - { - Bounds = configuration.MapBounds, - ResolutionMm = configuration.MapResolutionMillimeters, - ObstacleSources = Array.Empty(), - AllowExplicitEmptyMap = true, - }, - Start = configuration.Start, - Goal = configuration.Goal, - Vehicle = new VehicleParameters - { - LengthMeters = configuration.VehicleLengthMeters, - WidthMeters = configuration.VehicleWidthMeters, - SafetyMarginMeters = configuration.SafetyMarginMeters, - MaximumCurvaturePerMeter = configuration.MaximumCurvaturePerMeter, - }, - Configuration = new HybridAStarConfiguration(), - StartDirection = TravelDirection.Forward, - GoalDirection = GoalDirectionConstraint.Forward, - }; - } -} diff --git a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectorySequenceExporter.cs b/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectorySequenceExporter.cs deleted file mode 100644 index 599e307..0000000 --- a/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectorySequenceExporter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; - -namespace TrajectoryOutputDemo; - -/// 将完整控制轨迹以稳定字段顺序导出为 UTF-8 CSV;只在完整内容写完后替换目标文件。 -public sealed class TrajectorySequenceExporter -{ - /// 控制模块 CSV 的固定表头;数值字段均按不受区域设置影响的点号格式写出。 - public const string Header = "time_s,x_m,y_m,yaw_rad,signed_velocity_mps,yaw_rate_radps,curvature_per_m,direction,segment_index,path_s_m,boundary_type"; - - /// 将完整序列原子写入目标路径,防止读方看到半写入的 CSV。 - public string Export(ControlTrajectorySequence sequence, string outputPath) - { - if (sequence == null) throw new ArgumentNullException(nameof(sequence)); - if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("输出路径不能为空。", nameof(outputPath)); - - string fullPath = Path.GetFullPath(outputPath); - string? directory = Path.GetDirectoryName(fullPath); - if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); - string temporaryPath = fullPath + ".tmp"; - - using (var writer = new StreamWriter(temporaryPath, false, new UTF8Encoding(false))) - { - writer.WriteLine(Header); - for (int index = 0; index < sequence.Points.Count; index++) - { - ControlTrajectoryPoint point = sequence.Points[index]; - writer.WriteLine(string.Join(",", new[] - { - Number(point.TimeFromStartSeconds), Number(point.XMeters), Number(point.YMeters), Number(point.YawRadians), - Number(point.SignedLongitudinalVelocityMetersPerSecond), Number(point.YawRateRadiansPerSecond), - Number(point.CurvaturePerMeter), point.Direction.ToString(), point.SegmentIndex.ToString(CultureInfo.InvariantCulture), - Number(point.PathSMeters), point.BoundaryType.ToString(), - })); - } - } - - File.Move(temporaryPath, fullPath, true); - return fullPath; - } - - private static string Number(double value) => value.ToString("G17", CultureInfo.InvariantCulture); -} diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs new file mode 100644 index 0000000..cff9cd8 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.Trajectory; +using MultiWheelC.TrajectoryPlanning.EMPlanner; +using MyParking.Shared; + +namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; + +/// 将冻结的单方向段 EM 轨迹转换为几何控制器使用的 SI 单位有符号速度轨迹。 +public sealed class EmControlTrajectoryAdapter +{ + private const double MinimumSegmentLengthMeters = 1e-6d; + + /// 按世界位置重建从零开始的弧长,同时保留航向、曲率以及前进为正倒车为负的参考速度。 + public Trajectory2D Create(EmTrajectory trajectory) + { + if (trajectory == null) + throw new ArgumentNullException(nameof(trajectory)); + + var points = new List(trajectory.Points.Count); + double arcLengthMeters = 0d; + for (int index = 0; index < trajectory.Points.Count; index++) + { + EmTrajectoryPoint source = trajectory.Points[index]; + var pose = new Pose2D(source.X, source.Y, source.Yaw); + var converted = new TrajectoryPoint(arcLengthMeters, pose, + source.VehicleCurvature, source.SignedLongitudinalVelocity); + + if (points.Count == 0) + { + points.Add(converted); + continue; + } + + TrajectoryPoint previous = points[points.Count - 1]; + double deltaX = pose.XMeters - previous.PoseInWorld.XMeters; + double deltaY = pose.YMeters - previous.PoseInWorld.YMeters; + double distanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + if (distanceMeters < MinimumSegmentLengthMeters) + { + points[points.Count - 1] = new TrajectoryPoint( + previous.ArcLengthMeters, pose, source.VehicleCurvature, + source.SignedLongitudinalVelocity); + continue; + } + + arcLengthMeters += distanceMeters; + points.Add(new TrajectoryPoint(arcLengthMeters, pose, + source.VehicleCurvature, source.SignedLongitudinalVelocity)); + } + + if (points.Count < 2) + { + throw new ArgumentException( + "EM轨迹至少需要包含两个不同位置的有效控制点。", nameof(trajectory)); + } + + return new Trajectory2D(points); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs new file mode 100644 index 0000000..ee53e56 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs @@ -0,0 +1,697 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using ClumsyCore; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using CommonUsage.Chassis; +using MDCSToolBox.Clumsy.Pilot; +using MultiWheelC.Trajectory; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.EMPlanner; +using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +using MyParking.Shared; +using TrajectoryPlanningVisualization; +using PlanningPose2D = MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D; + +namespace MultiWheelC; + +/// 规划一次并冻结首个 EM 方向段,再使用现有几何控制器完成实车状态反馈闭环。 +[MovementTest(name = "EM闭环测试")] +public sealed class EmClosedLoopMovementTest : MovementTest +{ + private const int MaximumManualObstacleCount = 20; + private static long nextObstacleSnapshotVersion; + + public double GoalXmm = double.NaN; + public double GoalYmm = double.NaN; + public double GoalYawDeg; + public double MapPaddingMeters = 2d; + public float MapResolutionMm = 50f; + public double SolverTimeoutSeconds = 5d; + public int MaximumOsqpIterations = 100000; + public double OutputTimeStepSeconds = 0.10d; + public double VehicleLengthMeters = 0.80d; + public double VehicleWidthMeters = 0.60d; + public double SafetyMarginMeters = 0.05d; + public double MaximumCurvaturePerMeter = 1d / 1.20d; + public bool EnableWebVisualization = true; + public bool AutoOpenWebVisualization = true; + public int WebVisualizationPort; + public double WebRefreshRateHz = 10d; + public int VisualizationHistoryCycleLimit = 60; + public bool EnableNativePainterVisualization = true; + public double MaximumCommandSpeedMetersPerSecond = 1.00d; + public double MaximumDistanceToTrajectoryMeters = 0.30d; + public double ExecutionTimeoutSeconds = 120d; + public float WheelAlignmentToleranceDegrees = 2f; + + /// 读取冻结输入并启动一次规划、一次控制器接管的后台会话。 + public override void Test() + { + try + { + double goalXMillimeters = GoalXmm; + double goalYMillimeters = GoalYmm; + double goalYawDegrees = GoalYawDeg; + if (!IsFinite(goalXMillimeters) || !IsFinite(goalYMillimeters)) + { + goalXMillimeters = ReadFiniteInput("EM闭环终点 X(世界 mm)"); + goalYMillimeters = ReadFiniteInput("EM闭环终点 Y(世界 mm)"); + goalYawDegrees = ReadFiniteInput("EM闭环终点航向(世界 deg)"); + } + + EnsureFinite(goalXMillimeters, nameof(GoalXmm)); + EnsureFinite(goalYMillimeters, nameof(GoalYmm)); + EnsureFinite(goalYawDegrees, nameof(GoalYawDeg)); + EnsurePositive(MaximumCommandSpeedMetersPerSecond, + nameof(MaximumCommandSpeedMetersPerSecond)); + EnsurePositive(MaximumDistanceToTrajectoryMeters, + nameof(MaximumDistanceToTrajectoryMeters)); + EnsurePositive(ExecutionTimeoutSeconds, nameof(ExecutionTimeoutSeconds)); + EnsurePositive(WheelAlignmentToleranceDegrees, + nameof(WheelAlignmentToleranceDegrees)); + + var settings = new TrajectoryObservationSettings + { + PlanningScope = EmPlanningScope.FullDirectionSegment, + MapPaddingMeters = MapPaddingMeters, + MapResolutionMillimeters = MapResolutionMm, + SolverTimeoutSeconds = SolverTimeoutSeconds, + MaximumOsqpIterations = MaximumOsqpIterations, + OutputTimeStepSeconds = OutputTimeStepSeconds, + VehicleLengthMeters = VehicleLengthMeters, + VehicleWidthMeters = VehicleWidthMeters, + SafetyMarginMeters = SafetyMarginMeters, + MaximumCurvaturePerMeter = MaximumCurvaturePerMeter, + EnableWebVisualization = EnableWebVisualization, + AutoOpenWebVisualization = AutoOpenWebVisualization, + WebVisualizationPort = WebVisualizationPort, + WebRefreshRateHz = WebRefreshRateHz, + VisualizationHistoryCycleLimit = VisualizationHistoryCycleLimit, + EnableNativePainterVisualization = EnableNativePainterVisualization, + }.CreateValidatedSnapshot(); + + IReadOnlyList obstacles = ReadManualObstacles(); + long obstacleSnapshotVersion = obstacles.Count == 0 + ? 0L + : Interlocked.Increment(ref nextObstacleSnapshotVersion); + var goal = new PlanningPose2D( + goalXMillimeters / 1000d, + goalYMillimeters / 1000d, + goalYawDegrees * Math.PI / 180d); + + EmClosedLoopMovementTestRunner.Start(goal, settings, obstacles, + obstacleSnapshotVersion, MaximumCommandSpeedMetersPerSecond, + MaximumDistanceToTrajectoryMeters, ExecutionTimeoutSeconds, + WheelAlignmentToleranceDegrees); + } + catch (Exception exception) + { + EmClosedLoopMovementTestRunner.ShowFailure("测试未启动", exception); + } + } + + /// 取消尚未完成的规划并停止正在运行的控制任务。 + public override void TestStop() + { + EmClosedLoopMovementTestRunner.Stop(); + } + + private static IReadOnlyList ReadManualObstacles() + { + int count = ReadBoundedIntegerInput("EM闭环手动障碍物数量(0-20)", + 0, MaximumManualObstacleCount); + var obstacles = new List(count); + for (int index = 0; index < count; index++) + { + string label = "障碍物 " + (index + 1).ToString(CultureInfo.InvariantCulture); + int kind = ReadBoundedIntegerInput(label + " 类型(1=圆形,2=轴对齐矩形)", 1, 2); + double centerXMillimeters = ReadFiniteInput(label + " 中心 X(世界 mm)"); + double centerYMillimeters = ReadFiniteInput(label + " 中心 Y(世界 mm)"); + if (kind == 1) + { + double radiusMillimeters = ReadPositiveFiniteInput(label + " 半径(mm)"); + obstacles.Add(TrajectoryObservationObstacle.Circle( + centerXMillimeters, centerYMillimeters, radiusMillimeters)); + } + else + { + double lengthXMillimeters = ReadPositiveFiniteInput(label + " X 方向长度(mm)"); + double widthYMillimeters = ReadPositiveFiniteInput(label + " Y 方向宽度(mm)"); + obstacles.Add(TrajectoryObservationObstacle.Rectangle( + centerXMillimeters - lengthXMillimeters / 2d, + centerXMillimeters + lengthXMillimeters / 2d, + centerYMillimeters - widthYMillimeters / 2d, + centerYMillimeters + widthYMillimeters / 2d)); + } + } + + return obstacles; + } + + private static int ReadBoundedIntegerInput(string prompt, int minimum, int maximum) + { + object raw = UI.GetInput(prompt); + string text = Convert.ToString(raw, CultureInfo.CurrentCulture); + int value; + if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out value) && + !int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + { + throw new ArgumentException("输入必须是整数:" + prompt); + } + + if (value < minimum || value > maximum) + throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt); + return value; + } + + private static double ReadPositiveFiniteInput(string prompt) + { + double value = ReadFiniteInput(prompt); + EnsurePositive(value, prompt); + return value; + } + + private static double ReadFiniteInput(string prompt) + { + object raw = UI.GetInput(prompt); + string text = Convert.ToString(raw, CultureInfo.CurrentCulture); + double value; + if (!double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) && + !double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) + { + throw new ArgumentException("输入必须是有限数字:" + prompt); + } + + EnsureFinite(value, prompt); + return value; + } + + private static void EnsurePositive(double value, string name) + { + EnsureFinite(value, name); + if (value <= 0d) + throw new ArgumentOutOfRangeException(name, "输入必须是正数:" + name); + } + + private static void EnsureFinite(double value, string name) + { + if (!IsFinite(value)) + throw new ArgumentException("输入必须是有限数字:" + name); + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} + +/// 拥有单次规划和单个控制任务的会话生命周期,并保证停止操作幂等。 +internal static class EmClosedLoopMovementTestRunner +{ + private static readonly object SessionSync = new object(); + private static CancellationTokenSource activeCancellation; + private static Task activeTask; + private static DriveTask activeDriveTask; + private static TrajectoryObservationPresentation activePresentation; + private static TrajectoryObservationVisualizationPublisher activeWebPublisher; + private static long nextSessionId; + private static long activeSessionId; + private static long stateSequence; + + internal static void Start(PlanningPose2D goal, TrajectoryObservationSettings settings, + IReadOnlyList obstacles, long obstacleSnapshotVersion, + double maximumCommandSpeedMetersPerSecond, + double maximumDistanceToTrajectoryMeters, + double executionTimeoutSeconds, + float wheelAlignmentToleranceDegrees) + { + if (goal == null) throw new ArgumentNullException(nameof(goal)); + if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (obstacles == null) throw new ArgumentNullException(nameof(obstacles)); + + var cancellation = new CancellationTokenSource(); + CancellationTokenSource previousCancellation; + DriveTask previousDriveTask; + TrajectoryObservationPresentation previousPresentation; + TrajectoryObservationVisualizationPublisher previousWebPublisher; + long sessionId; + lock (SessionSync) + { + previousCancellation = activeCancellation; + previousDriveTask = activeDriveTask; + previousPresentation = activePresentation; + previousWebPublisher = activeWebPublisher; + activeCancellation = cancellation; + activeDriveTask = null; + activePresentation = null; + activeWebPublisher = null; + activeTask = null; + activeSessionId = sessionId = ++nextSessionId; + } + + Cancel(previousCancellation); + StopDriveTask(previousDriveTask); + ClearPresentation(previousPresentation); + StopWebPublisher(previousWebPublisher); + PrintStatus("会话 " + sessionId.ToString(CultureInfo.InvariantCulture) + + " 已启动;车辆运动前将只规划一次。"); + + Task task = Task.Run(() => RunSessionAsync(sessionId, goal, settings, + obstacles, obstacleSnapshotVersion, maximumCommandSpeedMetersPerSecond, + maximumDistanceToTrajectoryMeters, executionTimeoutSeconds, + wheelAlignmentToleranceDegrees, cancellation.Token), cancellation.Token); + lock (SessionSync) + { + if (activeSessionId == sessionId) + activeTask = task; + } + + _ = task.ContinueWith(completed => Finish(sessionId, cancellation, completed), + TaskScheduler.Default); + } + + internal static void Stop() + { + CancellationTokenSource cancellation; + DriveTask driveTask; + TrajectoryObservationPresentation presentation; + TrajectoryObservationVisualizationPublisher webPublisher; + lock (SessionSync) + { + cancellation = activeCancellation; + driveTask = activeDriveTask; + presentation = activePresentation; + webPublisher = activeWebPublisher; + activeCancellation = null; + activeDriveTask = null; + activePresentation = null; + activeWebPublisher = null; + activeTask = null; + activeSessionId = 0L; + } + + Cancel(cancellation); + StopDriveTask(driveTask); + ClearPresentation(presentation); + StopWebPublisher(webPublisher); + PrintStatus("已请求取消规划并停止控制任务。"); + } + + internal static void ShowFailure(string context, Exception exception) + { + PrintStatus(context + ":" + (exception == null ? "未知错误" : exception.Message)); + } + + private static async Task RunSessionAsync(long sessionId, PlanningPose2D goal, + TrajectoryObservationSettings settings, + IReadOnlyList obstacles, + long obstacleSnapshotVersion, + double maximumCommandSpeedMetersPerSecond, + double maximumDistanceToTrajectoryMeters, + double executionTimeoutSeconds, + float wheelAlignmentToleranceDegrees, + CancellationToken token) + { + token.ThrowIfCancellationRequested(); + VehicleMotionState initialState = ReadPlanningState(); + using var planningDeadline = new TrajectoryObservationPlanningDeadline( + TimeSpan.FromSeconds(settings.SolverTimeoutSeconds), token); + CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( + initialState.Pose, goal, settings, obstacles, obstacleSnapshotVersion); + TrajectoryObservationBootstrapResult bootstrap = + new TrajectoryObservationBootstrapper().Bootstrap(job, planningDeadline, token); + if (!bootstrap.Succeeded) + throw new InvalidOperationException(bootstrap.FailureReason); + + var controller = new TrajectoryObservationController( + bootstrap, settings, new EmPlanningService(new OsqpNativeSolver()), + "em-closed-loop-" + sessionId.ToString(CultureInfo.InvariantCulture)); + PlanningCycleResult cycle = await controller.StartCycle( + initialState.CapturedAtUtc, initialState, planningDeadline, token).ConfigureAwait(false); + if (!cycle.Published || controller.PublishedTrajectory == null) + { + string reason = cycle.Result == null ? string.Empty : cycle.Result.FailureReason; + throw new InvalidOperationException(string.IsNullOrWhiteSpace(reason) + ? cycle.Diagnostic + : reason); + } + + EmTrajectory emTrajectory = controller.PublishedTrajectory; + Trajectory2D controlTrajectory = new EmControlTrajectoryAdapter().Create(emTrajectory); + TrajectoryObservationVisualizationPublisher webPublisher = StartWebVisualization( + sessionId, bootstrap, controller, settings, obstacleSnapshotVersion, token); + TrajectoryObservationPresentation presentation = settings.EnableNativePainterVisualization + ? new TrajectoryObservationPresentation() + : null; + if (!TryRegisterPresentation(sessionId, presentation, token)) + { + ClearPresentation(presentation); + token.ThrowIfCancellationRequested(); + return; + } + UpdateVisualization(sessionId, presentation, bootstrap, controller, emTrajectory, + initialState, cycle, "规划已冻结,尚未向底盘下发控制指令。"); + PrintStatus("规划完成:EM点数=" + emTrajectory.Points.Count.ToString(CultureInfo.InvariantCulture) + + ",控制点数=" + controlTrajectory.Count.ToString(CultureInfo.InvariantCulture) + + ",长度=" + controlTrajectory.TotalLengthMeters.ToString("F3", CultureInfo.InvariantCulture) + + " m,方向=" + emTrajectory.Metadata.Direction + + ",终端=" + emTrajectory.Metadata.TerminalType + "。"); + + token.ThrowIfCancellationRequested(); + EmClosedLoopWheelSafety.RequireWheelsForward(wheelAlignmentToleranceDegrees); + var movement = new TrajectoryTrackingMovement + { + Trajectory = controlTrajectory, + MaximumCommandSpeedMetersPerSecond = maximumCommandSpeedMetersPerSecond, + MaximumDistanceToTrajectoryMeters = maximumDistanceToTrajectoryMeters, + ExecutionTimeoutSeconds = executionTimeoutSeconds, + CycleObserver = control => UpdatePresentationFromControlCycle( + sessionId, presentation, bootstrap, controller, emTrajectory, cycle, control), + }; + var driveTask = new DriveTask(movement.Get()); + if (!TryRegisterDriveTask(sessionId, driveTask, token)) + { + StopDriveTask(driveTask); + token.ThrowIfCancellationRequested(); + return; + } + + try + { + PrintStatus("冻结轨迹已交给现有控制器,开始单方向段闭环跟踪。"); + driveTask.Wait(); + if (emTrajectory.Metadata.TerminalType == EmTerminalType.GearSwitch) + PrintStatus("已在换向边界停车;本测试不启动下一方向段。"); + else + PrintStatus("首个方向段已完成并停车。"); + } + finally + { + StopDriveTask(driveTask); + ClearDriveTask(sessionId, driveTask); + } + } + + private static void UpdatePresentationFromControlCycle(long sessionId, + TrajectoryObservationPresentation presentation, + TrajectoryObservationBootstrapResult bootstrap, + TrajectoryObservationController planningController, + EmTrajectory trajectory, + PlanningCycleResult cycle, + MultiWheelC.Control.Execution.ParkingGeometricController control) + { + if (control == null || !control.LastVehicleState.HasValue) + return; + + var state = control.LastVehicleState.Value; + var planningState = new VehicleMotionState( + new PlanningPose2D(state.PoseInWorld.XMeters, state.PoseInWorld.YMeters, + state.PoseInWorld.YawRadians), + state.TwistInBody.VxMetersPerSecond, null, DateTimeOffset.UtcNow, + Interlocked.Increment(ref stateSequence)); + string diagnostic = "现有控制器正在执行冻结轨迹"; + if (control.LastCommand.HasValue) + { + var command = control.LastCommand.Value; + diagnostic += ":底盘速度=" + + command.SpeedMetersPerSecond.ToString("F3", CultureInfo.InvariantCulture) + + " m/s,前GCP=" + + (command.FrontAngleRadians * 180d / Math.PI).ToString("F2", CultureInfo.InvariantCulture) + + " deg,后GCP=" + + (command.RearAngleRadians * 180d / Math.PI).ToString("F2", CultureInfo.InvariantCulture) + " deg。"; + } + + UpdateVisualization(sessionId, presentation, bootstrap, planningController, + trajectory, planningState, cycle, diagnostic); + } + + private static void UpdateVisualization(long sessionId, + TrajectoryObservationPresentation presentation, + TrajectoryObservationBootstrapResult bootstrap, + TrajectoryObservationController controller, + EmTrajectory trajectory, + VehicleMotionState state, + PlanningCycleResult cycle, + string diagnostic) + { + lock (SessionSync) + { + if (activeSessionId != sessionId) + return; + var observation = new TrajectoryObservationObservation( + DateTimeOffset.UtcNow, state, trajectory, null, null, null); + DirectionSegmentView segment = controller.ActiveSegment; + TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build( + trajectory, segment, + controller.CreateEffectiveConfigurationSnapshot().Frenet.MaximumProjectionDistanceMeters); + if (presentation != null && ReferenceEquals(activePresentation, presentation)) + { + presentation.DrawWorld(bootstrap, observation, + TrajectoryObservationRuntimeState.Create(DateTimeOffset.UtcNow, trajectory), diagnostic); + presentation.DrawLs(charts, diagnostic); + presentation.DrawSt(charts, diagnostic); + } + + TrajectoryObservationVisualizationPublisher publisher = activeWebPublisher; + if (publisher != null) + { + var tick = new TrajectoryObservationLoopTick(observation, cycle, TimeSpan.Zero, + false, false, true, false, controller.SegmentState); + publisher.TryPublish(observation.ObservedAtUtc, () => + { + PlanningVisualizationDynamicSnapshot source = + new TrajectoryObservationDynamicSnapshotBuilder().Build( + Interlocked.Increment(ref stateSequence), tick, segment, + controller.PreviousTrajectoryForVisualization, bootstrap.Vehicle, + controller.CreateEffectiveConfigurationSnapshot()); + return new PlanningVisualizationDynamicSnapshot(source.Sequence, + source.ObservedAtUtc, "EM闭环控制中", source.ActiveSegmentIndex, + source.ActiveDirection, source.VehiclePose, source.DynamicPolylines, + source.DynamicMarkers, source.Charts, source.StatusValues, source.CycleSummary); + }); + } + } + } + + private static TrajectoryObservationVisualizationPublisher StartWebVisualization( + long sessionId, TrajectoryObservationBootstrapResult bootstrap, + TrajectoryObservationController controller, TrajectoryObservationSettings settings, + long obstacleSnapshotVersion, CancellationToken token) + { + if (!settings.EnableWebVisualization) + return null; + var publisher = new TrajectoryObservationVisualizationPublisher(settings, + new PlanningVisualizationSessionSink(), PrintStatus); + try + { + PlanningVisualizationStaticSnapshot source = + new TrajectoryObservationStaticSnapshotBuilder().Build(bootstrap, + controller.CreateEffectiveConfigurationSnapshot(), settings, obstacleSnapshotVersion); + var closedLoopSnapshot = new PlanningVisualizationStaticSnapshot("EM闭环测试", + source.WorldBounds, source.OccupancyGrid, source.StaticPolylines, + source.StaticMarkers, source.DirectionSegments, source.ConfigurationGroups); + PlanningVisualizationSessionInfo info = publisher.Start(closedLoopSnapshot); + if (info == null) + return null; + lock (SessionSync) + { + if (activeSessionId != sessionId || token.IsCancellationRequested) + { + publisher.Stop(); + return null; + } + activeWebPublisher = publisher; + } + PrintStatus("网页可视化地址(含会话令牌):" + info.Uri.AbsoluteUri); + if (settings.AutoOpenWebVisualization) + { + Process.Start(new ProcessStartInfo + { + FileName = info.Uri.AbsoluteUri, + UseShellExecute = true, + }); + } + return publisher; + } + catch (Exception exception) + { + publisher.Disable(exception); + PrintStatus("网页可视化未启动:" + exception.Message); + return null; + } + } + + private static bool TryRegisterPresentation(long sessionId, + TrajectoryObservationPresentation presentation, CancellationToken token) + { + if (presentation == null) + return true; + lock (SessionSync) + { + if (activeSessionId != sessionId || activeCancellation == null || + activeCancellation.IsCancellationRequested || token.IsCancellationRequested) + return false; + activePresentation = presentation; + return true; + } + } + + private static VehicleMotionState ReadPlanningState() + { + var location = DetourInterface.getCartLocation(); + if (location == null) + throw new InvalidOperationException("实时定位不可用。"); + if (BasicPilotBase.Chassis == null) + throw new InvalidOperationException("实时底盘读接口不可用。"); + + var speed = BasicPilotBase.Chassis.GetCarSpeed(true); + return new VehicleMotionState( + new PlanningPose2D(location.x / 1000d, location.y / 1000d, + location.th * Math.PI / 180d), + speed.Vx, null, DateTimeOffset.UtcNow, + Interlocked.Increment(ref stateSequence)); + } + + private static bool TryRegisterDriveTask(long sessionId, DriveTask driveTask, + CancellationToken token) + { + lock (SessionSync) + { + if (activeSessionId != sessionId || activeCancellation == null || + activeCancellation.IsCancellationRequested || token.IsCancellationRequested) + { + return false; + } + + activeDriveTask = driveTask; + return true; + } + } + + private static void ClearDriveTask(long sessionId, DriveTask driveTask) + { + lock (SessionSync) + { + if (activeSessionId == sessionId && ReferenceEquals(activeDriveTask, driveTask)) + activeDriveTask = null; + ClearPresentation(activePresentation); + activePresentation = null; + StopWebPublisher(activeWebPublisher); + activeWebPublisher = null; + } + } + + private static void Finish(long sessionId, CancellationTokenSource cancellation, + Task completed) + { + lock (SessionSync) + { + if (activeSessionId == sessionId) + { + activeCancellation = null; + activeDriveTask = null; + activeTask = null; + activeSessionId = 0L; + } + } + + if (completed.IsFaulted) + { + Exception failure = completed.Exception == null + ? null + : completed.Exception.GetBaseException(); + ShowFailure("闭环会话失败并已停车", failure); + } + else if (completed.IsCanceled) + { + PrintStatus("闭环会话已取消。"); + } + + cancellation.Dispose(); + } + + private static void Cancel(CancellationTokenSource cancellation) + { + if (cancellation == null) + return; + try + { + cancellation.Cancel(); + } + catch (ObjectDisposedException) + { + } + } + + private static void StopDriveTask(DriveTask driveTask) + { + if (driveTask == null) + return; + try + { + driveTask.Stop(); + } + catch (Exception exception) + { + ShowFailure("停止控制任务失败", exception); + } + } + + private static void ClearPresentation(TrajectoryObservationPresentation presentation) + { + if (presentation == null) + return; + try + { + presentation.ClearAll(); + } + catch (Exception exception) + { + ShowFailure("清理闭环可视化失败", exception); + } + } + + private static void StopWebPublisher(TrajectoryObservationVisualizationPublisher publisher) + { + if (publisher == null) + return; + try + { + publisher.Stop(); + } + catch (Exception exception) + { + ShowFailure("停止闭环网页可视化失败", exception); + } + } + + private static void PrintStatus(string message) + { + Console.WriteLine("[EM闭环测试] " + message); + } +} + +/// 在控制器接管底盘前只读检查四个舵轮是否已经与车体前向对齐。 +internal static class EmClosedLoopWheelSafety +{ + internal static void RequireWheelsForward(float toleranceDegrees) + { + var chassis = PilotDefinition.Chassis as MultiWheelChassis; + if (chassis == null) + throw new InvalidOperationException("当前底盘不是 MultiWheelChassis。"); + + var adapter = new MultiWheelChassisAdapter(chassis, PilotDefinition.Self.CarNum); + double toleranceRadians = AngleMath.DegreesToRadians(toleranceDegrees); + if (!adapter.AreParallelWheelsAligned(0d, toleranceRadians)) + { + throw new InvalidOperationException( + "四个舵轮尚未与车头方向一致,控制器未接管底盘。"); + } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md index 2df4649..e4ff396 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md @@ -1,157 +1,201 @@ -# EM trajectory observation MovementTest +# EM 轨迹规划 MovementTest(观察与闭环) -`TrajectoryObservationMovementTest` is an observe-only host for the real MDCS localization and chassis-speed read -interfaces. It bootstraps the coarse path and Local G2 reference once, requests one frozen full-direction-segment EM -plan per active direction segment by default, samples the latest published trajectory, and draws the world, LS, and ST -layers. It does not drive, steer, brake, change gear, or invoke a geometric vehicle controller. +`tarjplanner_movementtest` 将粗路径、Local G2 连续参考和 EM 轨迹规划接入两个 MovementTest: -Every runtime status contains `OBSERVE_ONLY: no chassis command is sent.` Treat the displayed control command as a -diagnostic prediction only. Goal and rolling-safety-stop commands are logged, never applied to hardware. At the end of a -gear-switch trajectory, the observer remains on the current direction segment and waits for real direction confirmation; -it does not create or dispatch a direction-change action. +- `EM轨迹规划观察闭环测试`:只读地采样真实定位和底盘速度,规划并观察轨迹,**绝不向底盘发送命令**。 +- `EM闭环测试`:冻结首个 EM 方向段后,把轨迹交给现有几何控制器执行;这是会实际控制车辆的测试,必须在受监督的安全场地使用。 -Every frozen observation session writes a text report to -`TrajectoryObservationReports/trajectory-observation-session-.txt` in the process working directory when the -session stops; the report path is printed to the console. +两种测试都会在启动时冻结目标、手动障碍物、地图和规划配置。它们不读取或修改传感器地图,也不做持续重建图。观察测试的所有状态都包含 `OBSERVE_ONLY: no chassis command is sent.`;其中显示的控制命令只是诊断预测,不能转发到硬件。 -## FullDirectionSegment observation (default) +## 模块说明(Module Overview) -`UseFullDirectionSegmentPlanning=true` maps to `EmPlanningScope.FullDirectionSegment`. MovementTest performs one frozen -optimization for the complete remaining active direction segment, then republishes that trajectory with fresh observation -state until the real direction boundary is confirmed and the next segment becomes active. `s_end` is the actual Local G2 -`PathS` at the active segment boundary; `T_end` is derived from the feasible acceleration, cruise, jerk-limited stopping, -and zero-speed hold behavior instead of being forced by a legacy time horizon. +| 模块 | 负责内容 | 不负责内容 | +| --- | --- | --- | +| `Map` | 根据冻结的起点、终点和手动障碍物创建不可变规划地图 | 实时环境更新、车辆控制 | +| `CoarsePath` | Hybrid A* 粗路径、碰撞复核与方向分段 | 速度规划、底盘命令 | +| `PathSmoothing` | 将粗路径转换为连续的 Local G2 参考 | 实时状态读取、控制 | +| `EMPlanner` | 针对活动方向段生成不可变 `EmTrajectory` | 定位读取、UI、硬件协议 | +| `TrajectoryExecution` | 轨迹采样、换向状态和诊断性控制意图 | 底盘接口、命令下发 | +| 本目录 | 会话编排、状态采样、可视化、报告,以及闭环测试的轨迹适配 | 改写上游规划结果或地图快照 | -`DistanceHorizonMeters` and `TimeHorizonSeconds` are rolling-compatible fields and do not truncate full mode. Forward -desired/hard maximum speed is `1.0 m/s`; reverse desired/hard maximum speed is `0.5 m/s`. A successful real boundary -requires a terminal stop plus exact world-position and normalized yaw matching (`0 m` / `0 rad`). +`EM轨迹规划观察闭环测试` 在换向边界只等待真实方向确认;不创建、不派发换向动作。`EM闭环测试` 只执行冻结的首个方向段;若终端是换向边界,它停车后不会自动启动下一段。 -## Rolling trajectory observation +## 文件结构(File Structure) -Rolling trajectory observation remains `OBSERVE_ONLY` and never sends a chassis command. Rolling and approach trajectories -may end with nonzero speed because `DistanceHorizonMeters` is an L-S reference window and `TimeHorizonSeconds` is the -single S-T output duration. Only a real Goal or gear-switch boundary may publish an exact zero-speed terminal. +```text +tarjplanner_movementtest/ +├── README.md # 模块边界、配置、操作和限制 +├── MovementTest.TrajectoryObservationTest.cs # 只读观察 UI 入口与会话生命周期 +├── MovementTest.EmClosedLoopTest.cs # 实际闭环 UI 入口、舵轮前向校验与停止 +├── EmControlTrajectoryAdapter.cs # EM 单方向段轨迹转几何控制器 Trajectory2D +├── TrajectoryObservationContracts.cs # 设置快照、障碍物、启动输入和结果契约 +├── TrajectoryObservationPipeline.cs # Bootstrap、EM 周期、采样和观察循环 +├── TrajectoryObservationPlanningDeadline.cs # 单周期截止时间与发布授权 +├── TrajectoryObservationSegmentTracker.cs # 终停、真实换向确认和活动方向段推进 +├── TrajectoryObservationStaticSnapshotBuilder.cs # 地图、路径、配置的静态网页快照 +├── TrajectoryObservationDynamicSnapshotBuilder.cs # 每个 tick 的动态 World/LS/ST 快照 +├── TrajectoryObservationKinematicChartBuilder.cs # 速度、加速度、jerk、曲率等图表数据 +├── TrajectoryObservationHandoffAnalyzer.cs # 轨迹交接与连续性诊断 +├── TrajectoryObservationDiagnostics.cs # 配置、规划和运行诊断模型 +├── TrajectoryObservationPresentation.cs # Painter 图层与中文状态文本 +├── TrajectoryObservationVisualizationPublisher.cs # 本机网页会话与 Painter 发布隔离 +└── TrajectoryObservationReportWriter.cs # 只读观察会话文本报告 +``` -## Configuration +## 运行数据流(Runtime Data Flow) -| Field | Unit | MovementTest UI default | Meaning | +```text +目标、手动障碍物、实时起始位姿和冻结设置 + │ + ▼ + Map -> CoarsePath -> PathSmoothing + │ + ▼ + 当前方向段的 EMPlanner -> EmTrajectory + │ + ┌─────────┴──────────────────┐ + ▼ ▼ +只读观察:每个 tick 读取状态 闭环测试:首段 Trajectory2D +并采样/绘制/报告 -> 现有几何控制器 -> 底盘 +``` + +观察测试中,规划失败、报告写入失败、网页或 Painter 发布失败均只产生诊断,不能改变其只读属性。闭环测试在控制器接管前检查底盘类型和四个舵轮是否与车体前向平行;不满足时不会接管底盘。 + +## 运行状态与停止(Runtime State and Stop) + +每个测试同时最多保留一个活动会话。重新启动会取消前一会话;`TestStop()` 取消规划和后台任务,并清理 Painter 与网页发布器。 + +观察测试在停止时把会话内容写入进程工作目录的: + +```text +TrajectoryObservationReports/trajectory-observation-session-.txt +``` + +报告路径会输出到控制台。关闭浏览器标签不会停止会话;必须使用 MovementTest 的正常停止操作。闭环测试停止时会请求停止当前 `DriveTask`,并清理其可视化资源。 + +## 坐标与单位(Coordinates and Units) + +| 项目 | 坐标/单位 | 说明 | +| --- | --- | --- | +| UI 目标与手动障碍物 | 世界坐标 `mm`、航向 `deg` | 入口处转换为规划用 SI 单位。 | +| `Pose2D` / EM 路径 | `m`、`rad` | 车辆几何中心的世界位姿。 | +| `ReferenceS` | `m` | 投影到完整活动方向段后的共享参考站。 | +| `PathS` | `m` | ST 求解使用、从活动段局部起点累计的弧长;不可与 `ReferenceS` 跨段直接相减。 | +| `l`、`v`、`a`、`j`、`κ`、`ω` | `m`、`m/s`、`m/s²`、`m/s³`、`m⁻¹`、`rad/s` | 网页/Painter 图表使用的横纵轴单位。 | + +`EmTrajectoryPoint.VelocityX` 与 `VelocityY` 是世界坐标系分量。观察测试不会进行驱动坐标转换,也不会把它们发送到车辆接口。闭环测试使用 `EmControlTrajectoryAdapter`,以世界位置重建弧长并保留有符号纵向速度、航向和曲率,得到现有控制器使用的 `Trajectory2D`。 + +## 最小使用说明(Minimal Use) + +### 只读观察 + +1. 在车辆 UI 选择 `EM轨迹规划观察闭环测试`。 +2. 设置有限的 `GoalXmm`、`GoalYmm`、`GoalYawDeg`;X 或 Y 为 `NaN` 时会依次弹出输入框。 +3. 输入 `0` 至 `20` 个手动障碍物,或输入 `0` 使用空障碍物快照。 +4. 启动测试,确认状态含有 `OBSERVE_ONLY: no chassis command is sent.`。 +5. 查看网页或 Painter 的 World、LS、ST 与运动学图层;停止时使用 UI 的正常停止操作。 + +### 实车闭环 + +1. 仅在已审查、受监督的车辆和场地中选择 `EM闭环测试`。 +2. 确认实时定位与底盘读接口可用;确认底盘为 `MultiWheelChassis`,且四个舵轮已对齐车头方向。 +3. 设置目标、障碍物以及命令速度、偏离阈值和执行超时。 +4. 启动后先等待“规划完成”状态;只有通过舵轮校验后,冻结的首个方向段才会交给现有控制器。 +5. 任意异常或人工停止时,使用 `TestStop()` 所对应的 UI 停止动作;确认控制任务已停止。 + +## 配置(Configuration) + +### 两个入口共有的规划与可视化配置 + +| 字段 | 单位 | UI 默认值 | 含义 | | --- | --- | ---: | --- | -| `GoalXmm` | world mm | `NaN` | Goal X. If X or Y is not finite, the host prompts for X, Y, and yaw. | -| `GoalYmm` | world mm | `NaN` | Goal Y. | -| `GoalYawDeg` | world deg | `0` | Goal heading. | -| `MapPaddingMeters` | m | `2.0` | Padding added on all sides of the start/goal bounds. | -| `MapResolutionMm` | mm | `50` | Local occupancy-grid resolution. | -| `UseFullDirectionSegmentPlanning` | bool | `true` | Selects `FullDirectionSegment`; set `false` for legacy `RollingHorizon`. | -| `ReplanPeriodSeconds` | s | `0.20` | Minimum interval between EM planning cycles in rolling scope. | -| `ObserverPeriodSeconds` | s | `0.05` | Live-state sampling and redraw interval. | -| `SolverTimeoutSeconds` | s | `5.0` | EM solver timeout frozen for the session. | -| `MaximumOsqpIterations` | iterations | `100000` | OSQP iteration limit frozen for the session. | -| `TimeHorizonSeconds` | s | `20.0` | Rolling-only ST trajectory horizon, not the observer period; full mode derives `T_end` instead. | -| `OutputTimeStepSeconds` | s | `0.10` | Published trajectory timestamp spacing. | -| `VehicleLengthMeters` | m | `0.80` | Vehicle envelope length supplied to coarse, smoothing, and EM planning. | -| `VehicleWidthMeters` | m | `0.60` | Vehicle envelope width supplied to planning. | -| `SafetyMarginMeters` | m | `0.05` | Additional planning clearance outside the vehicle envelope. | -| `MaximumCurvaturePerMeter` | 1/m | `1 / 1.20` | Maximum allowed vehicle curvature (about `0.8333 1/m`). | -| `EnableWebVisualization` | bool | `true` | Enables the primary local web dashboard session. | -| `AutoOpenWebVisualization` | bool | `true` | Opens the tokenized local dashboard URI when web output is enabled. | -| `WebVisualizationPort` | port | `0` | `0` selects an ephemeral port; otherwise use `1024` through `65535`. | -| `WebRefreshRateHz` | Hz | `10` | Maximum dynamic web snapshot publication rate. | -| `VisualizationHistoryCycleLimit` | cycles | `60` | Bounded web history length. | -| `EnableNativePainterVisualization` | bool | `true` | Enables optional native Painter audit windows; disable unless explicitly reviewed. | -| `DirectionConfirmationSpeedMetersPerSecond` | m/s | `0.02` | Minimum signed-speed magnitude used to confirm the next direction. | -| `DirectionConfirmationSamples` | samples | `3` | Strictly increasing matching-direction samples required after the stop hold. | -| `GearSwitchProjectionToleranceMeters` | m | `0.50` | Maximum switch-point projection distance for both adjacent segments. | -| `GearSwitchStopHoldSeconds` | s | `0.20` | Continuous real-stop duration required before direction samples count. | +| `GoalXmm` / `GoalYmm` | 世界 mm | `NaN` | 终点位置;非有限值时弹出输入。 | +| `GoalYawDeg` | 世界 deg | `0` | 终点航向。 | +| `MapPaddingMeters` | m | `2.0` | 起点/终点范围四周增加的地图边距。 | +| `MapResolutionMm` | mm | `50` | 局部占据栅格分辨率。 | +| `SolverTimeoutSeconds` | s | `5.0` | 冻结的单次规划截止时间。 | +| `MaximumOsqpIterations` | 次 | `100000` | OSQP 最大迭代数。 | +| `OutputTimeStepSeconds` | s | `0.10` | 发布轨迹相邻时间戳间隔。 | +| `VehicleLengthMeters` / `VehicleWidthMeters` | m | `0.80` / `0.60` | 规划使用的车辆外形尺寸。 | +| `SafetyMarginMeters` | m | `0.05` | 车辆外形之外的额外安全余量。 | +| `MaximumCurvaturePerMeter` | `m⁻¹` | `1 / 1.20` | 允许的最大曲率。 | +| `EnableWebVisualization` | bool | `true` | 启用本机网页看板。 | +| `AutoOpenWebVisualization` | bool | `true` | 网页会话启动后尝试打开浏览器。 | +| `WebVisualizationPort` | port | `0` | `0` 自动选择回环端口;否则必须为 `1024` 至 `65535`。 | +| `WebRefreshRateHz` | Hz | `10` | 动态网页快照的最高发布频率。 | +| `VisualizationHistoryCycleLimit` | cycles | `60` | 网页保存的有限历史周期数。 | +| `EnableNativePainterVisualization` | bool | `true` | 启用可选 Painter 图层。 | -`TrajectoryObservationSettings` contract defaults are `PlanningScope=FullDirectionSegment`, -`TimeHorizonSeconds=2.0`, `EnableWebVisualization=true`, and `EnableNativePainterVisualization=false`; the MovementTest -UI currently exposes the values above. All public fields are validated and copied into a frozen input snapshot before -the background session starts. Later edits cannot change an active session. The live pose and actual longitudinal speed -are then read once per observer tick. +`TrajectoryObservationSettings` 的契约默认值与 UI 不完全相同:`TimeHorizonSeconds=2.0`、`EnableNativePainterVisualization=false`,其余入口字段由各 MovementTest UI 显式赋值。启动前调用 `CreateValidatedSnapshot()`;之后 UI 改动不会影响活动会话。 -In full scope, EM planning runs once per active segment with one attempt in flight. In rolling scope, EM planning -respects `ReplanPeriodSeconds` with at most one planning cycle in flight. Every observer tick still captures fresh -state, samples the currently published trajectory, redraws all layers, and emits a session-guarded status; a slow -planner therefore does not reduce the configured observation cadence. +### 只读观察专用配置 -## Manual obstacles +| 字段 | 单位 | UI 默认值 | 含义 | +| --- | --- | ---: | --- | +| `UseFullDirectionSegmentPlanning` | bool | `true` | `true` 使用 `FullDirectionSegment`,`false` 使用 `RollingHorizon`。 | +| `ReplanPeriodSeconds` | s | `0.20` | 滚动模式两次规划之间的最小间隔。 | +| `ObserverPeriodSeconds` | s | `0.05` | 读取状态、采样、绘图的观察周期。 | +| `TimeHorizonSeconds` | s | `20.0` | 仅滚动兼容字段;完整方向段模式不用于截断。 | +| `DirectionConfirmationSpeedMetersPerSecond` | m/s | `0.02` | 换向确认所需的下一方向速度阈值。 | +| `DirectionConfirmationSamples` | samples | `3` | 停车后连续有效方向样本数。 | +| `GearSwitchProjectionToleranceMeters` | m | `0.50` | 换向点在相邻两段上的最大投影距离。 | +| `GearSwitchStopHoldSeconds` | s | `0.20` | 方向样本开始计数前要求的连续停车保持时间。 | -Enter a count from `0` through `20`, then select each obstacle type. Coordinates are global/world millimeters. +完整方向段模式对每个活动方向段仅规划一次;滚动模式遵守 `ReplanPeriodSeconds`。无论规划是否较慢,观察循环仍按 `ObserverPeriodSeconds` 读取新鲜状态并发布当前轨迹快照。 -- Circle example: center `(2500, 1200) mm`, radius `300 mm`. -- Axis-aligned rectangle example: center `(4000, -500) mm`, X length `800 mm`, Y width `500 mm`. +### 闭环专用配置 -The complete obstacle envelope must fit inside the start/goal bounds plus `MapPaddingMeters`; otherwise bootstrap is -rejected before EM planning. +| 字段 | 单位 | UI 默认值 | 含义 | +| --- | --- | ---: | --- | +| `MaximumCommandSpeedMetersPerSecond` | m/s | `1.00` | 交给现有控制器的最大命令速度。 | +| `MaximumDistanceToTrajectoryMeters` | m | `0.30` | 控制器允许的最大轨迹偏离距离。 | +| `ExecutionTimeoutSeconds` | s | `120` | 首个方向段执行超时。 | +| `WheelAlignmentToleranceDegrees` | deg | `2` | 四个舵轮与前向平行的最大角度误差。 | -## Reading the layers +闭环入口固定使用 `FullDirectionSegment`,规划成功后只执行第一个活动方向段。 -- `TrajectoryObserver.World` shows map bounds and occupied cells, the frozen start and goal, Hybrid A* coarse path, - Local G2 smoothed path, current real pose, and latest published EM trajectory. -- `TrajectoryObserver.LS` plots reference path-S horizontally and lateral offset vertically. Projection failures indicate - trajectory points that could not be associated with the current direction segment. -- `TrajectoryObserver.ST` overlays time-to-path-S and time-to-signed-speed. Use it to check monotonic time/progress, - stop profiles, and the sign of forward/reverse velocity. +## 手动障碍物与会话冻结(Obstacles and Frozen Session) -`EmTrajectoryPoint.VelocityX` and `VelocityY` are world-frame components. They are not chassis-frame velocity commands -and must never be forwarded directly to a vehicle motion interface. This MovementTest performs no coordinate conversion -for driving and has no driving capability; any future execution mode requires a separate safety-reviewed design. +两个入口都要求输入 `0` 至 `20` 个障碍物,坐标均为世界 `mm`: -## 网页看板与可选 Painter +- 圆形:中心 `(2500, 1200) mm`、半径 `300 mm`。 +- 轴对齐矩形:中心 `(4000, -500) mm`、X 向长度 `800 mm`、Y 向宽度 `500 mm`。 -将 `EnableWebVisualization=true` 可启用本机网页看板;它只绑定 `127.0.0.1`,启动日志会给出完整的、带随机 -会话令牌的 URL。端口为 `0` 时由系统选择可用回环端口;浏览器必须使用该完整 URL,不能删掉 token。默认 -`AutoOpenWebVisualization=true` 会尝试打开默认浏览器;无桌面会话或打开失败只记录 URL,服务和观察循环继续运行。 -网页默认以 `10 Hz` 发布不可变动态快照,网页关闭、断开或慢客户端只会丢弃网页帧,绝不会阻塞、序列化等待或改变 -规划周期。 +障碍物完整包络必须位于起点/终点矩形加 `MapPaddingMeters` 后的地图范围内,否则启动阶段拒绝会话。启动成功后,目标、障碍物、地图、车辆参数和配置均为会话快照;不会跟随 UI 后续编辑改变。 -`EnableNativePainterVisualization=true` 才会为本会话创建 `TrajectoryObserver.World`、`LS`、`ST` Painter。网页是主 -观察界面,Painter 仅作为可选的正确性/审计输出;受监督车辆清单执行前应显式关闭 Painter,除非另行审查。网页与 -Painter 可以同时开启,也可全部关闭:全部关闭时仍保留 `OBSERVE_ONLY` 规划、控制台和 UI 状态。网页的启动、快照或 -服务出现异常时会只记录一次中文诊断并熔断本次网页输出,不自动循环重启,也不会停止规划观察。 +## 网页看板与可选 Painter(Visualization) -网页采用中文说明配合科研绘图约定:白底、细灰网格和细曲线;当前轨迹为蓝色实线,上一轮为灰色虚线,换向与交接点 -为橙色,红色仅表示实际越界或失败。路径总览把已完成方向段画为灰色细实线、活动段画为蓝色细实线、未来段画为浅灰 -虚线,并以深蓝覆盖当前规划视界。页面的 `ReferenceS (m)` 是将轨迹投影到**完整活动方向段**后的共享参考站; -`PathS (m)` 是 ST 求解使用、从该段局部起点累计的实际路径弧长,二者不可互相替代或跨段直接相减。 +启用 `EnableWebVisualization=true` 后,服务只绑定 `127.0.0.1`。启动日志输出含随机会话令牌的完整 URL;必须使用完整地址,不能去掉 token。端口为 `0` 时系统选择可用回环端口。浏览器打开失败、客户端断开或快照发布异常只记录诊断并隔离该网页输出,不应阻塞规划、观察或闭环停止。 -所有图表轴标签与单位由实际快照提供:世界 `X/Y (m)`、`ReferenceS (m)`、`PathS (m)`、`t (s)`、`l (m)`、 -`v (m/s)`、`a (m/s²)`、`j (m/s³)`、`κ (m⁻¹)`、`ω (rad/s)`。`ls` 的横轴是 `ReferenceS (m)`、纵轴是 `l (m)`; -`st` 的横轴是 `t (s)`、纵轴是 `PathS (m)`。每张图支持鼠标滚轮以指针为中心缩放、拖拽框选局部放大、`重置视图` -与 `全屏`;这些操作只改变本地视口,不修改原始快照,也不向规划器发送参数。 +启用 `EnableNativePainterVisualization=true` 后,可查看 `TrajectoryObserver.World`、`TrajectoryObserver.LS` 与 `TrajectoryObserver.ST` 图层。网页是主观察界面;Painter 是可选审计输出。网页与 Painter 可同时启用,也可同时关闭。 -运动学页中 `j[i]` 仅表示真实区间 `[tᵢ, tᵢ₊₁)`,所以 jerk 只有 `N-1` 个样本;末点之后没有虚构的 `j=0`。 -状态页将滚动续航、接近停车边界和精确停车边界分别标为 `RollingContinuation`、 -`ApproachStopBoundary`、`ExactStopAtBoundary`:前两者允许非零末端速度,只有真实边界才是精确停车锚点。换向高亮 -仅会在投影位置满足容差、已连续停车保持 `GearSwitchStopHoldSeconds`,并取得 -`DirectionConfirmationSamples` 个满足 `DirectionConfirmationSpeedMetersPerSecond` 阈值的下一方向带符号速度样本后, -严格从 `N -> N+1` 前进。 +- World:地图边界、占据格、冻结起点/终点、粗路径、Local G2、实时位姿和 EM 轨迹。 +- LS:横轴 `ReferenceS (m)`,纵轴 `l (m)`;投影失败表示点无法关联到活动方向段。 +- ST:横轴 `t (s)`,纵轴 `PathS (m)`;用于检查时间、进度、停车与速度符号。 +- 运动学图:显示 `v`、`a`、`j`、`κ`、`ω` 等快照数据。`j[i]` 对应区间 `[tᵢ, tᵢ₊₁)`,因此只有 `N-1` 个样本。 -## Operator launch and stop checklist +## 报告与诊断(Reports and Diagnostics) -The vehicle UI entry is `EM轨迹规划观察闭环测试`. It is an observation session only; it has no chassis, -motor, steering, brake, or gear output. +只读观察会话把带时间戳的诊断写入文本报告,包含会话编号、运行状态和 `OBSERVE_ONLY` 安全标识。常见状态包括当前活动方向段、规划是否已发布、轨迹采样结果、换向确认进度和可视化地址。 -Before starting: +闭环测试会在控制器接管前输出冻结轨迹点数、控制点数、长度、方向和终端类型;执行期间输出底盘速度与前后 GCP 角度,供监督人员诊断。该测试的命令输出属于真实控制流程,不能按观察模式解释。 -1. Ensure the vehicle is in a safe, supervised state; this test reads live localization and signed chassis speed but - never controls the vehicle. -2. Confirm localization and the chassis read interface are available. The session reports a startup failure if either - cannot be read. -3. Set a finite goal X/Y/yaw (or be ready to enter them when prompted), then enter zero to twenty manual obstacles. - Each obstacle must remain inside the configured start/goal rectangle plus map padding. -4. Keep `EnableWebVisualization=true` and set `EnableNativePainterVisualization=false` unless the Painter audit output is - explicitly required and reviewed for this vehicle session. +## 常见错误(Common Errors) -To start, select the entry in the vehicle UI and start the MovementTest. Confirm the status begins with -`OBSERVE_ONLY: no chassis command is sent.`, then review the bootstrap status and the -`TrajectoryObserver.World`, `TrajectoryObserver.LS`, and `TrajectoryObserver.ST` layers. A logged trajectory control -command is diagnostic information only and must not be copied into a vehicle-control interface. +| 现象 | 检查项 | +| --- | --- | +| 启动即提示定位或底盘接口不可用 | 确认 MDCS 定位和底盘只读接口已连接;观察与闭环入口都需要初始状态。 | +| 目标或障碍物输入被拒绝 | 检查所有值为有限数;障碍物数量为 `0–20`;半径/边长为正;包络位于地图范围内。 | +| 设置校验失败 | 正数配置不得为零或非有限;`OutputTimeStepSeconds` 不得大于 `TimeHorizonSeconds`;端口只能为 `0` 或 `1024–65535`。 | +| 观察测试没有车辆动作 | 这是预期行为。该入口永远是 `OBSERVE_ONLY`。 | +| 闭环测试未接管车辆 | 检查首段规划是否成功、控制轨迹是否至少有两个不同位置点、底盘类型以及舵轮前向对齐。 | +| 闭环在换向边界停止 | 这是预期行为;该测试不自动执行下一方向段。 | +| 网页没有打开或无法访问 | 从控制台复制完整令牌 URL;确认使用本机回环地址;打开失败不会停止会话。 | -To stop, use the vehicle UI's normal MovementTest stop action. Confirm the status says -`Observation stop requested; all observer layers were cleared.` The cancellation request stops observer work and clears -an existing Painter, stops the web publisher/HTTP/SSE service, and releases its port. A current-session runtime fault -also clears an existing Painter and stops the web session so stale diagnostics are not left on screen; an intentional -bootstrap failure still reports through UI/console even when both visualization modes are off. Closing a browser tab does -not stop the session; `TestStop()` owns final web and Painter cleanup. Stopping, running, or starting this test must not -issue chassis, motor, steering, brake, or gear output. +## 当前限制(Current Limits) + +- 地图和障碍物只在启动时冻结;本模块不实现实时感知更新或全局重规划。 +- 观察测试不具备任何底盘、转向、制动或换向命令能力。 +- 闭环测试只覆盖冻结的首个方向段,换向后续段不自动执行。 +- 闭环控制依赖现有 `TrajectoryTrackingMovement` 和 `MultiWheelChassis`;它不是对任意底盘的通用执行接口。 +- 网页服务仅限本机回环访问,且可视化故障只作为诊断处理。 diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs index b8074ea..44ed7a9 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs @@ -7,6 +7,7 @@ using MultiWheelC.TrajectoryPlanning.EMPlanner; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 观察模式下的不可变诊断消息;错误只报告给操作者,不能触发车辆动作。 public sealed class TrajectoryObservationDiagnostic { public TrajectoryObservationDiagnostic(string text) @@ -17,6 +18,7 @@ public sealed class TrajectoryObservationDiagnostic public string Text { get; } } +/// 构造带会话上下文的统一观察诊断文本。 public static class TrajectoryObservationDiagnostics { public static TrajectoryObservationDiagnostic CreateCurveReport(TrajectoryObservationCharts charts, diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs index e642722..f95a322 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs @@ -7,6 +7,7 @@ using TrajectoryPlanningVisualization; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 将每个 tick 的新鲜车辆状态与已发布轨迹转换为不可变动态可视化快照。 public sealed class TrajectoryObservationDynamicSnapshotBuilder { private readonly TrajectoryObservationKinematicChartBuilder charts = new TrajectoryObservationKinematicChartBuilder(); diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs index 9bbcbaa..b23a645 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs @@ -4,6 +4,7 @@ using MultiWheelC.TrajectoryPlanning.EMPlanner; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 描述相邻已发布轨迹在交接处的位置、航向和速度连续性,仅作诊断。 public sealed class TrajectoryObservationHandoffMetrics { internal TrajectoryObservationHandoffMetrics(bool available, double? position, double? referenceS, double? velocity, double? acceleration) @@ -15,6 +16,7 @@ public sealed class TrajectoryObservationHandoffMetrics public double? DeltaAccelerationMetersPerSecondSquared { get; } } +/// 计算观察会话中的轨迹交接指标,不参与轨迹发布或控制决策。 public sealed class TrajectoryObservationHandoffAnalyzer { public TrajectoryObservationHandoffMetrics Analyze(EmTrajectory current, EmTrajectory previous, DirectionSegmentView segment) diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs index 564d651..e2fd515 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs @@ -7,6 +7,7 @@ using TrajectoryPlanningVisualization; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 建立轨迹的时间、路径弧长、速度、加速度和 jerk 图表数据;仅用于观察与诊断。 public sealed class TrajectoryObservationKinematicChartBuilder { public IReadOnlyList Build(EmTrajectory trajectory, DirectionSegmentView segment, diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs index 8f2e174..6ca9a10 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MultiWheelC.TrajectoryPlanning.CoarsePath; @@ -12,6 +13,7 @@ using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 一次观察会话启动的不可变结果,成功时同时携带冻结的粗路径、平滑路径、地图和方向段。 public sealed class TrajectoryObservationBootstrapResult { private readonly VehicleParameters vehicle; @@ -87,6 +89,7 @@ public sealed class TrajectoryObservationBootstrapResult } } +/// 只在会话开始阶段构造并冻结规划输入;后续观察 tick 不会重新读取或改写该快照。 public sealed class TrajectoryObservationBootstrapper { private readonly CoarsePathPlanningService coarseService; @@ -109,22 +112,110 @@ public sealed class TrajectoryObservationBootstrapper { if (job == null) throw new ArgumentNullException(nameof(job)); - CoarsePathPlanningJobResult coarse = coarseService.Plan(job, cancellationToken); + TimeSpan configuredLimit = job.Configuration == null + ? TimeSpan.FromSeconds(5d) + : job.Configuration.SearchTimeout; + using var deadline = new TrajectoryObservationPlanningDeadline(configuredLimit, cancellationToken); + return Bootstrap(job, deadline, cancellationToken); + } + + public TrajectoryObservationBootstrapResult Bootstrap(CoarsePathPlanningJob job, + TrajectoryObservationPlanningDeadline deadline, CancellationToken cancellationToken = default) + { + if (job == null) throw new ArgumentNullException(nameof(job)); + if (deadline == null) throw new ArgumentNullException(nameof(deadline)); + if (DeadlineExpired(deadline)) + return DeadlineFailure(job, null, null, deadline, "coarse"); + + CoarsePathPlanningJob effectiveJob = CopyJobWithDeadline(job, deadline); + bool coarseUsesCycleRemainder = job.Configuration != null && + effectiveJob.Configuration.SearchTimeout < job.Configuration.SearchTimeout; + using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( + deadline.Token, cancellationToken); + + CoarsePathPlanningJobResult coarse = coarseService.Plan(effectiveJob, linkedCancellation.Token); + if (DeadlineExpired(deadline) || + (coarseUsesCycleRemainder && coarse.PlanningResult.Status == PlanningStatus.SearchTimeout)) + { + return DeadlineFailure(job, coarse, null, deadline, "coarse"); + } if (coarse.PlanningResult.Status != PlanningStatus.Success) return TrajectoryObservationBootstrapResult.FromFailure( job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status); + if (DeadlineExpired(deadline)) + return DeadlineFailure(job, coarse, null, deadline, "smoothing"); + var smoothingRequest = new PathSmoothingRequest( CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map), coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle, new PathSmoothingConfiguration()); - PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, cancellationToken); + PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, linkedCancellation.Token); + if (DeadlineExpired(deadline)) + return DeadlineFailure(job, coarse, smooth, deadline, "smoothing"); if (!IsPublishedSmoothingStatus(smooth.Status)) return TrajectoryObservationBootstrapResult.FromFailure( job, coarse, smooth, smooth.Diagnostics.TerminationReason); return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth)); } + private static bool DeadlineExpired(TrajectoryObservationPlanningDeadline deadline) + { + return deadline.IsExpired && !deadline.CallerCancellationRequested; + } + + private static TrajectoryObservationBootstrapResult DeadlineFailure(CoarsePathPlanningJob job, + CoarsePathPlanningJobResult coarse, PathSmoothingResult smooth, + TrajectoryObservationPlanningDeadline deadline, string phase) + { + string diagnostic = "cycleDeadlineExpired=true;phase=" + phase + ";remainingMs=" + + deadline.Remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture); + return TrajectoryObservationBootstrapResult.FromFailure(job, coarse, smooth, diagnostic); + } + + private static CoarsePathPlanningJob CopyJobWithDeadline(CoarsePathPlanningJob source, + TrajectoryObservationPlanningDeadline deadline) + { + return new CoarsePathPlanningJob + { + MapRequest = source.MapRequest, + Start = source.Start, + Goal = source.Goal, + Vehicle = source.Vehicle, + Configuration = CopyConfigurationWithDeadline(source.Configuration, deadline), + StartVehicleCurvature = source.StartVehicleCurvature, + StartDirection = source.StartDirection, + GoalDirection = source.GoalDirection, + DebugOptions = source.DebugOptions, + }; + } + + private static HybridAStarConfiguration CopyConfigurationWithDeadline(HybridAStarConfiguration source, + TrajectoryObservationPlanningDeadline deadline) + { + if (source == null) return null; + return new HybridAStarConfiguration + { + PrimitiveLengthMeters = source.PrimitiveLengthMeters, + IntegrationStepMeters = source.IntegrationStepMeters, + MaximumCollisionCheckStepMeters = source.MaximumCollisionCheckStepMeters, + HeadingResolutionRadians = source.HeadingResolutionRadians, + CurvatureLevelCount = source.CurvatureLevelCount, + GoalPositionToleranceMeters = source.GoalPositionToleranceMeters, + GoalHeadingToleranceRadians = source.GoalHeadingToleranceRadians, + MaximumExpandedNodes = source.MaximumExpandedNodes, + SearchTimeout = deadline.Clamp(source.SearchTimeout), + HeuristicWeight = source.HeuristicWeight, + ReverseCostMultiplier = source.ReverseCostMultiplier, + GearSwitchPenaltyMeters = source.GearSwitchPenaltyMeters, + CurvatureMagnitudeWeight = source.CurvatureMagnitudeWeight, + CurvatureChangePenaltyMetersPerLevel = source.CurvatureChangePenaltyMetersPerLevel, + ClearanceCostWeight = source.ClearanceCostWeight, + ClearanceCostDistanceMeters = source.ClearanceCostDistanceMeters, + AllowReverse = source.AllowReverse, + }; + } + private static IReadOnlyList CopyFiniteClearance(IReadOnlyList path, PlanningGridMap map) { @@ -158,6 +249,7 @@ public sealed class TrajectoryObservationBootstrapper } } +/// 单个观察 tick 的只读结果,供状态文本、报告和可视化共同消费。 public sealed class TrajectoryObservationObservation { internal TrajectoryObservationObservation(DateTimeOffset observedAtUtc, VehicleMotionState vehicleState, @@ -185,6 +277,7 @@ public sealed class TrajectoryObservationObservation public TrajectoryExecutionState ExecutorState { get; } } +/// 协调一次冻结会话中的 EM 规划、轨迹采样和诊断;从不将预测命令发送给车辆。 public sealed class TrajectoryObservationController { private readonly TrajectoryObservationBootstrapResult bootstrap; @@ -272,8 +365,19 @@ public sealed class TrajectoryObservationController public async Task StartCycle(DateTimeOffset now, VehicleMotionState state, CancellationToken cancellationToken) + { + using var deadline = new TrajectoryObservationPlanningDeadline( + TimeSpan.FromSeconds(settings.SolverTimeoutSeconds), cancellationToken); + return await StartCycle(now, state, deadline, cancellationToken).ConfigureAwait(false); + } + + public async Task StartCycle(DateTimeOffset now, VehicleMotionState state, + TrajectoryObservationPlanningDeadline deadline, CancellationToken cancellationToken) { if (state == null) throw new ArgumentNullException(nameof(state)); + if (deadline == null) throw new ArgumentNullException(nameof(deadline)); + using var callerCancellation = CancellationTokenSource.CreateLinkedTokenSource( + deadline.CallerToken, cancellationToken); int targetSegmentIndex = ActiveSegment.SegmentIndex; int pendingSegmentIndex = -1; @@ -303,18 +407,53 @@ public sealed class TrajectoryObservationController } long currentCycleId = Interlocked.Increment(ref cycleId); + EmPlannerConfiguration effectiveConfiguration = configuration.Copy(); + TimeSpan cycleRemaining = deadline.Remaining; + TimeSpan configuredSolverLimit = TimeSpan.FromSeconds( + configuration.Scheduling.SolverTimeoutSeconds); + if (cycleRemaining > TimeSpan.Zero && + cycleRemaining < configuredSolverLimit - TimeSpan.FromMilliseconds(1d)) + { + effectiveConfiguration.Scheduling.SolverTimeoutSeconds = cycleRemaining.TotalSeconds; + } var request = new EmPlanningRequest( - bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, configuration, + bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, effectiveConfiguration, targetSegmentIndex, previousTrajectory, now, now, sessionId + "-trajectory-" + currentCycleId, sessionId + "-reference", previousTrajectory?.Metadata.TrajectoryId ?? string.Empty, - EmMotionModel.NonholonomicForwardReverse, settings.PlanningScope); - PlanningCycleResult result = await targetCoordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken) - .ConfigureAwait(false); - if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment && result.Published && - result.Result.Trajectory != null && !planningPendingFullDirection) + EmMotionModel.NonholonomicForwardReverse, settings.PlanningScope, cycleRemaining, + callerCancellation.Token, deadline.DeadlineToken, + deadline.PublicationAuthorization); + if (callerCancellation.IsCancellationRequested) { - activeFullDirectionTrajectory = result.Result.Trajectory; + var cancelledResult = new EmPlanningResult(EmPlanningStatus.Cancelled, null, + "callerCancellation=true;phase=request"); + return new PlanningCycleResult(currentCycleId, PlanningCycleIdentity.FromRequest(request), + cancelledResult, false, cancelledResult.FailureReason); + } + if (deadline.DeadlineToken.IsCancellationRequested) + { + var expiredResult = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null, + "cycleDeadlineExpired=true;phase=request;remainingMs=" + + cycleRemaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); + return new PlanningCycleResult(currentCycleId, PlanningCycleIdentity.FromRequest(request), + expiredResult, false, expiredResult.FailureReason); + } + Action fullDirectionCommit = + settings.PlanningScope == EmPlanningScope.FullDirectionSegment && !planningPendingFullDirection + ? trajectory => activeFullDirectionTrajectory = trajectory + : null; + PlanningCycleResult result = await targetCoordinator.PlanLatestAsync( + new PlanningCycleInput(request, now), callerCancellation.Token, + fullDirectionCommit).ConfigureAwait(false); + if (!result.Published && deadline.IsExpired && !callerCancellation.IsCancellationRequested && + result.Result.Status == EmPlanningStatus.Cancelled) + { + var expiredResult = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null, + "cycleDeadlineExpired=true;phase=planning;remainingMs=" + + deadline.Remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); + result = new PlanningCycleResult(result.Version, result.Identity, expiredResult, false, + expiredResult.FailureReason); } return result; } @@ -454,6 +593,7 @@ public sealed class TrajectoryObservationLoopTick public bool ShouldLog => true; } +/// 按观察周期捕获新鲜只读状态并发布快照的循环;规划耗时不能改变观察安全边界。 public sealed class TrajectoryObservationLoop { private readonly TrajectoryObservationController controller; @@ -511,6 +651,7 @@ public enum TrajectoryObservationSessionEndReason Cancellation, } +/// 集中定义观察会话的开始、停止和故障清理语义,避免留下旧会话可视化。 public static class TrajectoryObservationSessionLifecycle { public static bool ShouldClearLayers(TrajectoryObservationSessionEndReason reason) diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPlanningDeadline.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPlanningDeadline.cs new file mode 100644 index 0000000..f1916a3 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPlanningDeadline.cs @@ -0,0 +1,226 @@ +using System; +using System.Diagnostics; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.EMPlanner; + +namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; + +/// Owns one monotonic time budget shared by every phase of an observation planning cycle. +public sealed class TrajectoryObservationPlanningDeadline : IDisposable +{ + private static readonly TimeSpan MaximumTimerDelay = TimeSpan.FromMilliseconds(uint.MaxValue - 1d); + private readonly object authorizationGate = new object(); + private readonly TimeSpan limit; + private readonly Stopwatch stopwatch; + private readonly TimeSpan? injectedElapsed; + private readonly CancellationToken callerToken; + private readonly CancellationTokenSource expirationCancellation; + private readonly CancellationTokenSource linkedCancellation; + private readonly Timer expirationTimer; + private readonly IEmPlanningPublicationAuthorization publicationAuthorization; + private bool callerCancellationObserved; + private bool deadlineExpired; + private bool disposed; + + public TrajectoryObservationPlanningDeadline(TimeSpan limit, CancellationToken callerToken = default) + : this(limit, null, callerToken) + { + } + + private TrajectoryObservationPlanningDeadline(TimeSpan limit, TimeSpan? injectedElapsed, + CancellationToken callerToken) + { + if (limit < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(limit), "The planning deadline cannot be negative."); + if (injectedElapsed.HasValue && injectedElapsed.Value < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(injectedElapsed)); + + this.limit = limit; + this.injectedElapsed = injectedElapsed; + this.callerToken = callerToken; + stopwatch = injectedElapsed.HasValue ? null : Stopwatch.StartNew(); + expirationCancellation = new CancellationTokenSource(); + TimeSpan initialRemaining = ComputeRemaining(); + deadlineExpired = initialRemaining <= TimeSpan.Zero; + if (deadlineExpired) + expirationCancellation.Cancel(); + linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( + callerToken, expirationCancellation.Token); + publicationAuthorization = new DeadlinePublicationAuthorization(this); + if (!deadlineExpired) + { + TimeSpan timerDelay = initialRemaining < MaximumTimerDelay + ? initialRemaining + : MaximumTimerDelay; + expirationTimer = new Timer(ExpireFromTimer, null, timerDelay, Timeout.InfiniteTimeSpan); + } + } + + public TimeSpan Remaining + { + get + { + bool cancelExpiration = false; + TimeSpan remaining; + lock (authorizationGate) + { + remaining = ComputeRemaining(); + if (!deadlineExpired && remaining <= TimeSpan.Zero) + { + deadlineExpired = true; + cancelExpiration = true; + } + if (deadlineExpired) + remaining = TimeSpan.Zero; + } + if (cancelExpiration) + CancelExpirationToken(); + return remaining; + } + } + + public bool IsExpired => callerToken.IsCancellationRequested || DeadlineExpired; + + public CancellationToken Token => linkedCancellation.Token; + + internal bool CallerCancellationRequested => callerToken.IsCancellationRequested; + + internal CancellationToken CallerToken => callerToken; + + internal CancellationToken DeadlineToken => expirationCancellation.Token; + + internal IEmPlanningPublicationAuthorization PublicationAuthorization => publicationAuthorization; + + internal static TrajectoryObservationPlanningDeadline CreateForTesting(TimeSpan limit, + TimeSpan elapsed, CancellationToken callerToken) + { + return new TrajectoryObservationPlanningDeadline(limit, elapsed, callerToken); + } + + public TimeSpan Clamp(TimeSpan configuredLimit) + { + if (configuredLimit < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(configuredLimit)); + TimeSpan remaining = Remaining; + return configuredLimit < remaining ? configuredLimit : remaining; + } + + public void Dispose() + { + lock (authorizationGate) + disposed = true; + expirationTimer?.Dispose(); + linkedCancellation.Dispose(); + expirationCancellation.Dispose(); + } + + private bool DeadlineExpired + { + get + { + _ = Remaining; + lock (authorizationGate) + return deadlineExpired; + } + } + + private TimeSpan ComputeRemaining() + { + TimeSpan elapsed = injectedElapsed ?? stopwatch.Elapsed; + TimeSpan remaining = limit - elapsed; + return remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero; + } + + private EmPlanningPublicationDecision TryPublish(CancellationToken requestCallerCancellationToken, + CancellationToken coordinatorCallerCancellationToken, Action publish) + { + if (publish == null) + throw new ArgumentNullException(nameof(publish)); + + bool cancelExpiration = false; + using CancellationTokenRegistration ownerCallerRegistration = + callerToken.Register(ObserveCallerCancellation); + using CancellationTokenRegistration requestCallerRegistration = + requestCallerCancellationToken.Register(ObserveCallerCancellation); + using CancellationTokenRegistration coordinatorCallerRegistration = + coordinatorCallerCancellationToken.Register(ObserveCallerCancellation); + EmPlanningPublicationDecision decision; + lock (authorizationGate) + { + if (callerToken.IsCancellationRequested || + requestCallerCancellationToken.IsCancellationRequested || + coordinatorCallerCancellationToken.IsCancellationRequested) + { + callerCancellationObserved = true; + } + if (!deadlineExpired && ComputeRemaining() <= TimeSpan.Zero) + { + deadlineExpired = true; + cancelExpiration = true; + } + if (callerCancellationObserved) + decision = EmPlanningPublicationDecision.CallerCancelled; + else if (deadlineExpired || disposed) + decision = EmPlanningPublicationDecision.DeadlineExpired; + else + { + publish(); + decision = EmPlanningPublicationDecision.Published; + } + } + if (cancelExpiration) + CancelExpirationToken(); + return decision; + } + + private void ObserveCallerCancellation() + { + lock (authorizationGate) + callerCancellationObserved = true; + } + + private void ExpireFromTimer(object state) + { + bool cancelExpiration = false; + lock (authorizationGate) + { + if (!disposed && !deadlineExpired) + { + deadlineExpired = true; + cancelExpiration = true; + } + } + if (cancelExpiration) + CancelExpirationToken(); + } + + private void CancelExpirationToken() + { + try + { + expirationCancellation.Cancel(); + } + catch (ObjectDisposedException) + { + // Disposal can race a timer callback only after the owning cycle has completed. + } + } + + private sealed class DeadlinePublicationAuthorization : IEmPlanningPublicationAuthorization + { + private readonly TrajectoryObservationPlanningDeadline owner; + + public DeadlinePublicationAuthorization(TrajectoryObservationPlanningDeadline owner) + { + this.owner = owner; + } + + public TimeSpan Remaining => owner.Remaining; + + public bool IsExpired => owner.DeadlineExpired; + + public EmPlanningPublicationDecision TryPublish(CancellationToken requestCallerCancellationToken, + CancellationToken coordinatorCallerCancellationToken, Action publish) => + owner.TryPublish(requestCallerCancellationToken, coordinatorCallerCancellationToken, publish); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs index bd84550..bc3e102 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs @@ -13,6 +13,7 @@ using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 将只读观察结果转换为面向操作者的中文文本,不包含控制指令下发逻辑。 public static class TrajectoryObservationPresentationText { public static string Create(TrajectoryObservationObservation observation, TrajectoryObservationCharts charts) @@ -77,6 +78,7 @@ public sealed class TrajectoryObservationLsPresentationModel } } +/// 组合当前会话的文本、图层和图表展示模型。 public sealed class TrajectoryObservationPresentation { private const float MillimetersPerMeter = 1000f; diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs index bb809a6..ddba99f 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs @@ -6,6 +6,7 @@ using MultiWheelC.TrajectoryPlanning.EMPlanner; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 观察器对当前方向段和真实换向确认过程的只读阶段。 public enum TrajectoryObservationSegmentPhase { Planning, @@ -54,6 +55,7 @@ public sealed class TrajectoryObservationSegmentUpdate public TrajectoryObservationSegmentState State { get; } } +/// 根据真实状态和方向确认推进活动段;不会自行请求或执行车辆换向。 public sealed class TrajectoryObservationSegmentTracker { private readonly IReadOnlyList segments; diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs index d4435e7..aa59d1d 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs @@ -8,6 +8,7 @@ using TrajectoryPlanningVisualization; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; +/// 从冻结的规划输入建立世界和参考路径静态快照,供会话期间重复只读使用。 public sealed class TrajectoryObservationStaticSnapshotBuilder { public PlanningVisualizationStaticSnapshot Build(TrajectoryObservationBootstrapResult bootstrap, diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs deleted file mode 100644 index 2a88735..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs +++ /dev/null @@ -1,512 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.EMPlanner; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace EMPlannerVerificationHost; - -internal static class CoordinatorChecks -{ - public static void Run() - { - VerifiesCallerSuppliedSchedulingDecision(); - VerifiesCompletedCycleDoesNotPoisonNextCancellationSource(); - VerifiesLatestCycleWinsAndEveryIdentityFieldSuppressesStaleResults(); - VerifiesSinkExceptionsAreIsolatedIntoCycleDiagnostics(); - VerifiesSafePreviousTrajectoryHandoffs(); - } - - public static void RunRollingEndToEnd() - { - VerifiesLatestCycleWinsAndEveryIdentityFieldSuppressesStaleResults(); - VerifiesSafePreviousTrajectoryHandoffs(); - VerifiesPublishedForwardAndReverseCommands(); - VerifiesFailureLeavesPublishedZeroSpeedTailExecutable(); - } - - private static void VerifiesCallerSuppliedSchedulingDecision() - { - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service); - DateTimeOffset start = DateTimeOffset.UnixEpoch.AddSeconds(100d); - PlanningCycleInput input = CreateInput(CreateMap(1), "schedule-reference", 7L, "prior", 0, "schedule", start); - - Verification.True(coordinator.ShouldStartCycle(start), "first cycle is due"); - Task cycle = coordinator.PlanLatestAsync(input, CancellationToken.None); - service.WaitUntilStarted("schedule"); - service.Complete("schedule"); - Verification.Equal(EmPlanningStatus.Success, cycle.GetAwaiter().GetResult().Result.Status, "schedule setup result"); - - Verification.True(!coordinator.ShouldStartCycle(start.AddSeconds(0.199d)), "cycle remains deferred before period"); - Verification.True(coordinator.ShouldStartCycle(start.AddSeconds(0.20d)), "cycle becomes due at exact period"); - } - - private static void VerifiesLatestCycleWinsAndEveryIdentityFieldSuppressesStaleResults() - { - PlanningGridMap firstMap = CreateMap(10); - PlanningGridMap secondMap = CreateMap(11); - DateTimeOffset now = DateTimeOffset.UnixEpoch.AddSeconds(200d); - - VerifySuperseded("later-version", CreateInput(firstMap, "reference", 7L, "prior", 0, "later-version-A", now), - CreateInput(firstMap, "reference", 7L, "prior", 0, "later-version-B", now.AddSeconds(0.20d))); - VerifySuperseded("map", CreateInput(firstMap, "reference", 7L, "prior", 0, "map-A", now), - CreateInput(secondMap, "reference", 7L, "prior", 0, "map-B", now.AddSeconds(0.20d))); - VerifySuperseded("reference", CreateInput(firstMap, "reference-A", 7L, "prior", 0, "reference-A", now), - CreateInput(firstMap, "reference-B", 7L, "prior", 0, "reference-B", now.AddSeconds(0.20d))); - VerifySuperseded("state", CreateInput(firstMap, "reference", 7L, "prior", 0, "state-A", now), - CreateInput(firstMap, "reference", 8L, "prior", 0, "state-B", now.AddSeconds(0.20d))); - VerifySuperseded("segment", CreateInput(firstMap, "reference", 7L, "prior", 0, "segment-A", now), - CreateInput(firstMap, "reference", 7L, "prior", 1, "segment-B", now.AddSeconds(0.20d))); - VerifySuperseded("previous", CreateInput(firstMap, "reference", 7L, "prior-A", 0, "previous-A", now), - CreateInput(firstMap, "reference", 7L, "prior-B", 0, "previous-B", now.AddSeconds(0.20d))); - } - - private static void VerifiesCompletedCycleDoesNotPoisonNextCancellationSource() - { - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service); - DateTimeOffset now = DateTimeOffset.UnixEpoch.AddSeconds(150d); - - PlanningCycleInput first = CreateInput(CreateMap(5), "completed-reference", 4L, "prior", 0, - "completed-first", now); - Task firstCycle = coordinator.PlanLatestAsync(first, CancellationToken.None); - service.WaitUntilStarted(first.Request.OutputTrajectoryId); - service.Complete(first.Request.OutputTrajectoryId); - Verification.Equal(EmPlanningStatus.Success, firstCycle.GetAwaiter().GetResult().Result.Status, - "first completed cycle succeeds"); - - PlanningCycleInput second = CreateInput(CreateMap(6), "completed-reference", 5L, "completed-first", 0, - "completed-second", now.AddSeconds(0.20d)); - Task secondCycle = coordinator.PlanLatestAsync(second, CancellationToken.None); - service.WaitUntilStarted(second.Request.OutputTrajectoryId); - service.Complete(second.Request.OutputTrajectoryId); - - PlanningCycleResult result = secondCycle.GetAwaiter().GetResult(); - Verification.Equal(EmPlanningStatus.Success, result.Result.Status, "next cycle after completion succeeds"); - Verification.True(result.Published, "next cycle after completion publishes"); - } - - private static void VerifiesSinkExceptionsAreIsolatedIntoCycleDiagnostics() - { - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service, new ThrowingCycleSink()); - PlanningCycleInput input = CreateInput(CreateMap(20), "sink-reference", 9L, "prior", 0, "sink", DateTimeOffset.UnixEpoch); - - Task cycle = coordinator.PlanLatestAsync(input, CancellationToken.None); - service.WaitUntilStarted("sink"); - service.Complete("sink"); - PlanningCycleResult result = cycle.GetAwaiter().GetResult(); - - Verification.Equal(EmPlanningStatus.Success, result.Result.Status, "sink failure preserves planning result"); - Verification.True(result.Diagnostic.IndexOf("cycle sink", StringComparison.OrdinalIgnoreCase) >= 0, - "sink failure is reported in diagnostic"); - } - - private static void VerifiesSafePreviousTrajectoryHandoffs() - { - DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(300d); - EmPlannerConfiguration configuration = CreateHandoffConfiguration(); - var selector = new TrajectoryHandoffSelector(); - EmTrajectory forward = CreateHandoffTrajectory("handoff-forward", TravelDirection.Forward, 2, effectiveAt, - EmBoundaryType.RollingSafetyStop, false); - VehicleMotionState forwardMeasured = CreateMeasuredState(0.01d, 0d, 3.1666666666666665d, 0.10d, - effectiveAt.AddSeconds(0.10d), 40L); - - TrajectoryHandoffSelection accepted = selector.Select(forward, forwardMeasured, 2, TravelDirection.Forward, - effectiveAt.AddSeconds(0.10d), configuration); - Verification.Equal(TrajectoryHandoffSource.PreviousTrajectory, accepted.Source, "forward handoff source"); - Verification.Equal(TrajectoryHandoffRejectionReason.None, accepted.RejectionReason, "forward handoff reason"); - Verification.True(object.ReferenceEquals(forward, accepted.PreviousTrajectory), "forward handoff seed"); - Verification.NearlyEqual(0.04d, accepted.StartState.Pose.X, "forward handoff position interpolation"); - Verification.NearlyEqual(3.3666666666666667d, accepted.StartState.Pose.Heading, - "forward handoff yaw remains unwrapped"); - Verification.NearlyEqual(0.10d, accepted.StartState.SignedLongitudinalSpeedMetersPerSecond, - "forward handoff signed speed interpolation"); - Verification.Equal(40L, accepted.StartState.SequenceId, "handoff retains measured-state identity"); - - EmTrajectory reverse = CreateHandoffTrajectory("handoff-reverse", TravelDirection.Reverse, 2, effectiveAt, - EmBoundaryType.RollingSafetyStop, false); - VehicleMotionState reverseMeasured = CreateMeasuredState(-0.01d, 0d, 3.1666666666666665d, -0.10d, - effectiveAt.AddSeconds(0.10d), 41L); - TrajectoryHandoffSelection reverseAccepted = selector.Select(reverse, reverseMeasured, 2, TravelDirection.Reverse, - effectiveAt.AddSeconds(0.10d), configuration); - Verification.Equal(TrajectoryHandoffSource.PreviousTrajectory, reverseAccepted.Source, - "reverse same-segment handoff source"); - Verification.NearlyEqual(-0.04d, reverseAccepted.StartState.Pose.X, "reverse handoff position interpolation"); - Verification.NearlyEqual(-0.10d, reverseAccepted.StartState.SignedLongitudinalSpeedMetersPerSecond, - "reverse handoff signed speed interpolation"); - - AssertHandoffRejected(selector, forward, forwardMeasured, 2, TravelDirection.Forward, - effectiveAt.AddSeconds(0.21d), configuration, TrajectoryHandoffRejectionReason.TrajectoryTooOld, - "stale trajectory handoff"); - configuration.Scheduling.MaximumVehicleStateAgeSeconds = 0.60d; - AssertHandoffRejected(selector, forward, - CreateMeasuredState(1d, 0d, 3.1666666666666665d, 0.10d, effectiveAt.AddSeconds(0.10d), 42L), 2, - TravelDirection.Forward, effectiveAt.AddSeconds(0.10d), configuration, - TrajectoryHandoffRejectionReason.TrackingErrorExceeded, "large tracking-error handoff"); - VehicleMotionState terminalMeasured = CreateMeasuredState(0.03d, 0d, 3.30d, 0.10d, - effectiveAt.AddSeconds(0.30d), 43L); - AssertHandoffRejected(selector, forward, terminalMeasured, 2, TravelDirection.Forward, - effectiveAt.AddSeconds(0.30d), configuration, TrajectoryHandoffRejectionReason.TerminalBoundary, - "terminal-proximity handoff"); - AssertHandoffRejected(selector, forward, forwardMeasured, 3, TravelDirection.Forward, - effectiveAt.AddSeconds(0.10d), configuration, TrajectoryHandoffRejectionReason.SegmentMismatch, - "segment-mismatch handoff"); - AssertHandoffRejected(selector, forward, forwardMeasured, 2, TravelDirection.Reverse, - effectiveAt.AddSeconds(0.10d), configuration, TrajectoryHandoffRejectionReason.DirectionMismatch, - "direction-mismatch handoff"); - VehicleMotionState beyondMeasured = CreateMeasuredState(0.031d, 0d, 3.3066666666666666d, 0.10d, - effectiveAt.AddSeconds(0.31d), 44L); - AssertHandoffRejected(selector, forward, beyondMeasured, 2, TravelDirection.Forward, - effectiveAt.AddSeconds(0.31d), configuration, TrajectoryHandoffRejectionReason.HandoffBeyondTrajectory, - "beyond-trajectory handoff"); - - EmTrajectory gearBoundary = CreateHandoffTrajectory("handoff-gear", TravelDirection.Forward, 2, effectiveAt, - EmBoundaryType.RollingSafetyStop, true); - AssertHandoffRejected(selector, gearBoundary, forwardMeasured, 2, TravelDirection.Forward, - effectiveAt.AddSeconds(0.10d), configuration, TrajectoryHandoffRejectionReason.GearBoundary, - "gear-boundary handoff"); - Verification.True(!new TrajectorySampler().TrySample(gearBoundary, 0.15d, out _), - "sampler never interpolates across different boundary types"); - - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service); - PlanningCycleInput coordinatorInput = CreateInput(CreateMap(22), "handoff-reference", 40L, "handoff-forward", - 2, "handoff-publish", effectiveAt.AddSeconds(0.10d), configuration, forwardMeasured); - Task publication = coordinator.PlanLatestAsync(coordinatorInput, CancellationToken.None); - service.WaitUntilStarted(coordinatorInput.Request.OutputTrajectoryId); - service.Complete(coordinatorInput.Request.OutputTrajectoryId, forward); - publication.GetAwaiter().GetResult(); - TrajectoryHandoffSelection coordinatorSelection = coordinator.SelectHandoff(coordinatorInput, TravelDirection.Forward); - Verification.Equal(TrajectoryHandoffSource.PreviousTrajectory, coordinatorSelection.Source, - "coordinator consumes only its published immutable trajectory"); - } - - private static void VerifiesPublishedForwardAndReverseCommands() - { - DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(800d); - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service); - var executor = new TrajectoryExecutor(); - - PlanningCycleInput forwardInput = CreateInput(CreateMap(30), "rolling-reference", 90L, string.Empty, 6, - "rolling-forward", effectiveAt, null, CreateMeasuredState(0d, 0d, 0d, 0.10d, effectiveAt, 90L)); - EmTrajectory forward = CreateExecutionTrajectory("rolling-forward", TravelDirection.Forward, effectiveAt, - EmBoundaryType.RollingSafetyStop, EmTerminalType.RollingSafetyStop); - PlanningCycleResult forwardResult = Publish(service, coordinator, forwardInput, forward); - Verification.True(forwardResult.Published, "first forward rolling cycle publishes"); - TrajectoryControlCommand forwardCommand = executor.UpdateCommand(effectiveAt, - forwardInput.Request.VehicleState, coordinator.PublishedTrajectory, TravelDirection.Forward, - TravelDirection.Forward, false); - AssertPublishedMotion(forwardCommand, coordinator.PublishedTrajectory.Points[0], "forward rolling command"); - - PlanningCycleInput forwardRepeat = CreateInput(CreateMap(31), "rolling-reference", 91L, "rolling-forward", 6, - "rolling-forward-repeat", effectiveAt.AddSeconds(0.20d), null, - CreateMeasuredState(0.02d, 0d, 0d, 0.10d, effectiveAt.AddSeconds(0.20d), 91L)); - EmTrajectory repeatedForward = CreateExecutionTrajectory("rolling-forward-repeat", TravelDirection.Forward, - effectiveAt.AddSeconds(0.20d), EmBoundaryType.RollingSafetyStop, EmTerminalType.RollingSafetyStop); - Publish(service, coordinator, forwardRepeat, repeatedForward); - Verification.Equal("rolling-forward-repeat", coordinator.PublishedTrajectory.Metadata.TrajectoryId, - "repeated forward replan replaces only the published trajectory"); - - PlanningCycleInput reverseInput = CreateInput(CreateMap(32), "rolling-reference", 92L, - "rolling-forward-repeat", 7, "rolling-reverse", effectiveAt.AddSeconds(0.40d), null, - CreateMeasuredState(0d, 0d, 0d, -0.10d, effectiveAt.AddSeconds(0.40d), 92L)); - EmTrajectory reverse = CreateExecutionTrajectory("rolling-reverse", TravelDirection.Reverse, - effectiveAt.AddSeconds(0.40d), EmBoundaryType.Goal, EmTerminalType.Goal); - PlanningCycleResult reverseResult = Publish(service, coordinator, reverseInput, reverse); - Verification.True(reverseResult.Published, "reverse rolling cycle publishes"); - TrajectoryControlCommand reverseCommand = executor.UpdateCommand(effectiveAt.AddSeconds(0.40d), - reverseInput.Request.VehicleState, coordinator.PublishedTrajectory, TravelDirection.Reverse, - TravelDirection.Reverse, false); - AssertPublishedMotion(reverseCommand, coordinator.PublishedTrajectory.Points[0], "reverse rolling command"); - Verification.True(reverseCommand.SignedLongitudinalVelocity < 0d, - "reverse rolling command preserves negative longitudinal velocity"); - - PlanningCycleInput unsafeTracking = CreateInput(CreateMap(32), "rolling-reference", 93L, "rolling-reverse", 7, - "unsafe-tracking", effectiveAt.AddSeconds(0.41d), CreateHandoffConfiguration(), - CreateMeasuredState(5d, 0d, 0d, -0.10d, effectiveAt.AddSeconds(0.41d), 93L)); - TrajectoryHandoffSelection reset = coordinator.SelectHandoff(unsafeTracking, TravelDirection.Reverse); - Verification.Equal(TrajectoryHandoffSource.MeasuredState, reset.Source, - "unsafe tracking resets rolling handoff to caller measurement"); - Verification.True(object.ReferenceEquals(unsafeTracking.Request.VehicleState, reset.StartState), - "unsafe tracking preserves the supplied measurement snapshot"); - } - - private static void VerifiesFailureLeavesPublishedZeroSpeedTailExecutable() - { - DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(900d); - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service); - var executor = new TrajectoryExecutor(); - PlanningCycleInput successfulInput = CreateInput(CreateMap(40), "tail-reference", 100L, string.Empty, 8, - "tail-published", effectiveAt, null, CreateMeasuredState(0d, 0d, 0d, 0.10d, effectiveAt, 100L)); - EmTrajectory published = CreateExecutionTrajectory("tail-published", TravelDirection.Forward, effectiveAt, - EmBoundaryType.RollingSafetyStop, EmTerminalType.RollingSafetyStop); - Publish(service, coordinator, successfulInput, published); - - for (int attempt = 1; attempt <= 2; attempt++) - { - DateTimeOffset now = effectiveAt.AddSeconds(0.20d * attempt); - PlanningCycleInput failedInput = CreateInput(CreateMap(40 + attempt), "tail-reference", 100L + attempt, - "tail-published", 8, "tail-failed-" + attempt, now, null, - CreateMeasuredState(0.02d, 0d, 0d, 0.10d, now, 100L + attempt)); - Task failedCycle = coordinator.PlanLatestAsync(failedInput, CancellationToken.None); - service.WaitUntilStarted(failedInput.Request.OutputTrajectoryId); - service.Fail(failedInput.Request.OutputTrajectoryId); - PlanningCycleResult failure = failedCycle.GetAwaiter().GetResult(); - Verification.Equal(EmPlanningStatus.Failed, failure.Result.Status, "failed replan status " + attempt); - Verification.True(!failure.Published, "failed replan cannot publish " + attempt); - Verification.True(object.ReferenceEquals(published, coordinator.PublishedTrajectory), - "failed replan retains the complete prior trajectory " + attempt); - } - - TrajectoryControlCommand beforeTail = executor.UpdateCommand(effectiveAt.AddSeconds(0.10d), - CreateMeasuredState(0.10d, 0d, 0d, 0.10d, effectiveAt.AddSeconds(0.10d), 103L), - coordinator.PublishedTrajectory, TravelDirection.Forward, TravelDirection.Forward, false); - Verification.True(beforeTail.SignedLongitudinalVelocity > 0d, - "old trajectory remains executable before its safety tail"); - - TrajectoryControlCommand atTail = executor.UpdateCommand(effectiveAt.AddSeconds(0.30d), - CreateMeasuredState(0d, 0d, 0d, 0d, effectiveAt.AddSeconds(0.30d), 104L), coordinator.PublishedTrajectory, - TravelDirection.Forward, TravelDirection.Forward, false); - TrajectoryControlCommand afterTail = executor.UpdateCommand(effectiveAt.AddSeconds(10d), - CreateMeasuredState(0d, 0d, 0d, 0d, effectiveAt.AddSeconds(10d), 105L), coordinator.PublishedTrajectory, - TravelDirection.Forward, TravelDirection.Forward, false); - AssertZeroTail(atTail, "exact rolling safety-stop terminal"); - AssertZeroTail(afterTail, "after rolling safety-stop terminal"); - } - - private static PlanningCycleResult Publish(ControlledPlanningService service, EmPlanningCoordinator coordinator, - PlanningCycleInput input, EmTrajectory trajectory) - { - Task cycle = coordinator.PlanLatestAsync(input, CancellationToken.None); - service.WaitUntilStarted(input.Request.OutputTrajectoryId); - service.Complete(input.Request.OutputTrajectoryId, trajectory); - return cycle.GetAwaiter().GetResult(); - } - - private static EmTrajectory CreateExecutionTrajectory(string trajectoryId, TravelDirection direction, - DateTimeOffset effectiveAt, EmBoundaryType terminalBoundary, EmTerminalType terminalType) - { - double signedVelocity = direction == TravelDirection.Forward ? 0.10d : -0.10d; - var metadata = new EmTrajectoryMetadata(trajectoryId, effectiveAt, effectiveAt, 110L, "rolling-reference", 100L, - string.Empty, 8, direction, terminalType, terminalType == EmTerminalType.RollingSafetyStop - ? EmLongitudinalMode.RollingContinuation - : EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(0d, 0d, 0d, signedVelocity, 0d, 0.20d, 8, 0d, 0d, direction, - EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(signedVelocity * 3d, 0d, 0d, 0d, 0.30d, 0.20d, 8, 0.03d, 0.03d, direction, - terminalBoundary, 0d, 0d), - }); - } - - private static void AssertPublishedMotion(TrajectoryControlCommand command, EmTrajectoryPoint point, string name) - { - Verification.NearlyEqual(point.SignedLongitudinalVelocity, command.SignedLongitudinalVelocity, - name + " originates from the currently published trajectory"); - Verification.NearlyEqual(point.YawRate, command.YawRate, - name + " preserves the currently published trajectory yaw rate"); - Verification.True(!command.HoldBrake && !command.IsTrajectoryComplete, - name + " is neither a brake hold nor an extrapolated completion command"); - } - - private static void AssertZeroTail(TrajectoryControlCommand command, string name) - { - Verification.NearlyEqual(0d, command.SignedLongitudinalVelocity, name + " signed velocity"); - Verification.NearlyEqual(0d, command.YawRate, name + " yaw rate"); - Verification.True(command.HoldBrake && command.IsTrajectoryComplete, name + " holds a completed trajectory"); - } - - private static void AssertHandoffRejected(TrajectoryHandoffSelector selector, EmTrajectory trajectory, - VehicleMotionState measuredState, int segmentIndex, TravelDirection direction, DateTimeOffset now, - EmPlannerConfiguration configuration, TrajectoryHandoffRejectionReason reason, string name) - { - TrajectoryHandoffSelection selection = selector.Select(trajectory, measuredState, segmentIndex, direction, now, - configuration); - Verification.Equal(TrajectoryHandoffSource.MeasuredState, selection.Source, name + " source"); - Verification.Equal(reason, selection.RejectionReason, name + " reason"); - Verification.True(selection.PreviousTrajectory == null, name + " has no seed"); - Verification.True(object.ReferenceEquals(measuredState, selection.StartState), name + " returns measured state"); - } - - private static EmPlannerConfiguration CreateHandoffConfiguration() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Validation.SpatialToleranceMeters = 0.05d; - configuration.Validation.KinematicTolerance = 0.05d; - return configuration; - } - - private static VehicleMotionState CreateMeasuredState(double x, double y, double yaw, double signedSpeed, - DateTimeOffset capturedAt, long sequenceId) - { - return new VehicleMotionState(new Pose2D(x, y, yaw), signedSpeed, 0d, capturedAt, sequenceId); - } - - private static EmTrajectory CreateHandoffTrajectory(string trajectoryId, TravelDirection direction, int segmentIndex, - DateTimeOffset effectiveAt, EmBoundaryType terminalBoundary, bool includeGearBoundary) - { - double sign = direction == TravelDirection.Forward ? 1d : -1d; - EmBoundaryType middleBoundary = includeGearBoundary ? EmBoundaryType.GearSwitchApproach : EmBoundaryType.None; - var metadata = new EmTrajectoryMetadata(trajectoryId, effectiveAt, effectiveAt, 55L, "handoff-reference", 39L, - string.Empty, segmentIndex, direction, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(0d, 0d, 3.10d, sign * 0.10d, 0d, 0.25d, segmentIndex, 0d, 0d, - direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.01d, 0d, 3.1666666666666665d, sign * 0.10d, 0.10d, 0.25d, - segmentIndex, 0.01d, 0.01d, direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.03d, 0d, 3.30d, sign * 0.10d, 0.30d, 0.25d, segmentIndex, 0.03d, - 0.03d, direction, middleBoundary, 0d, 0d), - new EmTrajectoryPoint(sign * 0.055d, 0d, 3.4666666666666668d, sign * 0.10d, 0.55d, 0.25d, - segmentIndex, 0.055d, 0.055d, direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.06d, 0d, 3.50d, 0d, 0.60d, 0.25d, segmentIndex, 0.06d, 0.06d, - direction, terminalBoundary, 0d, 0d), - }); - } - - private static void VerifySuperseded(string name, PlanningCycleInput older, PlanningCycleInput newer) - { - var service = new ControlledPlanningService(); - var coordinator = new EmPlanningCoordinator(service); - - Task slow = coordinator.PlanLatestAsync(older, CancellationToken.None); - service.WaitUntilStarted(older.Request.OutputTrajectoryId); - Task current = coordinator.PlanLatestAsync(newer, CancellationToken.None); - service.WaitUntilStarted(newer.Request.OutputTrajectoryId); - - Verification.True(service.WasCancelled(older.Request.OutputTrajectoryId), name + " cancels older cycle token"); - service.Complete(newer.Request.OutputTrajectoryId); - PlanningCycleResult currentResult = current.GetAwaiter().GetResult(); - service.Complete(older.Request.OutputTrajectoryId); - PlanningCycleResult staleResult = slow.GetAwaiter().GetResult(); - - Verification.Equal(EmPlanningStatus.Success, currentResult.Result.Status, name + " current result succeeds"); - Verification.True(currentResult.Published, name + " current result publishes"); - Verification.Equal(EmPlanningStatus.Superseded, staleResult.Result.Status, name + " old result is superseded"); - Verification.True(staleResult.Result.Trajectory == null, name + " stale result exposes no trajectory"); - Verification.Equal(newer.Request.OutputTrajectoryId, coordinator.PublishedTrajectory.Metadata.TrajectoryId, - name + " only latest trajectory is published"); - } - - private static PlanningCycleInput CreateInput(PlanningGridMap map, string referencePathId, long stateSequenceId, - string previousTrajectoryId, int segmentIndex, string outputTrajectoryId, DateTimeOffset now, - EmPlannerConfiguration? configuration = null, VehicleMotionState? state = null) - { - configuration ??= EmPlannerConfiguration.CreateDefault(); - state ??= new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0d, 0d, now, stateSequenceId); - var request = new EmPlanningRequest(null, map, null, state, configuration, segmentIndex, null, now, now, - outputTrajectoryId, referencePathId, previousTrajectoryId, EmMotionModel.NonholonomicForwardReverse, - EmPlanningScope.RollingHorizon); - return new PlanningCycleInput(request, now); - } - - private static PlanningGridMap CreateMap(int widthOffset) - { - PlanningMapBuildResult build = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(-1000f, 1000f + widthOffset * 20f, -1000f, 1000f), - ResolutionMm = 20f, - ObstacleSources = Array.Empty(), - AllowExplicitEmptyMap = true, - }); - Verification.True(build.Succeeded && build.Map != null && build.Map.PlanningReady, "coordinator map builds"); - return build.Map!; - } - - private sealed class ControlledPlanningService : IEmPlanningService - { - private readonly object gate = new object(); - private readonly Dictionary pending = new Dictionary(); - - public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) - { - var cycle = new PendingCycle(request, cancellationToken); - lock (gate) - { - pending.Add(request.OutputTrajectoryId, cycle); - Monitor.PulseAll(gate); - } - return cycle.Completion.Task.GetAwaiter().GetResult(); - } - - public void WaitUntilStarted(string outputTrajectoryId) - { - DateTimeOffset timeout = DateTimeOffset.UtcNow.AddSeconds(5d); - lock (gate) - { - while (!pending.ContainsKey(outputTrajectoryId)) - { - TimeSpan remaining = timeout - DateTimeOffset.UtcNow; - if (remaining <= TimeSpan.Zero || !Monitor.Wait(gate, remaining)) - throw new InvalidOperationException("Planner cycle did not start: " + outputTrajectoryId); - } - } - } - - public bool WasCancelled(string outputTrajectoryId) - { - lock (gate) - return pending[outputTrajectoryId].CancellationToken.IsCancellationRequested; - } - - public void Complete(string outputTrajectoryId, EmTrajectory? trajectory = null) - { - PendingCycle cycle; - lock (gate) - cycle = pending[outputTrajectoryId]; - EmPlanningResult result = trajectory == null - ? CreateSuccess(cycle.Request) - : new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty); - cycle.Completion.TrySetResult(result); - } - - public void Fail(string outputTrajectoryId) - { - PendingCycle cycle; - lock (gate) - cycle = pending[outputTrajectoryId]; - cycle.Completion.TrySetResult(new EmPlanningResult(EmPlanningStatus.Failed, null, "scripted rolling failure")); - } - - private static EmPlanningResult CreateSuccess(EmPlanningRequest request) - { - var metadata = new EmTrajectoryMetadata(request.OutputTrajectoryId, request.RequestedAtUtc, request.EffectiveAtUtc, - request.Map.SnapshotId, request.ReferencePathId, request.VehicleState.SequenceId, request.PreviousTrajectoryId, - request.SegmentIndex, TravelDirection.Forward, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, request.PlanningScope); - var point = new EmTrajectoryPoint(0d, 0d, 0d, 0d, 0d, 0d, request.SegmentIndex, 0d, 0d, - TravelDirection.Forward, EmBoundaryType.RollingSafetyStop, 0d, 0d); - return new EmPlanningResult(EmPlanningStatus.Success, new EmTrajectory(metadata, new[] { point }), string.Empty); - } - - private sealed class PendingCycle - { - public PendingCycle(EmPlanningRequest request, CancellationToken cancellationToken) - { - Request = request; - CancellationToken = cancellationToken; - Completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } - - public EmPlanningRequest Request { get; } - public CancellationToken CancellationToken { get; } - public TaskCompletionSource Completion { get; } - } - } - - private sealed class ThrowingCycleSink : IEmPlanningCycleSink - { - public void OnCycleCompleted(PlanningCycleResult result) - { - throw new InvalidOperationException("cycle sink failure"); - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/CorridorChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/CorridorChecks.cs deleted file mode 100644 index 4d64dd7..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/CorridorChecks.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class CorridorChecks -{ - public static void Run() - { - VerifiesEmptyAndNarrowedCorridors(); - VerifiesSeedConnectedIntervalIsPreserved(); - VerifiesDisappearingSeedIntervalFails(); - } - - private static void VerifiesEmptyAndNarrowedCorridors() - { - DirectionSegmentView segment = CreateStraightSegment(); - CorridorConfiguration configuration = EmPlannerConfiguration.CreateDefault().Corridor; - VehicleParameters vehicle = CreateVehicle(); - var builder = new StaticCorridorBuilder(); - - Verification.True(builder.TryBuild(segment, 0.2d, 1.8d, Array.Empty(), - CreateMap(Array.Empty()), vehicle, configuration, out StaticCorridor empty, out string emptyReason), - "empty map corridor succeeds: " + emptyReason); - VerifyAnchors(empty, 0.2d, 1.8d); - for (int index = 1; index + 1 < empty.Stations.Count; index++) - { - Verification.NearlyEqual(-0.3d, empty.Stations[index].MinimumL, "empty map minimum l"); - Verification.NearlyEqual(0.3d, empty.Stations[index].MaximumL, "empty map maximum l"); - } - - IMapObstacle[] leftNarrowing = - { - new AxisAlignedRectangleObstacle(800f, 1200f, 150f, 400f), - }; - PlanningGridMap narrowedMap = CreateMap(leftNarrowing); - IReadOnlyList seed = CreateSeed(segment, -0.2d); - Verification.True(builder.TryBuild(segment, 0.2d, 1.8d, seed, narrowedMap, vehicle, configuration, - out StaticCorridor narrowed, out string narrowedReason), "narrowed corridor succeeds: " + narrowedReason); - LateralInterval narrowedStation = FindStation(narrowed, 1d); - Verification.True(narrowedStation.MaximumL < 0.3d, "left obstacle narrows positive-l side"); - VerifyAcceptedSamplesUseExactFootprints(segment, narrowed, narrowedMap, vehicle, configuration); - } - - private static void VerifiesSeedConnectedIntervalIsPreserved() - { - DirectionSegmentView segment = CreateStraightSegment(); - CorridorConfiguration configuration = EmPlannerConfiguration.CreateDefault().Corridor; - VehicleParameters vehicle = CreateVehicle(); - IMapObstacle[] split = - { - new AxisAlignedRectangleObstacle(800f, 1200f, -50f, 50f), - }; - IReadOnlyList seed = CreateSeed(segment, -0.2d); - var builder = new StaticCorridorBuilder(); - - Verification.True(builder.TryBuild(segment, 0.2d, 1.8d, seed, CreateMap(split), vehicle, configuration, - out StaticCorridor corridor, out string reason), "split corridor succeeds for left seed: " + reason); - for (int index = 0; index < corridor.Stations.Count; index++) - { - LateralInterval station = corridor.Stations[index]; - Verification.True(station.MinimumL <= station.SeedL && station.SeedL <= station.MaximumL, - "chosen interval contains seed at station " + index); - } - Verification.True(FindStation(corridor, 1d).MaximumL < 0d, - "split station retains seed-connected left interval instead of right interval"); - } - - private static void VerifiesDisappearingSeedIntervalFails() - { - DirectionSegmentView segment = CreateStraightSegment(); - CorridorConfiguration configuration = EmPlannerConfiguration.CreateDefault().Corridor; - VehicleParameters vehicle = CreateVehicle(); - IMapObstacle[] removesLeft = - { - new AxisAlignedRectangleObstacle(800f, 1200f, -400f, -50f), - }; - var builder = new StaticCorridorBuilder(); - - Verification.True(!builder.TryBuild(segment, 0.2d, 1.8d, CreateSeed(segment, -0.2d), CreateMap(removesLeft), - vehicle, configuration, out _, out string failureReason), - "disappearing seed-connected interval does not switch sides"); - Verification.True(failureReason.IndexOf("S=", StringComparison.Ordinal) >= 0, - "failure identifies the first failed reference-s station: " + failureReason); - } - - private static void VerifyAnchors(StaticCorridor corridor, double startS, double endS) - { - Verification.NearlyEqual(startS, corridor.Stations[0].ReferenceS, "first station is exact requested start"); - Verification.NearlyEqual(endS, corridor.Stations[corridor.Stations.Count - 1].ReferenceS, - "last station is exact requested end"); - } - - private static void VerifyAcceptedSamplesUseExactFootprints(DirectionSegmentView segment, StaticCorridor corridor, - PlanningGridMap map, VehicleParameters vehicle, CorridorConfiguration configuration) - { - var checker = new FootprintCollisionChecker(); - for (int stationIndex = 0; stationIndex < corridor.Stations.Count; stationIndex++) - { - LateralInterval station = corridor.Stations[stationIndex]; - FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(segment, station.ReferenceS); - for (double l = station.MinimumL; l <= station.MaximumL + 1e-12d; l += configuration.LateralSampleSpacingMeters) - { - Verification.True(FrenetTransform.TryReconstruct(reference, l, 0d, 0.2d, out Pose2D pose), - "accepted sample reconstructs"); - Verification.True(checker.IsPoseCollisionFree(pose, map, vehicle, configuration.AdditionalClearanceReserveMeters, - out _), "accepted sample passes exact rotated footprint"); - } - } - } - - private static DirectionSegmentView CreateStraightSegment() - { - var points = new List - { - Point(0d, 0d), - Point(1d, 1d), - Point(2d, 2d), - }; - return new DirectionSegmentView(0, TravelDirection.Forward, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, 2d, EmBoundaryType.Goal, 2d), 0d); - } - - private static IReadOnlyList CreateSeed(DirectionSegmentView segment, double l) - { - return new FrenetProjection[] - { - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0.2d), l, 0d, 0d), - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 1.8d), l, 0d, 0d), - }; - } - - private static SmoothedPathPoint Point(double x, double s) - { - return new SmoothedPathPoint(x, 0d, 0d, 0d, s, TravelDirection.Forward, 0d, 0d, 0d, 1d, - false, SmoothedPathPointSource.Anchor); - } - - private static VehicleParameters CreateVehicle() - { - return new VehicleParameters - { - LengthMeters = 0.10d, - WidthMeters = 0.10d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 1d, - }; - } - - private static PlanningGridMap CreateMap(IReadOnlyList obstacles) - { - IMapObstacleSource[] sources = obstacles.Count == 0 - ? Array.Empty() - : new IMapObstacleSource[] { new ManualObstacleSource("corridor-test", 1L, true, obstacles) }; - var result = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(-1000f, 3000f, -1000f, 1000f), - ResolutionMm = 20f, - ObstacleSources = sources, - AllowExplicitEmptyMap = obstacles.Count == 0, - }); - Verification.True(result.Succeeded && result.Map != null && result.Map.PlanningReady, - "corridor test map builds: " + result.FailureReason); - return result.Map!; - } - - private static LateralInterval FindStation(StaticCorridor corridor, double referenceS) - { - for (int index = 0; index < corridor.Stations.Count; index++) - { - if (Math.Abs(corridor.Stations[index].ReferenceS - referenceS) <= 1e-12d) - return corridor.Stations[index]; - } - throw new InvalidOperationException("Requested corridor station was not sampled."); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj b/ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj deleted file mode 100644 index c91072e..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - Exe - net10.0-windows - disable - enable - - - - - diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs deleted file mode 100644 index 9107273..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace EMPlannerVerificationHost; - -internal static class EmFixtureFactory -{ - public static PathSmoothingResult CreateGearPairReferencePath(double firstSegmentLength = 2d) - { - if (firstSegmentLength <= 0d) - throw new ArgumentOutOfRangeException(nameof(firstSegmentLength)); - double midpoint = 0.5d * firstSegmentLength; - var points = new List - { - Point(0d, 0d, TravelDirection.Forward, false, SmoothedPathPointSource.Anchor), - Point(midpoint, midpoint, TravelDirection.Forward, false, SmoothedPathPointSource.Anchor), - Point(firstSegmentLength, firstSegmentLength, TravelDirection.Forward, false, SmoothedPathPointSource.Anchor), - Point(firstSegmentLength, firstSegmentLength, TravelDirection.Reverse, true, SmoothedPathPointSource.GearSwitch), - Point(midpoint, firstSegmentLength + midpoint, TravelDirection.Reverse, false, SmoothedPathPointSource.Anchor), - Point(0d, 2d * firstSegmentLength, TravelDirection.Reverse, false, SmoothedPathPointSource.Anchor), - }; - var segments = new List - { - new SmoothedPathSegment(0, TravelDirection.Forward, 0, 2, false, true), - new SmoothedPathSegment(1, TravelDirection.Reverse, 3, 5, true, false), - }; - var metrics = new PathQualityMetrics(true, 2d * firstSegmentLength, 0d, 0d, 0d, 0d, 1d, 0d, 0d, 0d, 0d, 0d); - return PathSmoothingResult.PublishLocalG2(PathSmoothingStatus.Complete, points, segments, - new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); - } - - private static SmoothedPathPoint Point( - double x, - double arcLength, - TravelDirection direction, - bool isGearSwitchPoint, - SmoothedPathPointSource source) - { - return new SmoothedPathPoint(x, 0d, 0d, 0d, arcLength, direction, 0d, 0d, 0d, 1d, - isGearSwitchPoint, source); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs deleted file mode 100644 index c569baa..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs +++ /dev/null @@ -1,1092 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class EmPlanningServiceChecks -{ - public static void Run() - { - VerifiesPreviousTrajectoryIsALongitudinalSoftReference(); - VerifiesForwardReverseAndBoundarySuccessesAreDeterministic(); - VerifiesServicePublishesRollingApproachAndExactStopModes(); - VerifiesFullScopePublishesItsRequestScope(); - VerifiesFullScopeAdmitsOnlyTheTrueSegmentStart(); - VerifiesRequestAndStateFailuresPublishNoTrajectory(); - VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory(); - VerifiesNoProgressPublishesNoTrajectory(); - VerifiesTimeoutFallbackAndCancellationSemantics(); - VerifiesLsAndStShareOneSolveBudget(); - VerifiesPublicationFailureAndDebugIsolation(); - VerifiesPublicationGateStatusMappings(); - } - - private static void VerifiesForwardReverseAndBoundarySuccessesAreDeterministic() - { - EmPlanningRequest forwardRequest = CreateRequest(TravelDirection.Forward, 0d, false, false); - var forwardService = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)); - EmPlanningResult firstForward = forwardService.Plan(forwardRequest, CancellationToken.None); - EmPlanningResult secondForward = forwardService.Plan(forwardRequest, CancellationToken.None); - VerifySuccess(firstForward, forwardRequest, EmTerminalType.Goal, "forward"); - VerifySuccess(secondForward, forwardRequest, EmTerminalType.Goal, "forward repeat"); - VerifySameTrajectory(firstForward, secondForward, "forward deterministic result"); - Verification.Equal(2, forwardRequest.ReferencePath.Path.Count, "request-owned reference list remains unchanged"); - - EmPlanningRequest reverseRequest = CreateRequest(TravelDirection.Reverse, -0.01d, false, false); - EmPlanningResult reverse = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(reverseRequest, - CancellationToken.None); - VerifySuccess(reverse, reverseRequest, EmTerminalType.Goal, "reverse"); - Verification.True(reverse.Trajectory.Points[1].SignedLongitudinalVelocity < 0d, - "reverse service publishes negative signed velocity"); - - EmPlanningRequest gearRequest = CreateRequest(TravelDirection.Forward, 0d, true, false); - EmPlanningResult gear = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(gearRequest, - CancellationToken.None); - VerifySuccess(gear, gearRequest, EmTerminalType.GearSwitch, "gear switch"); - - EmPlanningRequest rollingRequest = CreateRequest(TravelDirection.Forward, 0d, false, true); - EmPlanningResult rolling = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(rollingRequest, - CancellationToken.None); - VerifySuccess(rolling, rollingRequest, EmTerminalType.RollingSafetyStop, "rolling stop"); - } - - private static void VerifiesServicePublishesRollingApproachAndExactStopModes() - { - EmPlanningRequest rollingRequest = CreateRequest(TravelDirection.Forward, 0.10d, false, false, - CreateReferencePath(TravelDirection.Forward, false, 10d), CreateMap(false, 12d)); - ConfigureFiveMeterWindowAndTwoSecondHorizon(rollingRequest.Configuration); - EmPlanningResult rolling = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( - rollingRequest, CancellationToken.None); - VerifySuccess(rolling, rollingRequest, EmTerminalType.RollingSafetyStop, "cycle 1 rolling"); - Verification.Equal(EmLongitudinalMode.RollingContinuation, - rolling.Trajectory.Metadata.LongitudinalMode, "cycle 1 rolls"); - Verification.Equal(21, rolling.Trajectory.Points.Count, "rolling publishes the two-second ST knot count"); - EmTrajectoryPoint rollingTerminal = rolling.Trajectory.Points[rolling.Trajectory.Points.Count - 1]; - Verification.True(rollingTerminal.PathS < 5d, "two-second ST output remains inside the five-metre LS window"); - Verification.True(rollingTerminal.SignedLongitudinalVelocity != 0d, "cycle 1 has nonzero terminal speed"); - - EmPlanningRequest approachRequest = CreateRequest(TravelDirection.Forward, 0.10d, false, false, - CreateReferencePath(TravelDirection.Forward, false, 4d), CreateMap(false, 5d)); - EmPlanningResult approach = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( - approachRequest, CancellationToken.None); - VerifySuccess(approach, approachRequest, EmTerminalType.Goal, "cycle 2 approach"); - Verification.Equal(EmLongitudinalMode.ApproachStopBoundary, - approach.Trajectory.Metadata.LongitudinalMode, "cycle 2 approaches"); - Verification.True(approach.Trajectory.Points[approach.Trajectory.Points.Count - 1].SignedLongitudinalVelocity != 0d, - "approach has no synthetic stop tail"); - - EmPlanningRequest exactRequest = CreateRequest(TravelDirection.Forward, 0.05d, false, false, - CreateReferencePath(TravelDirection.Forward, false, 0.0075d)); - ConfigureExactStopServiceScenario(exactRequest.Configuration); - EmPlanningResult exact = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( - exactRequest, CancellationToken.None); - VerifySuccess(exact, exactRequest, EmTerminalType.Goal, "cycle 3 exact goal stop"); - Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, - exact.Trajectory.Metadata.LongitudinalMode, "cycle 3 stops"); - AssertExactStopStabilization(exact.Trajectory, EmBoundaryType.Goal, "goal"); - - EmPlanningRequest gearRequest = CreateRequest(TravelDirection.Forward, 0.05d, false, false, - EmFixtureFactory.CreateGearPairReferencePath(0.0075d)); - ConfigureExactStopServiceScenario(gearRequest.Configuration); - EmPlanningResult gear = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( - gearRequest, CancellationToken.None); - VerifySuccess(gear, gearRequest, EmTerminalType.GearSwitch, "gear-switch exact stop"); - Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, - gear.Trajectory.Metadata.LongitudinalMode, "gear switch stops exactly"); - AssertExactStopStabilization(gear.Trajectory, EmBoundaryType.GearSwitchApproach, "gear switch"); - for (int index = 0; index < gear.Trajectory.Points.Count; index++) - { - Verification.Equal(TravelDirection.Forward, gear.Trajectory.Points[index].Direction, - "gear-switch publication excludes the next direction point " + index); - } - } - - private static void VerifiesFullScopePublishesItsRequestScope() - { - EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0.05d, false, false, - CreateReferencePath(TravelDirection.Forward, false, 0.0075d), null, - EmPlanningScope.FullDirectionSegment); - ConfigureExactStopServiceScenario(request.Configuration); - double[] strictFullPrimal = CreateStrictFullScopePrimal(request.Configuration); - EmPlanningResult result = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success, null, - strictFullPrimal)).Plan( - request, CancellationToken.None); - VerifySuccess(result, request, EmTerminalType.Goal, "full scope publication"); - Verification.Equal(EmPlanningScope.FullDirectionSegment, result.Trajectory.Metadata.PlanningScope, - "service metadata preserves full scope"); - Verification.NearlyEqual(0d, result.Trajectory.Points[0].PathS, "full publication starts at projection"); - Verification.NearlyEqual(0.0075d, result.Trajectory.Points[result.Trajectory.Points.Count - 1].PathS, - "full publication reaches the real segment boundary"); - } - - private static void VerifiesFullScopeAdmitsOnlyTheTrueSegmentStart() - { - EmPlanningRequest uRequest = CreateRequest(TravelDirection.Forward, 0d, false, false, - CreateAllForwardUReferencePath(), CreateMap(false, 12d), EmPlanningScope.FullDirectionSegment); - DateTimeOffset capturedAt = uRequest.RequestedAtUtc; - uRequest = ReplaceState(uRequest, new VehicleMotionState( - new Pose2D(5d, 0.4d, Math.PI), 0d, 0d, capturedAt, 8L)); - - EmPlanningResult laterBranch = new EmPlanningService( - new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(uRequest, CancellationToken.None); - - VerifyFailure(laterBranch, EmPlanningStatus.ProjectionFailed, - "FullDirection later U branch is not an admissible segment start"); - - EmPlanningRequest oppositeHeading = CreateRequest(TravelDirection.Forward, 0d, false, false, - planningScope: EmPlanningScope.FullDirectionSegment); - oppositeHeading = ReplaceState(oppositeHeading, new VehicleMotionState( - new Pose2D(0d, 0d, Math.PI), 0d, 0d, oppositeHeading.RequestedAtUtc, 9L)); - - EmPlanningResult foldedHeading = new EmPlanningService( - new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(oppositeHeading, CancellationToken.None); - - VerifyFailure(foldedHeading, EmPlanningStatus.ProjectionFailed, - "FullDirection opposite heading is rejected before tangent slope conversion"); - } - - private static void ConfigureFiveMeterWindowAndTwoSecondHorizon(EmPlannerConfiguration configuration) - { - configuration.Scheduling.DistanceHorizonMeters = 5d; - configuration.Scheduling.TimeHorizonSeconds = 2d; - configuration.Scheduling.OutputTimeStepSeconds = 0.1d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.2d; - } - - private static void ConfigureExactStopServiceScenario(EmPlannerConfiguration configuration) - { - configuration.Scheduling.TimeHorizonSeconds = 0.40d; - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 20d; - configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; - } - - private static double[] CreateStrictFullScopePrimal(EmPlannerConfiguration configuration) - { - var path = new LateralPath(new[] - { - new LateralPathPoint(0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d), - new LateralPathPoint(0.0075d, 0.0075d, 0d, 0d, 0d, 0d, 0.0075d, 0d, 0d, 0d, 0d, 0d), - }, true); - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.05d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "full scope test envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.05d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule schedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "full scope test schedule: " + failureReason); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.05d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, schedule, Array.Empty(), Array.Empty()); - int motionIntervalCount = schedule.TerminalHoldStartIndex; - Verification.True(motionIntervalCount >= 3, "full scope test schedule has three motion intervals"); - int terminalFirstInterval = motionIntervalCount - 3; - var terminalTimes = new double[4]; - for (int index = 1; index < terminalTimes.Length; index++) - terminalTimes[index] = terminalTimes[index - 1] + - schedule.KnotTimes[terminalFirstInterval + index] - - schedule.KnotTimes[terminalFirstInterval + index - 1]; - var motionTimes = new double[motionIntervalCount + 1]; - for (int index = 0; index < motionTimes.Length; index++) - motionTimes[index] = schedule.KnotTimes[index]; - var influence = new double[3, 3]; - for (int interval = 0; interval < 3; interval++) - { - var basis = new double[3]; - basis[interval] = 1d; - LongitudinalCandidate response = LongitudinalCandidate.Integrate(terminalTimes, 0d, 0d, 0d, basis); - int terminalIndex = response.S.Count - 1; - influence[0, interval] = response.A[terminalIndex]; - influence[1, interval] = response.U[terminalIndex]; - influence[2, interval] = response.S[terminalIndex]; - } - var validator = new LongitudinalSolutionValidator(); - for (int firstJerkStep = -20; firstJerkStep <= 0; firstJerkStep++) - { - for (int secondJerkStep = terminalFirstInterval >= 2 ? -20 : 0; - secondJerkStep <= (terminalFirstInterval >= 2 ? 20 : 0); secondJerkStep++) - { - for (int thirdJerkStep = terminalFirstInterval >= 3 ? -20 : 0; - thirdJerkStep <= (terminalFirstInterval >= 3 ? 20 : 0); thirdJerkStep++) - { - var jerk = new double[motionIntervalCount]; - jerk[0] = firstJerkStep; - if (terminalFirstInterval >= 2) - jerk[1] = secondJerkStep; - if (terminalFirstInterval >= 3) - jerk[2] = thirdJerkStep; - LongitudinalCandidate baseline = LongitudinalCandidate.Integrate(motionTimes, 0d, 0.05d, - 0d, jerk); - double[] target = - { - -baseline.A[baseline.A.Count - 1], - -baseline.U[baseline.U.Count - 1], - 0.0075d - baseline.S[baseline.S.Count - 1], - }; - if (!TrySolveThreeByThree(influence, target, out double[] terminalJerk)) - throw new InvalidOperationException("Full scope strict candidate terminal system is singular."); - for (int interval = 0; interval < 3; interval++) - jerk[terminalFirstInterval + interval] = terminalJerk[interval]; - LongitudinalCandidate motion = LongitudinalCandidate.Integrate(motionTimes, 0d, 0.05d, 0d, - jerk); - LongitudinalCandidate candidate = AppendFullStopTail(schedule.KnotTimes, motionIntervalCount, - motion); - if (!validator.TryValidate(input, speedLimit, candidate, out LongitudinalCandidate strict, out _)) - continue; - return ToPrimal(strict); - } - } - } - throw new InvalidOperationException("Unable to construct a strict full-scope test candidate: hold=" + - motionIntervalCount + ";times=" + string.Join(",", schedule.KnotTimes)); - } - - private static LongitudinalCandidate AppendFullStopTail(IReadOnlyList times, int motionIntervalCount, - LongitudinalCandidate motion) - { - var pathS = new double[times.Count]; - var speed = new double[times.Count]; - var acceleration = new double[times.Count]; - var jerk = new double[times.Count - 1]; - for (int index = 0; index <= motionIntervalCount; index++) - { - pathS[index] = index == motionIntervalCount ? 0.0075d : motion.S[index]; - speed[index] = index == motionIntervalCount ? 0d : motion.U[index]; - acceleration[index] = index == motionIntervalCount ? 0d : motion.A[index]; - } - for (int index = motionIntervalCount + 1; index < times.Count; index++) - pathS[index] = 0.0075d; - for (int index = 0; index < motion.J.Count; index++) - jerk[index] = motion.J[index]; - return new LongitudinalCandidate(times, pathS, speed, acceleration, jerk); - } - - private static double[] ToPrimal(LongitudinalCandidate candidate) - { - var layout = new LongitudinalVariableLayout(candidate.S.Count); - var primal = new double[layout.VariableCount]; - for (int index = 0; index < candidate.S.Count; index++) - { - primal[layout.S(index)] = candidate.S[index]; - primal[layout.U(index)] = candidate.U[index]; - primal[layout.A(index)] = candidate.A[index]; - } - for (int index = 0; index < candidate.J.Count; index++) - primal[layout.J(index)] = candidate.J[index]; - return primal; - } - - private static bool TrySolveThreeByThree(double[,] matrix, IReadOnlyList rightHandSide, - out double[] solution) - { - var augmented = new double[3, 4]; - for (int row = 0; row < 3; row++) - { - for (int column = 0; column < 3; column++) - augmented[row, column] = matrix[row, column]; - augmented[row, 3] = rightHandSide[row]; - } - for (int column = 0; column < 3; column++) - { - int pivot = column; - for (int row = column + 1; row < 3; row++) - { - if (Math.Abs(augmented[row, column]) > Math.Abs(augmented[pivot, column])) - pivot = row; - } - if (Math.Abs(augmented[pivot, column]) < 1e-12d) - { - solution = Array.Empty(); - return false; - } - if (pivot != column) - { - for (int index = column; index < 4; index++) - { - double temporary = augmented[column, index]; - augmented[column, index] = augmented[pivot, index]; - augmented[pivot, index] = temporary; - } - } - double divisor = augmented[column, column]; - for (int index = column; index < 4; index++) - augmented[column, index] /= divisor; - for (int row = 0; row < 3; row++) - { - if (row == column) - continue; - double factor = augmented[row, column]; - for (int index = column; index < 4; index++) - augmented[row, index] -= factor * augmented[column, index]; - } - } - solution = new[] { augmented[0, 3], augmented[1, 3], augmented[2, 3] }; - return true; - } - - private static void AssertExactStopStabilization(EmTrajectory trajectory, EmBoundaryType boundaryType, string name) - { - int exactAnchor = -1; - for (int index = 0; index < trajectory.Points.Count; index++) - { - if (trajectory.Points[index].BoundaryType == boundaryType) - { - exactAnchor = index; - break; - } - } - Verification.True(exactAnchor >= 0, name + " has a real boundary anchor"); - Verification.NearlyEqual(0d, trajectory.Points[exactAnchor].SignedLongitudinalVelocity, - name + " speed is zero"); - Verification.True(trajectory.Points.Count > exactAnchor + 1, - name + " anchor is followed by a QP stabilization point"); - Verification.NearlyEqual(trajectory.Points[exactAnchor].PathS, trajectory.Points[exactAnchor + 1].PathS, - name + " stabilization keeps the stop position"); - Verification.NearlyEqual(0d, trajectory.Points[exactAnchor + 1].SignedLongitudinalVelocity, - name + " stabilization speed is zero"); - } - - private static void VerifiesRequestAndStateFailuresPublishNoTrajectory() - { - EmPlanningRequest invalidSmoothing = CreateRequest(TravelDirection.Forward, 0d, false, false, - PathSmoothingResult.Failure(PathSmoothingStatus.Failed, new PathSmoothingDiagnostics())); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(invalidSmoothing, - CancellationToken.None), EmPlanningStatus.InvalidReferencePath, "invalid smoothing"); - - EmPlanningRequest stale = CreateRequest(TravelDirection.Forward, 0d, false, false); - stale = ReplaceState(stale, new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0d, null, - stale.RequestedAtUtc.AddSeconds(-1d), stale.VehicleState.SequenceId)); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(stale, - CancellationToken.None), EmPlanningStatus.StaleVehicleState, "stale state"); - - EmPlanningRequest directionMismatch = CreateRequest(TravelDirection.Reverse, 0.02d, false, false); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(directionMismatch, - CancellationToken.None), EmPlanningStatus.StateDirectionMismatch, "state direction mismatch"); - } - - private static void VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory() - { - EmPlanningRequest projectionFailure = CreateRequest(TravelDirection.Forward, 0d, false, false); - projectionFailure = ReplaceState(projectionFailure, new VehicleMotionState(new Pose2D(3d, 0d, 0d), 0d, null, - projectionFailure.RequestedAtUtc, projectionFailure.VehicleState.SequenceId)); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(projectionFailure, - CancellationToken.None), EmPlanningStatus.ProjectionFailed, "bounded projection failure"); - - EmPlanningRequest corridorFailure = CreateRequest(TravelDirection.Forward, 0d, false, false, - referencePath: null, map: CreateMap(true)); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(corridorFailure, - CancellationToken.None), EmPlanningStatus.CorridorInfeasible, "corridor infeasible"); - - EmPlanningRequest stoppingFailure = CreateRequest(TravelDirection.Forward, 0.20d, false, false, - referencePath: CreateReferencePath(TravelDirection.Forward, false, 0.0055d)); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(stoppingFailure, - CancellationToken.None), EmPlanningStatus.StoppingDistanceInsufficient, "stopping distance insufficient"); - - EmPlanningRequest regular = CreateRequest(TravelDirection.Forward, 0d, false, false); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.LateralInfeasible)).Plan(regular, - CancellationToken.None), EmPlanningStatus.LateralInfeasible, "lateral infeasible"); - EmPlanningResult longitudinalFallback = new EmPlanningService( - new ScriptedPipelineSolver(PipelineSolverMode.LongitudinalInfeasible)).Plan(regular, CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, longitudinalFallback.Status, - "longitudinal infeasible uses the validated fallback seed"); - Verification.True(longitudinalFallback.Trajectory != null, - "longitudinal fallback still publishes a complete trajectory"); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.SolverUnavailable)).Plan(regular, - CancellationToken.None), EmPlanningStatus.SolverUnavailable, "solver unavailable"); - } - - private static void VerifiesTimeoutFallbackAndCancellationSemantics() - { - EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.TimeoutWithoutFallback)).Plan(request, - CancellationToken.None), EmPlanningStatus.SolverTimedOut, "timeout without fallback"); - - EmPlanningResult fallback = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.TimeoutWithFallback)).Plan( - request, CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, fallback.Status, "timeout uses only strict fallback"); - Verification.True(fallback.Trajectory != null && fallback.Trajectory.Points.Count > 0, - "timeout fallback publishes a complete trajectory"); - - using (var cancellation = new CancellationTokenSource()) - { - cancellation.Cancel(); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(request, - cancellation.Token), EmPlanningStatus.Cancelled, "cancellation"); - } - - using (var cancellation = new CancellationTokenSource()) - { - EmPlanningRequest publicationRequest = CreateRequest(TravelDirection.Forward, 0d, false, false); - publicationRequest.Configuration.Solver.NativeVerbose = true; - var service = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success), - new CancellingPublicationDebugSink(cancellation)); - - EmPlanningResult cancelledBeforePublication = service.Plan(publicationRequest, cancellation.Token); - - VerifyFailure(cancelledBeforePublication, EmPlanningStatus.Cancelled, - "cancellation immediately before service publication"); - } - } - - private static void VerifiesLsAndStShareOneSolveBudget() - { - EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false); - request.Configuration.Scheduling.SolverTimeoutSeconds = 1d; - request.Configuration.Solver.MaximumOuterIterations = 1; - var solver = new ScriptedPipelineSolver(PipelineSolverMode.Success, - lateralSolveDelay: TimeSpan.FromMilliseconds(200d)); - - EmPlanningResult result = new EmPlanningService(solver).Plan(request, CancellationToken.None); - - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - "shared LS/ST budget test publishes a validated trajectory"); - Verification.True(solver.LateralSolveBudgets.Count > 0 && solver.LongitudinalSolveBudgets.Count > 0, - "shared LS/ST budget test reached both optimizers"); - Verification.True(solver.LongitudinalSolveBudgets[0] < - solver.LateralSolveBudgets[0] - TimeSpan.FromMilliseconds(100d), - "ST receives only the LS/ST budget remaining after the delayed LS solve"); - } - - private static void VerifiesNoProgressPublishesNoTrajectory() - { - EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false, - CreateReferencePath(TravelDirection.Forward, false, 0.10d), null, - EmPlanningScope.FullDirectionSegment); - request.Configuration.Scheduling.MaximumOptimizationSpatialStepMeters = 1d; - request.Configuration.Scheduling.MaximumOptimizationTimeStepSeconds = 1d; - - EmPlanningResult result = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.NoProgress)).Plan( - request, CancellationToken.None); - - VerifyFailure(result, EmPlanningStatus.NoProgress, "full nonterminal no progress"); - Verification.True(result.FailureReason.IndexOf("NoProgress", StringComparison.Ordinal) >= 0, - "no-progress failure preserves its diagnostic"); - } - - private static void VerifiesPublicationFailureAndDebugIsolation() - { - EmPlanningRequest validationFailure = CreateRequest(TravelDirection.Forward, 0d, false, false); - VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.PublicationValidationFailure, - validationFailure.Map)).Plan(validationFailure, CancellationToken.None), EmPlanningStatus.ValidationFailed, - "publication validation failure"); - - EmPlanningRequest debugRequest = CreateRequest(TravelDirection.Forward, 0d, false, false); - debugRequest.Configuration.Solver.NativeVerbose = true; - EmPlanningResult debugIsolated = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success), - new ThrowingDebugSink()).Plan(debugRequest, CancellationToken.None); - VerifySuccess(debugIsolated, debugRequest, EmTerminalType.Goal, "debug-sink isolation"); - } - - private static void VerifiesPublicationGateStatusMappings() - { - Verification.Equal(EmPlanningStatus.TerminalPoseMismatch, - EmPlanningService.MapPublicationFailure(EmTrajectoryValidationFailure.TerminalPoseMismatch), - "terminal-pose validator failure has its precise service status"); - - EmPlanningRequest sampleLimited = CreateRequest(TravelDirection.Forward, 0d, false, false); - sampleLimited.Configuration.Scheduling.MaximumPublishedSampleCount = 2; - EmPlanningResult sampleLimitResult = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( - sampleLimited, CancellationToken.None); - VerifyFailure(sampleLimitResult, EmPlanningStatus.FullSegmentResourceLimitExceeded, - "sample-limit publication failure"); - } - - private static void VerifiesPreviousTrajectoryIsALongitudinalSoftReference() - { - EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false); - - var withoutPreviousSolver = new ScriptedPipelineSolver(PipelineSolverMode.Success); - EmPlanningResult withoutPrevious = new EmPlanningService(withoutPreviousSolver).Plan(request, CancellationToken.None); - VerifySuccess(withoutPrevious, request, EmTerminalType.Goal, "no previous longitudinal seed"); - - EmTrajectory validPrevious = CreateLongitudinalPreviousTrajectory(request.EffectiveAtUtc, TravelDirection.Forward, - request.SegmentIndex); - EmPlanningRequest withPreviousRequest = ReplacePreviousTrajectory(request, validPrevious); - var withPreviousSolver = new ScriptedPipelineSolver(PipelineSolverMode.Success); - EmPlanningResult withPrevious = new EmPlanningService(withPreviousSolver).Plan(withPreviousRequest, - CancellationToken.None); - VerifySuccess(withPrevious, withPreviousRequest, EmTerminalType.Goal, "valid previous longitudinal seed"); - - QuadraticProgram withoutPreviousProblem = withoutPreviousSolver.LastLongitudinalProblem - ?? throw new InvalidOperationException("The no-seed longitudinal QP was not captured."); - QuadraticProgram withPreviousProblem = withPreviousSolver.LastLongitudinalProblem - ?? throw new InvalidOperationException("The seeded longitudinal QP was not captured."); - var layout = new LongitudinalVariableLayout((withPreviousProblem.VariableCount + 1) / 4); - Verification.True(MatrixValue(withPreviousProblem.UpperTriangularP, layout.S(1), layout.S(1)) > - MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.S(1), layout.S(1)), - "valid previous seed adds a nonzero previous-S soft-reference Hessian term"); - Verification.True(MatrixValue(withPreviousProblem.UpperTriangularP, layout.U(1), layout.U(1)) > - MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.U(1), layout.U(1)), - "valid previous seed adds a nonzero previous-U soft-reference Hessian term"); - Verification.True(Math.Abs(withPreviousProblem.LinearCost[layout.S(1)] - - withoutPreviousProblem.LinearCost[layout.S(1)]) > 1e-12d, - "valid previous seed adds a nonzero previous-S soft-reference linear term"); - Verification.True(Math.Abs(withPreviousProblem.LinearCost[layout.U(1)] - - withoutPreviousProblem.LinearCost[layout.U(1)]) > 1e-12d, - "valid previous seed adds a nonzero previous-U soft-reference linear term"); - - EmPlanningRequest incompatibleRequest = ReplacePreviousTrajectory(request, - CreateLongitudinalPreviousTrajectory(request.EffectiveAtUtc, TravelDirection.Reverse, request.SegmentIndex)); - var incompatibleSolver = new ScriptedPipelineSolver(PipelineSolverMode.Success); - EmPlanningResult incompatible = new EmPlanningService(incompatibleSolver).Plan(incompatibleRequest, - CancellationToken.None); - VerifySuccess(incompatible, incompatibleRequest, EmTerminalType.Goal, "incompatible previous seed"); - QuadraticProgram incompatibleProblem = incompatibleSolver.LastLongitudinalProblem - ?? throw new InvalidOperationException("The incompatible-seed longitudinal QP was not captured."); - Verification.NearlyEqual(MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.S(1), layout.S(1)), - MatrixValue(incompatibleProblem.UpperTriangularP, layout.S(1), layout.S(1)), - "incompatible previous seed omits previous-S soft-reference term"); - Verification.NearlyEqual(MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.U(1), layout.U(1)), - MatrixValue(incompatibleProblem.UpperTriangularP, layout.U(1), layout.U(1)), - "incompatible previous seed omits previous-U soft-reference term"); - } - - private static void VerifySuccess(EmPlanningResult result, EmPlanningRequest request, EmTerminalType terminalType, - string name) - { - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - name + " successful status: " + result.FailureReason); - Verification.True(result.Trajectory != null && result.Trajectory.Metadata.TerminalType == terminalType, - name + " terminal type"); - string identifiers = "map=" + request.Map.SnapshotId + ";reference=" + request.ReferencePathId + ";state=" + - request.VehicleState.SequenceId + ";previous=" + request.PreviousTrajectoryId + ";segment=" + request.SegmentIndex; - Verification.True(result.FailureReason.IndexOf(identifiers, StringComparison.Ordinal) >= 0, - name + " preserves request identifiers in deterministic diagnostics"); - } - - private static void VerifyFailure(EmPlanningResult result, EmPlanningStatus expected, string name) - { - Verification.Equal(expected, result.Status, name + " status: " + result.FailureReason); - Verification.True(result.Trajectory == null, name + " publishes no partial trajectory"); - } - - private static void VerifySameTrajectory(EmPlanningResult left, EmPlanningResult right, string name) - { - Verification.Equal(left.Status, right.Status, name + " status"); - Verification.Equal(left.Trajectory.Points.Count, right.Trajectory.Points.Count, name + " point count"); - for (int index = 0; index < left.Trajectory.Points.Count; index++) - { - EmTrajectoryPoint first = left.Trajectory.Points[index]; - EmTrajectoryPoint second = right.Trajectory.Points[index]; - Verification.NearlyEqual(first.X, second.X, name + " X " + index); - Verification.NearlyEqual(first.Y, second.Y, name + " Y " + index); - Verification.NearlyEqual(first.TimeFromStart, second.TimeFromStart, name + " time " + index); - Verification.NearlyEqual(first.SignedLongitudinalVelocity, second.SignedLongitudinalVelocity, - name + " signed speed " + index); - } - } - - private static EmPlanningRequest CreateRequest(TravelDirection direction, double signedSpeed, bool endsAtGearSwitch, - bool rolling, PathSmoothingResult? referencePath = null, PlanningGridMap? map = null, - EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) - { - DateTimeOffset requestedAtUtc = DateTimeOffset.UnixEpoch.AddSeconds(10d); - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Solver.MaximumOuterIterations = 2; - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.20d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 0.20d; - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond = 0.20d; - configuration.Longitudinal.DesiredReverseSpeedMetersPerSecond = 0.20d; - if (rolling) - configuration.Scheduling.DistanceHorizonMeters = 0.30d; - return new EmPlanningRequest(referencePath ?? CreateReferencePath(direction, endsAtGearSwitch), map ?? CreateMap(false), - new VehicleParameters - { - LengthMeters = 0.10d, - WidthMeters = 0.10d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 1d, - }, - new VehicleMotionState(new Pose2D(0d, 0d, 0d), signedSpeed, 0d, requestedAtUtc, 7L), configuration, 0, - null, requestedAtUtc, requestedAtUtc, "output-trajectory", "reference-42", "prior-42", - EmMotionModel.NonholonomicForwardReverse, planningScope); - } - - private static EmPlanningRequest ReplaceState(EmPlanningRequest source, VehicleMotionState state) - { - return new EmPlanningRequest(source.ReferencePath, source.Map, source.Vehicle, state, source.Configuration, - source.SegmentIndex, source.PreviousTrajectory, source.RequestedAtUtc, source.EffectiveAtUtc, - source.OutputTrajectoryId, source.ReferencePathId, source.PreviousTrajectoryId, source.MotionModel, - source.PlanningScope); - } - - private static EmPlanningRequest ReplacePreviousTrajectory(EmPlanningRequest source, EmTrajectory previousTrajectory) - { - return new EmPlanningRequest(source.ReferencePath, source.Map, source.Vehicle, source.VehicleState, - source.Configuration, source.SegmentIndex, previousTrajectory, source.RequestedAtUtc, source.EffectiveAtUtc, - source.OutputTrajectoryId, source.ReferencePathId, source.PreviousTrajectoryId, source.MotionModel, - source.PlanningScope); - } - - private static EmTrajectory CreateLongitudinalPreviousTrajectory(DateTimeOffset effectiveAtUtc, - TravelDirection direction, int segmentIndex) - { - var metadata = new EmTrajectoryMetadata("previous-service", effectiveAtUtc, effectiveAtUtc, 1L, - "previous-reference", 1L, string.Empty, segmentIndex, direction, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, EmPlanningScope.RollingHorizon); - double sign = direction == TravelDirection.Forward ? 1d : -1d; - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(sign * 0.001d, 0d, 0d, sign * 0.01d, 0d, 0d, segmentIndex, 0.001d, 0.001d, - direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.001d, 0d, 0d, sign * 0.01d, 6d, 0d, segmentIndex, 0.001d, 0.001d, - direction, EmBoundaryType.None, 0d, 0d), - }); - } - - private static double MatrixValue(SparseCscMatrix matrix, int row, int column) - { - for (int index = matrix.ColumnPointers[column]; index < matrix.ColumnPointers[column + 1]; index++) - { - if (matrix.RowIndices[index] == row) - return matrix.Values[index]; - } - return 0d; - } - - private static PathSmoothingResult CreateReferencePath(TravelDirection direction, bool endsAtGearSwitch, - double lengthMeters = 2d) - { - double endX = direction == TravelDirection.Forward ? lengthMeters : -lengthMeters; - var points = new List - { - new SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, direction, 0d, 0d, 0d, 1d, false, - SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(endX, 0d, 0d, 0d, lengthMeters, direction, 0d, 0d, 0d, 1d, endsAtGearSwitch, - endsAtGearSwitch ? SmoothedPathPointSource.GearSwitch : SmoothedPathPointSource.Anchor), - }; - var segments = new List - { - new SmoothedPathSegment(0, direction, 0, 1, false, endsAtGearSwitch), - }; - var metrics = new PathQualityMetrics(true, lengthMeters, 0d, 0d, 0d, 0d, 1d, 0d, 0d, 0d, 0d, 0d); - return PathSmoothingResult.PublishLocalG2(PathSmoothingStatus.Complete, points, segments, - new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); - } - - private static PathSmoothingResult CreateAllForwardUReferencePath() - { - var points = new List - { - new SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, TravelDirection.Forward, - 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(10d, 0d, 0d, 0d, 10d, TravelDirection.Forward, - 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(10d, 0.4d, Math.PI, Math.PI, 10.4d, TravelDirection.Forward, - 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(0d, 0.4d, Math.PI, Math.PI, 20.4d, TravelDirection.Forward, - 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - var segments = new List - { - new SmoothedPathSegment(0, TravelDirection.Forward, 0, points.Count - 1, false, false), - }; - var metrics = new PathQualityMetrics(true, 20.4d, 0d, 0d, 0d, 0d, 1d, 0d, 0d, 0d, 0d, 0d); - return PathSmoothingResult.PublishLocalG2(PathSmoothingStatus.Complete, points, segments, - new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); - } - - private static PlanningGridMap CreateMap(bool blockStart, double halfExtentMeters = 3d) - { - IMapObstacleSource[] sources = blockStart - ? new IMapObstacleSource[] { new ManualObstacleSource("service-obstacle", 1L, true, - new IMapObstacle[] { new AxisAlignedRectangleObstacle(-20f, 20f, -20f, 20f) }) } - : Array.Empty(); - PlanningMapBuildResult result = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm((float)(-1000d * halfExtentMeters), (float)(1000d * halfExtentMeters), -1000f, 1000f), - ResolutionMm = 20f, - ObstacleSources = sources, - AllowExplicitEmptyMap = !blockStart, - }); - Verification.True(result.Succeeded && result.Map != null && result.Map.PlanningReady, - "service map builds: " + result.FailureReason); - return result.Map!; - } - - private enum PipelineSolverMode - { - Success, - LateralInfeasible, - LongitudinalInfeasible, - SolverUnavailable, - TimeoutWithoutFallback, - TimeoutWithFallback, - NoProgress, - PublicationValidationFailure, - } - - private sealed class ScriptedPipelineSolver : IQpSolver - { - private readonly PipelineSolverMode mode; - private readonly PlanningGridMap? mapToCorrupt; - private readonly IReadOnlyList? strictFullPrimal; - private readonly TimeSpan lateralSolveDelay; - private int longitudinalCallCount; - - public QuadraticProgram? LastLongitudinalProblem { get; private set; } - public List LateralSolveBudgets { get; } = new(); - public List LongitudinalSolveBudgets { get; } = new(); - - public ScriptedPipelineSolver(PipelineSolverMode mode, PlanningGridMap? mapToCorrupt = null, - IReadOnlyList? strictFullPrimal = null, TimeSpan? lateralSolveDelay = null) - { - this.mode = mode; - this.mapToCorrupt = mapToCorrupt; - this.strictFullPrimal = strictFullPrimal; - this.lateralSolveDelay = lateralSolveDelay.GetValueOrDefault(); - } - - public QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, IReadOnlyList warmStart, - CancellationToken cancellationToken) - { - bool longitudinal = IsLongitudinalProblem(problem); - if (mode == PipelineSolverMode.SolverUnavailable) - return Result(QpSolveStatus.SolverUnavailable, Array.Empty()); - if (!longitudinal) - { - LateralSolveBudgets.Add(settings.TimeLimit); - if (lateralSolveDelay > TimeSpan.Zero) - Thread.Sleep(lateralSolveDelay); - if (mode == PipelineSolverMode.LateralInfeasible) - return Result(QpSolveStatus.PrimalInfeasible, Array.Empty()); - if (mode == PipelineSolverMode.TimeoutWithoutFallback) - return Result(QpSolveStatus.TimeLimit, Array.Empty()); - return Result(QpSolveStatus.Solved, new double[problem.VariableCount]); - } - LongitudinalSolveBudgets.Add(settings.TimeLimit); - LastLongitudinalProblem = problem; - if (mode == PipelineSolverMode.LongitudinalInfeasible) - return Result(QpSolveStatus.PrimalInfeasible, Array.Empty()); - if (mode == PipelineSolverMode.NoProgress) - return Result(QpSolveStatus.Solved, new double[problem.VariableCount]); - if (strictFullPrimal != null && strictFullPrimal.Count == problem.VariableCount) - { - longitudinalCallCount++; - return Result(QpSolveStatus.Solved, strictFullPrimal); - } - if (mode == PipelineSolverMode.PublicationValidationFailure && longitudinalCallCount == 0) - CorruptMapAtOrigin(mapToCorrupt); - if (mode == PipelineSolverMode.TimeoutWithFallback && ++longitudinalCallCount > 1) - return Result(QpSolveStatus.TimeLimit, Array.Empty()); - longitudinalCallCount++; - return Result(QpSolveStatus.Solved, - TryCreateStrictExactStopPrimal(problem, out double[] strictPrimal) ? strictPrimal : warmStart); - } - - private static void CorruptMapAtOrigin(PlanningGridMap? map) - { - if (map == null || !map.TryWorldToGrid(0d, 0d, out int row, out int column)) - throw new InvalidOperationException("Unable to corrupt the publication test map."); - FieldInfo occupiedField = typeof(PlanningGridMap).GetField("_occupied", BindingFlags.Instance | BindingFlags.NonPublic)! - ?? throw new InvalidOperationException("Planning map occupancy storage was unavailable."); - FieldInfo distanceField = typeof(PlanningGridMap).GetField("_conservativeDistances", - BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("Planning map distance storage was unavailable."); - byte[] occupied = (byte[])occupiedField.GetValue(map)!; - double[] distances = (double[])distanceField.GetValue(map)!; - int index = row * map.Cols + column; - occupied[index] = 1; - distances[index] = 0d; - } - - private static bool TryCreateStrictExactStopPrimal(QuadraticProgram problem, out double[] primal) - { - primal = Array.Empty(); - int variableCount = problem.VariableCount; - int knotCount = (variableCount + 1) / 4; - var layout = new LongitudinalVariableLayout(knotCount); - int stabilizationStart = FindExactStopTailStart(problem, layout); - if (stabilizationStart < 3) - return false; - var times = new double[knotCount]; - for (int index = 0; index < knotCount - 1; index++) - { - if (!TryReadDynamicsDuration(problem, layout, index, out double duration)) - return false; - times[index + 1] = times[index] + duration; - } - - var motionTimes = new double[stabilizationStart + 1]; - Array.Copy(times, motionTimes, motionTimes.Length); - double initialPathS = ReadFixedVariable(problem, layout.S(0)); - double initialSpeed = ReadFixedVariable(problem, layout.U(0)); - double initialAcceleration = ReadFixedVariable(problem, layout.A(0)); - double terminalPathS = ReadFixedVariable(problem, layout.S(stabilizationStart)); - var preferredJerk = new double[stabilizationStart]; - LongitudinalCandidate baseline = LongitudinalCandidate.Integrate(motionTimes, initialPathS, initialSpeed, - initialAcceleration, preferredJerk); - var influence = new double[3, stabilizationStart]; - for (int interval = 0; interval < stabilizationStart; interval++) - { - var basis = new double[stabilizationStart]; - basis[interval] = 1d; - LongitudinalCandidate response = LongitudinalCandidate.Integrate(motionTimes, 0d, 0d, 0d, basis); - int terminalIndex = response.S.Count - 1; - influence[0, interval] = response.A[terminalIndex]; - influence[1, interval] = response.U[terminalIndex]; - influence[2, interval] = response.S[terminalIndex]; - } - double[] target = - { - -baseline.A[baseline.A.Count - 1], - -baseline.U[baseline.U.Count - 1], - terminalPathS - baseline.S[baseline.S.Count - 1], - }; - var jerk = new double[knotCount - 1]; - if (stabilizationStart >= 4) - { - int terminalFirstInterval = stabilizationStart - 3; - var terminalInfluence = new double[3, 3]; - for (int row = 0; row < 3; row++) - { - for (int column = 0; column < 3; column++) - terminalInfluence[row, column] = influence[row, terminalFirstInterval + column]; - } - if (!TrySolveThreeByThree(terminalInfluence, target, out double[] terminalJerk)) - return false; - for (int interval = 0; interval < 3; interval++) - jerk[terminalFirstInterval + interval] = terminalJerk[interval]; - } - else - { - var gram = new double[3, 3]; - for (int row = 0; row < 3; row++) - { - for (int column = 0; column < 3; column++) - { - for (int interval = 0; interval < stabilizationStart; interval++) - gram[row, column] += influence[row, interval] * influence[column, interval]; - } - } - if (!TrySolveThreeByThree(gram, target, out double[] multipliers)) - return false; - for (int interval = 0; interval < stabilizationStart; interval++) - { - jerk[interval] = preferredJerk[interval]; - for (int row = 0; row < 3; row++) - jerk[interval] += influence[row, interval] * multipliers[row]; - } - } - var motionJerk = new double[stabilizationStart]; - Array.Copy(jerk, motionJerk, motionJerk.Length); - LongitudinalCandidate candidate = LongitudinalCandidate.Integrate(motionTimes, initialPathS, initialSpeed, - initialAcceleration, motionJerk); - primal = CreateExactStopPrimal(layout, knotCount, stabilizationStart, terminalPathS, candidate); - return true; - } - - private static double[] CreateExactStopPrimal(LongitudinalVariableLayout layout, int knotCount, - int stabilizationStart, double terminalPathS, LongitudinalCandidate candidate) - { - var primal = new double[layout.VariableCount]; - for (int index = 0; index < knotCount; index++) - { - bool isTerminalTail = index >= stabilizationStart; - primal[layout.S(index)] = isTerminalTail ? terminalPathS : candidate.S[index]; - primal[layout.U(index)] = isTerminalTail ? 0d : candidate.U[index]; - primal[layout.A(index)] = isTerminalTail ? 0d : candidate.A[index]; - } - for (int index = 0; index < candidate.J.Count; index++) - primal[layout.J(index)] = candidate.J[index]; - return primal; - } - - private static int FindExactStopTailStart(QuadraticProgram problem, LongitudinalVariableLayout layout) - { - for (int index = 1; index < layout.KnotCount; index++) - { - if (TryReadFixedVariable(problem, layout.S(index), out _) && - TryReadFixedVariable(problem, layout.U(index), out _) && - TryReadFixedVariable(problem, layout.A(index), out _)) - { - return index; - } - } - return -1; - } - - private static bool TryReadDynamicsDuration(QuadraticProgram problem, LongitudinalVariableLayout layout, - int interval, out double duration) - { - duration = 0d; - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (Math.Abs(problem.LowerBounds[row]) > 1e-12d || Math.Abs(problem.UpperBounds[row]) > 1e-12d || - CountRowEntries(problem, row) != 3 || - Math.Abs(ReadCoefficient(problem, row, layout.A(interval + 1)) - 1d) > 1e-12d || - Math.Abs(ReadCoefficient(problem, row, layout.A(interval)) + 1d) > 1e-12d) - { - continue; - } - double jerkCoefficient = ReadCoefficient(problem, row, layout.J(interval)); - if (jerkCoefficient >= -1e-12d) - continue; - duration = -jerkCoefficient; - return true; - } - return false; - } - - private static int CountRowEntries(QuadraticProgram problem, int row) - { - int count = 0; - for (int column = 0; column < problem.ConstraintMatrix.ColumnCount; column++) - { - for (int index = problem.ConstraintMatrix.ColumnPointers[column]; - index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) - { - if (problem.ConstraintMatrix.RowIndices[index] == row) - count++; - } - } - return count; - } - - private static double ReadCoefficient(QuadraticProgram problem, int row, int column) - { - for (int index = problem.ConstraintMatrix.ColumnPointers[column]; - index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) - { - if (problem.ConstraintMatrix.RowIndices[index] == row) - return problem.ConstraintMatrix.Values[index]; - } - return 0d; - } - - private static bool TrySolveThreeByThree(double[,] matrix, IReadOnlyList rightHandSide, - out double[] solution) - { - var augmented = new double[3, 4]; - for (int row = 0; row < 3; row++) - { - for (int column = 0; column < 3; column++) - augmented[row, column] = matrix[row, column]; - augmented[row, 3] = rightHandSide[row]; - } - for (int column = 0; column < 3; column++) - { - int pivot = column; - for (int row = column + 1; row < 3; row++) - { - if (Math.Abs(augmented[row, column]) > Math.Abs(augmented[pivot, column])) - pivot = row; - } - if (Math.Abs(augmented[pivot, column]) < 1e-12d) - { - solution = Array.Empty(); - return false; - } - if (pivot != column) - { - for (int index = column; index < 4; index++) - { - double temporary = augmented[column, index]; - augmented[column, index] = augmented[pivot, index]; - augmented[pivot, index] = temporary; - } - } - double divisor = augmented[column, column]; - for (int index = column; index < 4; index++) - augmented[column, index] /= divisor; - for (int row = 0; row < 3; row++) - { - if (row == column) - continue; - double factor = augmented[row, column]; - for (int index = column; index < 4; index++) - augmented[row, index] -= factor * augmented[column, index]; - } - } - solution = new[] { augmented[0, 3], augmented[1, 3], augmented[2, 3] }; - return true; - } - - private static bool IsLongitudinalProblem(QuadraticProgram problem) - { - if (problem.VariableCount < 7 || (problem.VariableCount + 1) % 4 != 0) - return false; - int knotCount = (problem.VariableCount + 1) / 4; - var layout = new LongitudinalVariableLayout(knotCount); - return TryReadFixedVariable(problem, layout.S(0), out _) && - TryReadFixedVariable(problem, layout.U(0), out _) && - TryReadFixedVariable(problem, layout.A(0), out _); - } - - private static double ReadFixedVariable(QuadraticProgram problem, int variable) - { - if (TryReadFixedVariable(problem, variable, out double value)) - return value; - throw new InvalidOperationException("Expected a fixed ST variable constraint."); - } - - private static bool TryReadFixedVariable(QuadraticProgram problem, int variable, out double value) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - int entryCount = 0; - double coefficient = 0d; - for (int column = 0; column < problem.ConstraintMatrix.ColumnCount; column++) - { - for (int index = problem.ConstraintMatrix.ColumnPointers[column]; - index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) - { - if (problem.ConstraintMatrix.RowIndices[index] != row) - continue; - entryCount++; - if (column == variable) - coefficient = problem.ConstraintMatrix.Values[index]; - } - } - if (entryCount == 1 && Math.Abs(coefficient) > 1e-12d && - Math.Abs(problem.LowerBounds[row] - problem.UpperBounds[row]) <= 1e-12d) - { - value = problem.LowerBounds[row] / coefficient; - return true; - } - } - value = 0d; - return false; - } - - private static QpSolveResult Result(QpSolveStatus status, IReadOnlyList primal) - { - return new QpSolveResult(status, primal, 0d, 0d, 0d, 1, TimeSpan.Zero, status.ToString(), string.Empty); - } - } - - private sealed class ThrowingDebugSink : IEmPlannerDebugSink - { - public void Write(string message) - { - throw new InvalidOperationException("debug sink failure"); - } - } - - private sealed class CancellingPublicationDebugSink : IEmPlannerDebugSink - { - private readonly CancellationTokenSource cancellation; - - public CancellingPublicationDebugSink(CancellationTokenSource cancellation) - { - this.cancellation = cancellation ?? throw new ArgumentNullException(nameof(cancellation)); - } - - public void Write(string message) - { - if (string.Equals(message, "world-space publication validation succeeded", StringComparison.Ordinal)) - cancellation.Cancel(); - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs deleted file mode 100644 index 880bf0f..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.EMPlanner; - -namespace EMPlannerVerificationHost; - -internal static class ExecutorChecks -{ - public static void Run() - { - VerifiesGearSwitchSequence(TravelDirection.Forward, TravelDirection.Reverse, "forward-to-reverse"); - VerifiesGearSwitchSequence(TravelDirection.Reverse, TravelDirection.Forward, "reverse-to-forward"); - VerifiesExecutorSamplesBoundariesAndCompletesAtSafeTerminals(); - VerifiesGenericControlCommandMapping(); - VerifiesVehicleStateProviderRemainsSnapshotOnly(); - } - - public static void RunRollingEndToEnd() - { - VerifiesGearSwitchSequence(TravelDirection.Forward, TravelDirection.Reverse, - "end-to-end forward-to-reverse"); - VerifiesGearSwitchSequence(TravelDirection.Reverse, TravelDirection.Forward, - "end-to-end reverse-to-forward"); - VerifiesGoalCommandStopsAtAndAfterTerminal(); - } - - private static void VerifiesGearSwitchSequence(TravelDirection currentDirection, TravelDirection desiredDirection, - string name) - { - var machine = new GearSwitchStateMachine(0.01d, 0.20d); - DateTimeOffset start = DateTimeOffset.UnixEpoch.AddSeconds(500d); - - GearSwitchStateUpdate approaching = machine.Update(start, 0.10d, desiredDirection, currentDirection, false, false, - false); - Verification.Equal(GearSwitchState.ApproachingGearSwitch, approaching.State, name + " approaches switch"); - Verification.True(approaching.AllowsTrajectoryMotion, name + " approach permits trajectory motion"); - - GearSwitchStateUpdate holding = machine.Update(start.AddMilliseconds(100), 0.005d, desiredDirection, - currentDirection, false, true, false); - AssertHeld(holding, GearSwitchState.HoldingZero, name + " holds at boundary"); - - GearSwitchStateUpdate movingAgain = machine.Update(start.AddMilliseconds(110), 0.01d, desiredDirection, - currentDirection, false, true, false); - AssertHeld(movingAgain, GearSwitchState.HoldingZero, name + " holds while measured speed is at tolerance"); - - GearSwitchStateUpdate dwellRestart = machine.Update(start.AddMilliseconds(120), 0d, desiredDirection, - currentDirection, false, true, false); - AssertHeld(dwellRestart, GearSwitchState.HoldingZero, name + " starts zero-speed dwell"); - GearSwitchStateUpdate dwellShort = machine.Update(start.AddMilliseconds(319), 0d, desiredDirection, - currentDirection, false, true, false); - AssertHeld(dwellShort, GearSwitchState.HoldingZero, name + " does not request before full dwell"); - - GearSwitchStateUpdate requesting = machine.Update(start.AddMilliseconds(320), 0d, desiredDirection, - currentDirection, false, true, false); - AssertHeld(requesting, GearSwitchState.RequestingDirectionChange, name + " requests after continuous dwell"); - Verification.True(requesting.RequestDirectionChange, name + " emits exactly one direction request"); - - GearSwitchStateUpdate awaiting = machine.Update(start.AddMilliseconds(321), 0d, desiredDirection, - currentDirection, false, true, false); - AssertHeld(awaiting, GearSwitchState.AwaitingDirectionConfirmation, name + " awaits confirmation"); - Verification.True(!awaiting.RequestDirectionChange, name + " does not repeat direction request"); - - GearSwitchStateUpdate following = machine.Update(start.AddMilliseconds(322), 0d, desiredDirection, - desiredDirection, true, false, false); - Verification.Equal(GearSwitchState.Following, following.State, name + " follows confirmed next segment"); - Verification.True(!following.HoldZero && following.AllowsTrajectoryMotion, - name + " resumes only after confirmation"); - - GearSwitchStateUpdate completed = machine.Update(start.AddMilliseconds(323), 0d, desiredDirection, - desiredDirection, false, false, true); - AssertHeld(completed, GearSwitchState.Completed, name + " completes at terminal while holding zero"); - Verification.True(completed.IsTrajectoryComplete, name + " marks terminal completion"); - } - - private static void VerifiesExecutorSamplesBoundariesAndCompletesAtSafeTerminals() - { - DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(600d); - var executor = new TrajectoryExecutor(); - EmTrajectory gearTrajectory = CreateTrajectory(effectiveAt, TravelDirection.Forward, - EmBoundaryType.GearSwitchApproach, EmTerminalType.GearSwitch); - - TrajectoryExecutionState approaching = executor.Update(effectiveAt.AddSeconds(0.10d), - CreateMeasuredState(0.10d, effectiveAt.AddSeconds(0.10d), 60L), gearTrajectory, TravelDirection.Reverse, - TravelDirection.Forward, false); - Verification.Equal(GearSwitchState.ApproachingGearSwitch, approaching.GearSwitchState, - "executor enters approach before exact gear boundary"); - Verification.NearlyEqual(0.10d, approaching.SelectedPoint.SignedLongitudinalVelocity, - "executor samples moving approach point"); - - TrajectoryExecutionState holding = executor.Update(effectiveAt.AddSeconds(0.30d), - CreateMeasuredState(0.005d, effectiveAt.AddSeconds(0.30d), 61L), gearTrajectory, TravelDirection.Reverse, - TravelDirection.Forward, false); - AssertHeld(holding, GearSwitchState.HoldingZero, "executor holds at exact gear boundary"); - Verification.Equal(EmBoundaryType.GearSwitchApproach, holding.SelectedPoint.BoundaryType, - "executor preserves exact gear boundary point"); - Verification.NearlyEqual(0d, holding.SelectedPoint.SignedLongitudinalVelocity, - "executor does not release nonzero speed while holding"); - - foreach (EmBoundaryType terminalBoundary in new[] { EmBoundaryType.Goal, EmBoundaryType.RollingSafetyStop }) - { - var terminalExecutor = new TrajectoryExecutor(); - EmTrajectory terminal = CreateTrajectory(effectiveAt, TravelDirection.Forward, terminalBoundary, - terminalBoundary == EmBoundaryType.Goal ? EmTerminalType.Goal : EmTerminalType.RollingSafetyStop); - TrajectoryExecutionState completed = terminalExecutor.Update(effectiveAt.AddSeconds(0.30d), - CreateMeasuredState(0d, effectiveAt.AddSeconds(0.30d), 62L), terminal, TravelDirection.Forward, - TravelDirection.Forward, false); - AssertHeld(completed, GearSwitchState.Completed, "executor completes " + terminalBoundary); - Verification.True(completed.IsTrajectoryComplete, "executor marks " + terminalBoundary + " completion"); - } - } - - private static void VerifiesGenericControlCommandMapping() - { - DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(700d); - var adapter = new TrajectoryControlAdapter(); - - VerifiesFollowingCommand(adapter, effectiveAt, TravelDirection.Forward, 0.12d, 0.35d, 0.042d, - "forward command"); - VerifiesFollowingCommand(adapter, effectiveAt, TravelDirection.Reverse, -0.12d, 0.35d, -0.042d, - "reverse command"); - - var executor = new TrajectoryExecutor(); - EmTrajectory gearTrajectory = CreateTrajectory(effectiveAt, TravelDirection.Forward, - EmBoundaryType.GearSwitchApproach, EmTerminalType.GearSwitch); - executor.Update(effectiveAt, CreateMeasuredState(0.10d, effectiveAt, 80L), gearTrajectory, - TravelDirection.Reverse, TravelDirection.Forward, false); - TrajectoryExecutionState holding = executor.Update(effectiveAt.AddSeconds(0.30d), - CreateMeasuredState(0d, effectiveAt.AddSeconds(0.30d), 81L), gearTrajectory, - TravelDirection.Reverse, TravelDirection.Forward, false); - TrajectoryControlCommand holdCommand = adapter.CreateCommand(holding.SelectedPoint, holding); - Verification.NearlyEqual(0d, holdCommand.SignedLongitudinalVelocity, - "holding command overrides signed longitudinal velocity"); - Verification.NearlyEqual(0d, holdCommand.YawRate, "holding command overrides yaw rate"); - Verification.True(holdCommand.HoldBrake && !holdCommand.IsTrajectoryComplete, - "gear holding command requests brake without completing trajectory"); - - TrajectoryExecutionState requesting = executor.Update(effectiveAt.AddSeconds(0.50d), - CreateMeasuredState(0d, effectiveAt.AddSeconds(0.50d), 82L), gearTrajectory, - TravelDirection.Reverse, TravelDirection.Forward, false); - TrajectoryControlCommand requestCommand = adapter.CreateCommand(requesting.SelectedPoint, requesting); - Verification.True(requestCommand.RequestDirectionChange && requestCommand.HoldBrake, - "direction request remains a generic zero-speed brake command"); - - EmTrajectory terminalTrajectory = CreateTrajectory(effectiveAt, TravelDirection.Forward, EmBoundaryType.Goal, - EmTerminalType.Goal); - var terminalExecutor = new TrajectoryExecutor(); - TrajectoryExecutionState completed = terminalExecutor.Update(effectiveAt.AddSeconds(0.30d), - CreateMeasuredState(0d, effectiveAt.AddSeconds(0.30d), 82L), terminalTrajectory, - TravelDirection.Forward, TravelDirection.Forward, false); - TrajectoryControlCommand completeCommand = adapter.CreateCommand(completed.SelectedPoint, completed); - Verification.NearlyEqual(0d, completeCommand.SignedLongitudinalVelocity, - "completed command keeps signed longitudinal velocity at zero"); - Verification.NearlyEqual(0d, completeCommand.YawRate, "completed command keeps yaw rate at zero"); - Verification.True(completeCommand.HoldBrake && completeCommand.IsTrajectoryComplete, - "completed command holds brake and reports completion"); - - Verification.True(typeof(TrajectoryControlCommand).GetProperty("BodyLateralVelocity") == null, - "generic command exposes no body lateral velocity"); - } - - private static void VerifiesFollowingCommand(TrajectoryControlAdapter adapter, DateTimeOffset effectiveAt, - TravelDirection direction, double signedVelocity, double curvature, double expectedYawRate, string name) - { - EmTrajectory trajectory = CreateMotionTrajectory(effectiveAt, direction, signedVelocity, curvature); - var executor = new TrajectoryExecutor(); - VehicleMotionState measured = CreateMeasuredState(signedVelocity, effectiveAt, 83L); - TrajectoryExecutionState following = executor.Update(effectiveAt, measured, trajectory, direction, direction, false); - TrajectoryControlCommand command = adapter.CreateCommand(following.SelectedPoint, following); - TrajectoryControlCommand executorCommand = executor.UpdateCommand(effectiveAt, measured, trajectory, direction, - direction, false); - - Verification.NearlyEqual(following.SelectedPoint.SignedLongitudinalVelocity, command.SignedLongitudinalVelocity, - name + " copies signed longitudinal velocity exactly"); - Verification.NearlyEqual(following.SelectedPoint.YawRate, command.YawRate, - name + " copies yaw rate exactly"); - Verification.NearlyEqual(expectedYawRate, command.YawRate, name + " preserves signed yaw rate"); - Verification.Equal(direction, command.Direction, name + " preserves direction"); - Verification.True(!command.HoldBrake && !command.RequestDirectionChange && !command.IsTrajectoryComplete, - name + " remains a normal following command"); - Verification.NearlyEqual(command.SignedLongitudinalVelocity, executorCommand.SignedLongitudinalVelocity, - name + " executor command preserves signed longitudinal velocity"); - Verification.NearlyEqual(command.YawRate, executorCommand.YawRate, - name + " executor command preserves yaw rate"); - Verification.True(command.SignedLongitudinalVelocity != 0d || command.YawRate == 0d, - name + " never creates in-place rotation"); - Verification.NearlyEqual(Math.Abs(signedVelocity), following.SelectedPoint.Speed, - name + " leaves speed available in execution telemetry"); - Verification.NearlyEqual(signedVelocity * Math.Cos(following.SelectedPoint.Yaw), following.SelectedPoint.VelocityX, - name + " leaves world velocity X in execution telemetry"); - Verification.NearlyEqual(signedVelocity * Math.Sin(following.SelectedPoint.Yaw), following.SelectedPoint.VelocityY, - name + " leaves world velocity Y in execution telemetry"); - Verification.NearlyEqual(curvature, following.SelectedPoint.VehicleCurvature, - name + " leaves curvature available in execution telemetry"); - } - - private static void VerifiesVehicleStateProviderRemainsSnapshotOnly() - { - DateTimeOffset capturedAt = DateTimeOffset.UnixEpoch.AddSeconds(710d); - IVehicleStateProvider provider = new FixedVehicleStateProvider(CreateMeasuredState(0.02d, capturedAt, 84L)); - Verification.Equal(84L, provider.Capture().SequenceId, "vehicle state provider returns caller-owned snapshot"); - } - - private static void VerifiesGoalCommandStopsAtAndAfterTerminal() - { - DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(950d); - EmTrajectory goalTrajectory = CreateMotionTrajectory(effectiveAt, TravelDirection.Forward, 0.10d, 0.20d); - var executor = new TrajectoryExecutor(); - VehicleMotionState moving = CreateMeasuredState(0.10d, effectiveAt, 95L); - TrajectoryControlCommand following = executor.UpdateCommand(effectiveAt, moving, goalTrajectory, - TravelDirection.Forward, TravelDirection.Forward, false); - Verification.NearlyEqual(goalTrajectory.Points[0].SignedLongitudinalVelocity, following.SignedLongitudinalVelocity, - "goal approach command originates from trajectory point"); - Verification.NearlyEqual(goalTrajectory.Points[0].YawRate, following.YawRate, - "goal approach command preserves trajectory yaw rate"); - - TrajectoryControlCommand atGoal = executor.UpdateCommand(effectiveAt.AddSeconds(0.30d), - CreateMeasuredState(0d, effectiveAt.AddSeconds(0.30d), 96L), goalTrajectory, TravelDirection.Forward, - TravelDirection.Forward, false); - TrajectoryControlCommand afterGoal = executor.UpdateCommand(effectiveAt.AddSeconds(5d), - CreateMeasuredState(0d, effectiveAt.AddSeconds(5d), 97L), goalTrajectory, TravelDirection.Forward, - TravelDirection.Forward, false); - AssertCompletedCommand(atGoal, "exact goal terminal command"); - AssertCompletedCommand(afterGoal, "after goal terminal command"); - } - - private static void AssertCompletedCommand(TrajectoryControlCommand command, string name) - { - Verification.NearlyEqual(0d, command.SignedLongitudinalVelocity, name + " signed velocity"); - Verification.NearlyEqual(0d, command.YawRate, name + " yaw rate"); - Verification.True(command.HoldBrake && command.IsTrajectoryComplete, name + " holds completed trajectory"); - } - - private static void AssertHeld(GearSwitchStateUpdate update, GearSwitchState expectedState, string name) - { - Verification.Equal(expectedState, update.State, name + " state"); - Verification.True(update.HoldZero && !update.AllowsTrajectoryMotion, name + " forbids nonzero motion output"); - } - - private static void AssertHeld(TrajectoryExecutionState state, GearSwitchState expectedState, string name) - { - Verification.Equal(expectedState, state.GearSwitchState, name + " state"); - Verification.True(state.HoldZero && !state.AllowsTrajectoryMotion, name + " forbids nonzero motion output"); - } - - private static VehicleMotionState CreateMeasuredState(double speed, DateTimeOffset capturedAt, long sequenceId) - { - return new VehicleMotionState(new Pose2D(0d, 0d, 0d), speed, 0d, capturedAt, sequenceId); - } - - private static EmTrajectory CreateTrajectory(DateTimeOffset effectiveAt, TravelDirection direction, - EmBoundaryType terminalBoundary, EmTerminalType terminalType) - { - double signedSpeed = direction == TravelDirection.Forward ? 0.10d : -0.10d; - var metadata = new EmTrajectoryMetadata("executor-" + terminalBoundary, effectiveAt, effectiveAt, 70L, - "executor-reference", 60L, string.Empty, 4, direction, terminalType, - terminalType == EmTerminalType.RollingSafetyStop - ? EmLongitudinalMode.RollingContinuation - : EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(0d, 0d, 0d, signedSpeed, 0d, 0d, 4, 0d, 0d, direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(signedSpeed * 3d, 0d, 0d, 0d, 0.30d, 0d, 4, 0.03d, 0.03d, direction, - terminalBoundary, 0d, 0d), - }); - } - - private static EmTrajectory CreateMotionTrajectory(DateTimeOffset effectiveAt, TravelDirection direction, - double signedVelocity, double curvature) - { - var metadata = new EmTrajectoryMetadata("control-" + direction, effectiveAt, effectiveAt, 85L, - "control-reference", 84L, string.Empty, 5, direction, EmTerminalType.Goal, - EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(1d, 2d, Math.PI / 3d, signedVelocity, 0d, curvature, 5, 0d, 0d, direction, - EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(1d, 2d, Math.PI / 3d, 0d, 0.30d, curvature, 5, 0.04d, 0.04d, direction, - EmBoundaryType.Goal, 0d, 0d), - }); - } - - private sealed class FixedVehicleStateProvider : IVehicleStateProvider - { - private readonly VehicleMotionState state; - - public FixedVehicleStateProvider(VehicleMotionState state) - { - this.state = state; - } - - public VehicleMotionState Capture() - { - return state; - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs deleted file mode 100644 index 18f4f87..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.EMPlanner; - -namespace EMPlannerVerificationHost; - -internal sealed class FakeQpSolver : IQpSolver -{ - private readonly Queue _results; - private readonly List _problems = new List(); - private readonly List> _warmStarts = new List>(); - - public FakeQpSolver(QpSolveResult result) - : this(new[] { result }) - { - } - - public FakeQpSolver(IEnumerable results) - { - if (results == null) - throw new ArgumentNullException(nameof(results)); - _results = new Queue(); - foreach (QpSolveResult result in results) - _results.Enqueue(result ?? throw new ArgumentException("Fake solver results cannot contain null values.", nameof(results))); - if (_results.Count == 0) - throw new ArgumentException("At least one fake solver result is required.", nameof(results)); - LastWarmStart = Array.Empty(); - } - - public QuadraticProgram? LastProblem { get; private set; } - - public QpSolverSettings? LastSettings { get; private set; } - - public IReadOnlyList LastWarmStart { get; private set; } - - public IReadOnlyList Problems => new ReadOnlyCollection(_problems); - - public IReadOnlyList> WarmStarts => new ReadOnlyCollection>(_warmStarts); - - public int SolveCallCount => _problems.Count; - - public QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, IReadOnlyList warmStart, - CancellationToken cancellationToken) - { - LastProblem = problem ?? throw new ArgumentNullException(nameof(problem)); - LastSettings = settings ?? throw new ArgumentNullException(nameof(settings)); - var copy = new List(warmStart == null ? 0 : warmStart.Count); - if (warmStart != null) - { - for (int index = 0; index < warmStart.Count; index++) - copy.Add(warmStart[index]); - } - LastWarmStart = new ReadOnlyCollection(copy); - _problems.Add(LastProblem); - _warmStarts.Add(LastWarmStart); - if (_results.Count == 0) - throw new InvalidOperationException("Fake solver was called more often than its scripted result sequence."); - return _results.Dequeue(); - } -} - -internal sealed class CancellingQpSolver : IQpSolver -{ - private readonly IQpSolver inner; - private readonly CancellationTokenSource cancellation; - private readonly int cancelAfterSolveCount; - private int solveCount; - - public CancellingQpSolver(IQpSolver inner, CancellationTokenSource cancellation, int cancelAfterSolveCount = 1) - { - this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); - this.cancellation = cancellation ?? throw new ArgumentNullException(nameof(cancellation)); - if (cancelAfterSolveCount <= 0) throw new ArgumentOutOfRangeException(nameof(cancelAfterSolveCount)); - this.cancelAfterSolveCount = cancelAfterSolveCount; - } - - public QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, IReadOnlyList warmStart, - CancellationToken cancellationToken) - { - QpSolveResult result = inner.Solve(problem, settings, warmStart, cancellationToken); - solveCount++; - if (solveCount == cancelAfterSolveCount) - cancellation.Cancel(); - return result; - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs deleted file mode 100644 index ccc5b08..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using System.Collections.Generic; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class FoundationChecks -{ - public static void Run() - { - VerifyContracts(); - VerifyConfigurationAndRequestValidation(); - VerifyFullDirectionScopeContractsConfigurationAndValidation(); - } - - private static void VerifyContracts() - { - var reverseState = new VehicleMotionState( - new Pose2D(1.5d, -2d, 0.25d), - -0.15d, - -0.03d, - new DateTimeOffset(2026, 8, 3, 0, 0, 0, TimeSpan.Zero), - 17L); - - EMPlannerVerificationHost.Verification.NearlyEqual(-0.15d, reverseState.SignedLongitudinalSpeedMetersPerSecond, - "reverse signed speed"); - - var point = new EmTrajectoryPoint( - 1.25d, - -0.75d, - 0.5d, - -0.12d, - 0.4d, - -0.2d, - 3, - 0.6d, - 0.8d, - TravelDirection.Reverse, - EmBoundaryType.GearSwitchApproach, - -0.04d, - 0.03d); - - EMPlannerVerificationHost.Verification.NearlyEqual(1.25d, point.X, "point x"); - EMPlannerVerificationHost.Verification.NearlyEqual(-0.75d, point.Y, "point y"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.5d, point.Yaw, "point yaw"); - EMPlannerVerificationHost.Verification.NearlyEqual(-0.12d, point.SignedLongitudinalVelocity, "point signed speed"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.4d, point.TimeFromStart, "point time"); - EMPlannerVerificationHost.Verification.NearlyEqual(-0.2d, point.VehicleCurvature, "point curvature"); - EMPlannerVerificationHost.Verification.Equal(3, point.SegmentIndex, "point segment index"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.6d, point.SegmentLocalS, "point segment local s"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.8d, point.PathS, "point path s"); - EMPlannerVerificationHost.Verification.Equal(TravelDirection.Reverse, point.Direction, "point direction"); - EMPlannerVerificationHost.Verification.Equal(EmBoundaryType.GearSwitchApproach, point.BoundaryType, "point boundary type"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.12d, point.Speed, "point derived speed"); - EMPlannerVerificationHost.Verification.NearlyEqual(-0.12d * Math.Cos(0.5d), point.VelocityX, "point derived velocity x"); - EMPlannerVerificationHost.Verification.NearlyEqual(-0.12d * Math.Sin(0.5d), point.VelocityY, "point derived velocity y"); - EMPlannerVerificationHost.Verification.NearlyEqual((-0.12d) * (-0.2d), point.YawRate, "point derived yaw rate"); - } - - private static void VerifyConfigurationAndRequestValidation() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, configuration.Scheduling.ReplanPeriodSeconds, "replan period"); - EMPlannerVerificationHost.Verification.NearlyEqual(6d, configuration.Scheduling.TimeHorizonSeconds, "time horizon"); - EMPlannerVerificationHost.Verification.True(configuration.Scheduling.DistanceHorizonMeters > 0d, - "distance horizon remains positive"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.05d, configuration.Scheduling.OutputTimeStepSeconds, "output time step"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.10d, configuration.Scheduling.SolverTimeoutSeconds, "solver timeout"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.30d, configuration.Scheduling.HandoffLookaheadSeconds, "handoff lookahead"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, configuration.Scheduling.MaximumVehicleStateAgeSeconds, "vehicle state age"); - - EMPlannerVerificationHost.Verification.NearlyEqual(0.10d, configuration.Corridor.LongitudinalSampleSpacingMeters, "longitudinal sample spacing"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.025d, configuration.Corridor.LateralSampleSpacingMeters, "lateral sample spacing"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.30d, configuration.Corridor.MaximumLateralOffsetMeters, "maximum lateral offset"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.02d, configuration.Corridor.AdditionalClearanceReserveMeters, "clearance reserve"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.025d, configuration.Corridor.MaximumCollisionCheckStepMeters, "collision check step"); - - EMPlannerVerificationHost.Verification.NearlyEqual(0.50d, configuration.Frenet.MaximumProjectionDistanceMeters, "maximum projection distance"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, configuration.Frenet.MinimumFrenetDenominator, "minimum Frenet denominator"); - EMPlannerVerificationHost.Verification.NearlyEqual(1e-8d, configuration.Frenet.BoundaryAnchorToleranceMeters, "boundary anchor tolerance"); - - EMPlannerVerificationHost.Verification.NearlyEqual(0.05d, configuration.Lateral.MaximumLateralStepPerIterationMeters, "lateral trust region"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.50d, configuration.Lateral.MaximumLateralSlope, "maximum lateral slope"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, configuration.Lateral.MaximumLateralSecondDerivativePerMeter, "maximum lateral second derivative"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, configuration.Lateral.MaximumLateralThirdDerivativePerSquareMeter, "maximum lateral third derivative"); - - EMPlannerVerificationHost.Verification.NearlyEqual(1d, configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond, "maximum forward speed"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.5d, configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond, "maximum reverse speed"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared, "maximum acceleration"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.30d, configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared, "maximum deceleration"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.50d, configuration.Longitudinal.MaximumJerkMetersPerSecondCubed, "maximum jerk"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared, "maximum lateral acceleration"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.50d, configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond, "maximum curvature rate"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.01d, configuration.Longitudinal.StopSpeedToleranceMetersPerSecond, "stop speed tolerance"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, configuration.Longitudinal.ZeroSpeedHoldSeconds, "zero speed hold"); - - EMPlannerVerificationHost.Verification.Equal(5, configuration.Solver.MaximumOuterIterations, "maximum outer iterations"); - EMPlannerVerificationHost.Verification.Equal(4000, configuration.Solver.MaximumOsqpIterations, "maximum OSQP iterations"); - EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.AbsoluteTolerance, "absolute tolerance"); - EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.RelativeTolerance, "relative tolerance"); - EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.StrictResidualTolerance, "strict residual tolerance"); - EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Validation.KinematicTolerance, - "kinematic tolerance"); - EMPlannerVerificationHost.Verification.Equal(true, configuration.Solver.WarmStart, "warm start"); - EMPlannerVerificationHost.Verification.Equal(true, configuration.Solver.Polish, "polish"); - EMPlannerVerificationHost.Verification.Equal(false, configuration.Solver.NativeVerbose, "native verbose"); - - VerifyLateralWeights(configuration.Lateral.Weights); - VerifyLongitudinalWeights(configuration.Longitudinal.Weights); - - EmPlannerConfiguration insufficientPreview = EmPlannerConfiguration.CreateDefault(); - insufficientPreview.Scheduling.DistanceHorizonMeters = 0.01d; - EmPlanningRequestValidationResult insufficientPreviewValidation = EmPlanningRequestValidator.Validate( - CreateValidRequest(insufficientPreview)); - AssertStatus(EmPlanningStatus.InvalidInput, insufficientPreviewValidation, "insufficient stopping preview"); - EMPlannerVerificationHost.Verification.True( - insufficientPreviewValidation.FailureReason.Contains("DistanceHorizonMeters") && - insufficientPreviewValidation.FailureReason.Contains("required") && - insufficientPreviewValidation.FailureReason.Contains("configured"), - "insufficient stopping preview describes configured and required distance"); - - EmPlanningRequest valid = CreateValidRequest(configuration); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(null!, valid.Map, valid.Vehicle, valid.VehicleState, configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "null reference path"); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, null!, valid.Vehicle, valid.VehicleState, configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "null map"); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, valid.VehicleState, null!, 0, EmMotionModel.NonholonomicForwardReverse)), "null configuration"); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, CreateMap(false), valid.Vehicle, valid.VehicleState, configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "map not ready"); - AssertStatus(EmPlanningStatus.InvalidReferencePath, EmPlanningRequestValidator.Validate( - CreateRequest(CreateFailedReferencePath(), valid.Map, valid.Vehicle, valid.VehicleState, configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "smoothing failure"); - AssertStatus(EmPlanningStatus.InvalidReferencePath, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, valid.VehicleState, configuration, 1, EmMotionModel.NonholonomicForwardReverse)), "segment index"); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, CreateState(double.NaN, 9L, valid.RequestedAtUtc), configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "non-finite speed"); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, CreateState(0d, -1L, valid.RequestedAtUtc), configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "negative sequence ID"); - AssertStatus(EmPlanningStatus.StaleVehicleState, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, CreateState(0d, 9L, valid.RequestedAtUtc.AddSeconds(-1d)), configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "stale state"); - AssertStatus(EmPlanningStatus.InvalidInput, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, new VehicleParameters { LengthMeters = 1d, WidthMeters = 0.5d }, valid.VehicleState, configuration, 0, EmMotionModel.NonholonomicForwardReverse)), "missing curvature limit"); - AssertStatus(EmPlanningStatus.UnsupportedMotionMode, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, valid.VehicleState, configuration, 0, EmMotionModel.CrabTranslation)), "crab motion"); - AssertStatus(EmPlanningStatus.UnsupportedMotionMode, EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, valid.VehicleState, configuration, 0, EmMotionModel.InPlaceRotation)), "in-place rotation"); - - EmPlanningRequestValidationResult first = EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, valid.VehicleState, configuration, 1, EmMotionModel.NonholonomicForwardReverse)); - EmPlanningRequestValidationResult second = EmPlanningRequestValidator.Validate( - CreateRequest(valid.ReferencePath, valid.Map, valid.Vehicle, valid.VehicleState, configuration, 1, EmMotionModel.NonholonomicForwardReverse)); - EMPlannerVerificationHost.Verification.Equal(first.FailureReason, second.FailureReason, "deterministic rejection message"); - } - - private static void VerifyFullDirectionScopeContractsConfigurationAndValidation() - { - EmPlannerConfiguration defaults = EmPlannerConfiguration.CreateDefault(); - EmPlanningRequest fullRequest = CreateValidRequest(defaults, EmPlanningScope.FullDirectionSegment); - EMPlannerVerificationHost.Verification.Equal(EmPlanningScope.FullDirectionSegment, fullRequest.PlanningScope, - "full request freezes its scope"); - var fullMetadata = new EmTrajectoryMetadata("full-scope", DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch, - 1L, "reference", 1L, string.Empty, 0, TravelDirection.Forward, EmTerminalType.Goal, - EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.FullDirectionSegment); - EMPlannerVerificationHost.Verification.Equal(EmPlanningScope.FullDirectionSegment, fullMetadata.PlanningScope, - "metadata freezes its scope"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, - defaults.Longitudinal.DesiredForwardSpeedMetersPerSecond, "forward desired speed"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.5d, - defaults.Longitudinal.DesiredReverseSpeedMetersPerSecond, "reverse desired speed"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, - defaults.Longitudinal.MaximumForwardSpeedMetersPerSecond, "forward hard limit"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.5d, - defaults.Longitudinal.MaximumReverseSpeedMetersPerSecond, "reverse hard limit"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.20d, - defaults.Scheduling.MaximumOptimizationTimeStepSeconds, "adaptive maximum dt"); - EMPlannerVerificationHost.Verification.NearlyEqual(0.10d, - defaults.Scheduling.MaximumOptimizationSpatialStepMeters, "adaptive maximum ds"); - EMPlannerVerificationHost.Verification.Equal(401, defaults.Scheduling.MaximumOptimizationKnotCount, - "optimization knot cap"); - EMPlannerVerificationHost.Verification.Equal(5001, defaults.Scheduling.MaximumPublishedSampleCount, - "publication sample cap"); - EMPlannerVerificationHost.Verification.NearlyEqual(0d, - defaults.Validation.TerminalPositionToleranceMeters, "terminal position tolerance"); - EMPlannerVerificationHost.Verification.NearlyEqual(0d, - defaults.Validation.TerminalYawToleranceRadians, "terminal yaw tolerance"); - EMPlannerVerificationHost.Verification.Equal(EmPlanningStatus.NoProgress, EmPlanningStatus.NoProgress, - "no-progress status exists"); - EMPlannerVerificationHost.Verification.Equal(EmPlanningStatus.TerminalPoseMismatch, - EmPlanningStatus.TerminalPoseMismatch, "terminal-pose status exists"); - EMPlannerVerificationHost.Verification.Equal(EmPlanningStatus.FullSegmentResourceLimitExceeded, - EmPlanningStatus.FullSegmentResourceLimitExceeded, "resource-limit status exists"); - - EmPlannerConfiguration copied = (EmPlannerConfiguration)typeof(EmPlannerConfiguration) - .GetMethod("Copy", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! - .Invoke(defaults, null)!; - defaults.Longitudinal.DesiredForwardSpeedMetersPerSecond = 0.25d; - defaults.Scheduling.MaximumOptimizationKnotCount = 3; - defaults.Validation.TerminalPositionToleranceMeters = 0.01d; - EMPlannerVerificationHost.Verification.NearlyEqual(1d, - copied.Longitudinal.DesiredForwardSpeedMetersPerSecond, "copy keeps desired forward speed"); - EMPlannerVerificationHost.Verification.Equal(401, copied.Scheduling.MaximumOptimizationKnotCount, - "copy keeps knot cap"); - EMPlannerVerificationHost.Verification.NearlyEqual(0d, - copied.Validation.TerminalPositionToleranceMeters, "copy keeps terminal tolerance"); - - EmPlannerConfiguration invalidKnotCap = EmPlannerConfiguration.CreateDefault(); - invalidKnotCap.Scheduling.MaximumOptimizationKnotCount = 2; - AssertStatus(EmPlanningStatus.InvalidInput, - EmPlanningRequestValidator.Validate(CreateValidRequest(invalidKnotCap, EmPlanningScope.FullDirectionSegment)), - "invalid optimization knot cap"); - EmPlannerConfiguration invalidDesiredSpeed = EmPlannerConfiguration.CreateDefault(); - invalidDesiredSpeed.Longitudinal.DesiredForwardSpeedMetersPerSecond = 1.01d; - AssertStatus(EmPlanningStatus.InvalidInput, - EmPlanningRequestValidator.Validate(CreateValidRequest(invalidDesiredSpeed, EmPlanningScope.FullDirectionSegment)), - "desired speed above hard limit"); - EmPlannerConfiguration invalidYawTolerance = EmPlannerConfiguration.CreateDefault(); - invalidYawTolerance.Validation.TerminalYawToleranceRadians = Math.PI + 0.01d; - AssertStatus(EmPlanningStatus.InvalidInput, - EmPlanningRequestValidator.Validate(CreateValidRequest(invalidYawTolerance, EmPlanningScope.FullDirectionSegment)), - "terminal yaw tolerance above pi"); - - EmPlannerConfiguration shortLegacyWindow = EmPlannerConfiguration.CreateDefault(); - shortLegacyWindow.Scheduling.DistanceHorizonMeters = 0.01d; - AssertStatus(EmPlanningStatus.Success, - EmPlanningRequestValidator.Validate(CreateValidRequest(shortLegacyWindow, EmPlanningScope.FullDirectionSegment)), - "full scope ignores legacy stopping-preview truncation"); - AssertStatus(EmPlanningStatus.InvalidInput, - EmPlanningRequestValidator.Validate(CreateValidRequest(shortLegacyWindow, EmPlanningScope.RollingHorizon)), - "rolling scope retains legacy stopping-preview validation"); - AssertStatus(EmPlanningStatus.InvalidInput, - EmPlanningRequestValidator.Validate(CreateValidRequest(EmPlannerConfiguration.CreateDefault(), - (EmPlanningScope)99)), "undefined planning scope"); - } - - private static void VerifyLateralWeights(LateralWeights weights) - { - EMPlannerVerificationHost.Verification.NearlyEqual(10d, weights.ReferenceOffset, "lateral reference weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, weights.HeadingDeviation, "lateral heading weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(5d, weights.SecondDerivative, "lateral second derivative weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(10d, weights.ThirdDerivative, "lateral third derivative weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(5d, weights.Curvature, "lateral curvature weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(20d, weights.CurvatureVariation, "lateral curvature variation weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(5d, weights.PreviousTrajectory, "lateral previous weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(10d, weights.RollingTerminal, "lateral rolling terminal weight"); - } - - private static void VerifyLongitudinalWeights(LongitudinalWeights weights) - { - EMPlannerVerificationHost.Verification.NearlyEqual(10d, weights.ReferenceSpeed, "longitudinal speed weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, weights.Acceleration, "longitudinal acceleration weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(10d, weights.Jerk, "longitudinal jerk weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(5d, weights.PreviousTrajectory, "longitudinal previous weight"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, weights.TerminalAcceleration, "longitudinal terminal acceleration weight"); - } - - private static void AssertStatus(EmPlanningStatus expected, EmPlanningRequestValidationResult actual, string name) - { - EMPlannerVerificationHost.Verification.Equal(expected, actual.Status, name); - } - - private static EmPlanningRequest CreateValidRequest(EmPlannerConfiguration configuration, - EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) - { - DateTimeOffset requestedAtUtc = new DateTimeOffset(2026, 8, 3, 1, 0, 0, TimeSpan.Zero); - return CreateRequest( - CreateValidReferencePath(), - CreateMap(true), - new VehicleParameters { LengthMeters = 1d, WidthMeters = 0.5d, SafetyMarginMeters = 0.05d, MaximumCurvaturePerMeter = 0.5d }, - CreateState(0d, 9L, requestedAtUtc), - configuration, - 0, - EmMotionModel.NonholonomicForwardReverse, - planningScope); - } - - private static EmPlanningRequest CreateRequest( - PathSmoothingResult referencePath, - PlanningGridMap map, - VehicleParameters vehicle, - VehicleMotionState vehicleState, - EmPlannerConfiguration configuration, - int segmentIndex, - EmMotionModel motionModel, - EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) - { - DateTimeOffset requestedAtUtc = new DateTimeOffset(2026, 8, 3, 1, 0, 0, TimeSpan.Zero); - return new EmPlanningRequest( - referencePath, - map, - vehicle, - vehicleState, - configuration, - segmentIndex, - null!, - requestedAtUtc, - requestedAtUtc.AddSeconds(0.1d), - "trajectory-1", - "reference-1", - string.Empty, - motionModel, - planningScope); - } - - private static VehicleMotionState CreateState(double speed, long sequenceId, DateTimeOffset capturedAtUtc) - { - return new VehicleMotionState(new Pose2D(0d, 0d, 0d), speed, null, capturedAtUtc, sequenceId); - } - - private static PlanningGridMap CreateMap(bool planningReady) - { - var result = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(-2000f, 2000f, -2000f, 2000f), - ResolutionMm = 100f, - ObstacleSources = new List(), - AllowExplicitEmptyMap = planningReady, - }); - if (!result.Succeeded || result.Map == null) - throw new InvalidOperationException("Unable to create test planning map."); - return result.Map; - } - - private static PathSmoothingResult CreateValidReferencePath() - { - var points = new List - { - new SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, TravelDirection.Forward, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(1d, 0d, 0d, 0d, 1d, TravelDirection.Forward, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - var segments = new List - { - new SmoothedPathSegment(0, TravelDirection.Forward, 0, 1, false, false), - }; - var metrics = new PathQualityMetrics(true, 1d, 0d, 0d, 0d, 0d, 1d, 0d, 0d, 0d, 0d, 0d); - return PathSmoothingResult.PublishLocalG2(PathSmoothingStatus.Complete, points, segments, - new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); - } - - private static PathSmoothingResult CreateFailedReferencePath() - { - return PathSmoothingResult.Failure(PathSmoothingStatus.Failed, new PathSmoothingDiagnostics()); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/FrenetChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/FrenetChecks.cs deleted file mode 100644 index fd2c7a8..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/FrenetChecks.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class FrenetChecks -{ - public static void Run() - { - VerifiesForwardProjectionAndReconstruction(); - VerifiesReverseProjectionAcrossYawWrap(); - VerifiesBoundedProjectionOnALoop(); - } - - private static void VerifiesForwardProjectionAndReconstruction() - { - DirectionSegmentView segment = CreateStraightSegment(TravelDirection.Forward, 0d, 0d); - var world = new Pose2D(1.5d, 0.2d, 0d); - var projector = new FrenetProjector(); - - Verification.True(projector.TryProject(world, segment, 0d, 4d, 0.5d, out FrenetProjection projection), - "forward projection succeeds"); - Verification.NearlyEqual(1.5d, projection.ReferenceS, "forward reference s"); - Verification.NearlyEqual(0.2d, projection.LateralOffset, "forward positive l is travel-left"); - Verification.True(FrenetTransform.TryReconstruct(projection.ReferencePoint, projection.LateralOffset, 0d, 0.2d, - out Pose2D reconstructed), "forward reconstruction succeeds"); - Verification.NearlyEqual(world.X, reconstructed.X, "forward reconstructed x"); - Verification.NearlyEqual(world.Y, reconstructed.Y, "forward reconstructed y"); - Verification.NearlyEqual(0d, reconstructed.Heading, "forward reconstructed vehicle yaw"); - } - - private static void VerifiesReverseProjectionAcrossYawWrap() - { - const double vehicleYaw = -Math.PI + 0.01d; - const double travelYaw = 0.01d; - DirectionSegmentView segment = CreateStraightSegment(TravelDirection.Reverse, vehicleYaw, travelYaw); - double expectedS = 1.5d; - double expectedL = 0.2d; - var world = new Pose2D( - expectedS * Math.Cos(travelYaw) - expectedL * Math.Sin(travelYaw), - expectedS * Math.Sin(travelYaw) + expectedL * Math.Cos(travelYaw), - vehicleYaw); - var projector = new FrenetProjector(); - - Verification.True(projector.TryProject(world, segment, 0d, 4d, 0.5d, out FrenetProjection projection), - "reverse projection succeeds"); - Verification.NearlyEqual(expectedS, projection.ReferenceS, "reverse reference s"); - Verification.NearlyEqual(expectedL, projection.LateralOffset, "reverse positive l is travel-left and body-right"); - Verification.True(FrenetTransform.TryReconstruct(projection.ReferencePoint, projection.LateralOffset, 0d, 0.2d, - out Pose2D reconstructed), "reverse reconstruction succeeds"); - Verification.NearlyEqual(world.X, reconstructed.X, "reverse reconstructed x"); - Verification.NearlyEqual(world.Y, reconstructed.Y, "reverse reconstructed y"); - Verification.NearlyEqual(AngleMath.NormalizeRadians(vehicleYaw), reconstructed.Heading, - "reverse reconstructed vehicle yaw"); - - DirectionSegmentView wrapped = CreateWrappedForwardSegment(); - FrenetReferencePoint wrappedPoint = ReferencePathInterpolator.Interpolate(wrapped, 1.5d); - Verification.NearlyEqual(3.18d, wrappedPoint.UnwrappedVehicleYaw, "interpolated yaw remains unwrapped internally"); - Verification.NearlyEqual(AngleMath.NormalizeRadians(3.18d), wrappedPoint.VehicleYaw, - "interpolated public yaw is normalized"); - Verification.True(!FrenetTransform.TryReconstruct(new FrenetReferencePoint(0d, 0d, 0d, 0d, 0d, - TravelDirection.Forward, 1d, 0d, 0d, 0d), 1d, 0d, 0.2d, out _), - "singular Frenet reconstruction is rejected"); - } - - private static void VerifiesBoundedProjectionOnALoop() - { - DirectionSegmentView segment = CreateLoopSegment(); - var projector = new FrenetProjector(); - var world = new Pose2D(1d, 0.2d, Math.PI); - - Verification.True(projector.TryProject(world, segment, 2.2d, 4.2d, 0.1d, out FrenetProjection projection), - "bounded loop projection succeeds"); - Verification.NearlyEqual(3.2d, projection.ReferenceS, "bounded loop picks the local upper branch"); - Verification.True(projection.ReferenceS >= 2.2d && projection.ReferenceS <= 4.2d, - "projection remains in supplied reference-s interval"); - Verification.True(FrenetTransform.TryReconstruct(projection.ReferencePoint, projection.LateralOffset, 0d, 0.2d, - out Pose2D reconstructed), "loop reconstruction succeeds"); - Verification.NearlyEqual(world.X, reconstructed.X, "loop reconstructed x"); - Verification.NearlyEqual(world.Y, reconstructed.Y, "loop reconstructed y"); - } - - private static DirectionSegmentView CreateStraightSegment(TravelDirection direction, double vehicleYaw, - double travelYaw) - { - var points = new List - { - Point(0d, 0d, AngleMath.NormalizeRadians(vehicleYaw), vehicleYaw, 0d, direction), - Point(4d * Math.Cos(travelYaw), 4d * Math.Sin(travelYaw), AngleMath.NormalizeRadians(vehicleYaw), - vehicleYaw, 4d, direction), - }; - return CreateSegment(direction, points); - } - - private static DirectionSegmentView CreateWrappedForwardSegment() - { - var points = new List - { - Point(0d, 0d, AngleMath.NormalizeRadians(3.12d), 3.12d, 0d, TravelDirection.Forward), - Point(1d, 0d, AngleMath.NormalizeRadians(3.16d), 3.16d, 1d, TravelDirection.Forward), - Point(2d, 0d, AngleMath.NormalizeRadians(3.20d), 3.20d, 2d, TravelDirection.Forward), - }; - return CreateSegment(TravelDirection.Forward, points); - } - - private static DirectionSegmentView CreateLoopSegment() - { - var points = new List - { - Point(0d, 0d, 0d, 0d, 0d, TravelDirection.Forward), - Point(2d, 0d, 0d, 0d, 2d, TravelDirection.Forward), - Point(2d, 0.2d, Math.PI / 2d, Math.PI / 2d, 2.2d, TravelDirection.Forward), - Point(0d, 0.2d, Math.PI, Math.PI, 4.2d, TravelDirection.Forward), - }; - return CreateSegment(TravelDirection.Forward, points); - } - - private static DirectionSegmentView CreateSegment(TravelDirection direction, IReadOnlyList points) - { - double length = points[points.Count - 1].ArcLength; - return new DirectionSegmentView(0, direction, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, length, EmBoundaryType.Goal, length), 0d); - } - - private static SmoothedPathPoint Point(double x, double y, double heading, double unwrappedHeading, double s, - TravelDirection direction) - { - return new SmoothedPathPoint(x, y, heading, unwrappedHeading, s, direction, 0d, 0d, 0d, 1d, - false, SmoothedPathPointSource.Anchor); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs deleted file mode 100644 index 0f69da2..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs +++ /dev/null @@ -1,472 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class LateralIntegrationChecks -{ - public static void Run() - { - VerifiesValidatedCandidateSurvivesLaterTimeout(); - VerifiesInvalidVectorsAndInaccurateResidualsNeverBecomeFallbacks(); - VerifiesCancellationAfterStrictCandidateNeverPublishesFallback(); - VerifiesRejectedCandidateAdvancesTheNextLateralLinearization(); - VerifiesTrustRegionWarmStartAndOuterIterationLimit(); - VerifiesCancellationAndTimeoutWithoutCandidate(); - VerifiesLateralPlannerDelegatesToTheSequentialOptimizer(); - } - - public static void RunRealOsqp() - { - foreach (LateralScenario scenario in CreateRealOsqpScenarios()) - { - LateralPlanningResult first = new LateralPlanner(new OsqpNativeSolver()).Plan(scenario.Input, - CancellationToken.None); - LateralPlanningResult second = new LateralPlanner(new OsqpNativeSolver()).Plan(scenario.Input, - CancellationToken.None); - VerifyRealScenarioResult(scenario, first); - VerifyRealScenarioResult(scenario, second); - VerifyDeterministicResult(scenario.Name, first, second); - if (scenario.RequiresSeedConnectedInterval) - { - for (int index = 0; index < first.Path.Points.Count; index++) - Verification.True(first.Path.Points[index].L <= -0.05d + 1e-10d, - scenario.Name + " remains in the seed-connected obstacle corridor"); - } - } - } - - public static void RunRealOsqpInCleanPluginBundle() - { - string pluginDirectory = Path.Combine(Path.GetTempPath(), "em-planner-lateral-real-" + Guid.NewGuid().ToString("N")); - try - { - Directory.CreateDirectory(pluginDirectory); - foreach (string sourcePath in Directory.GetFiles(AppContext.BaseDirectory)) - { - if (string.Equals(Path.GetFileName(sourcePath), "osqp.dll", StringComparison.OrdinalIgnoreCase)) - continue; - File.Copy(sourcePath, Path.Combine(pluginDirectory, Path.GetFileName(sourcePath)), false); - } - string nativeSource = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ThirdParty", - "OSQP", "win-x64", "osqp.dll")); - Verification.True(File.Exists(nativeSource), "pinned OSQP DLL is available for the real lateral bundle"); - File.Copy(nativeSource, Path.Combine(pluginDirectory, "osqp.dll"), false); - - var startInfo = new ProcessStartInfo - { - FileName = Path.Combine(pluginDirectory, "EMPlannerVerificationHost.exe"), - Arguments = "lateral-real-osqp-probe", - WorkingDirectory = pluginDirectory, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - using (var process = new Process { StartInfo = startInfo }) - { - process.Start(); - string standardOutput = process.StandardOutput.ReadToEnd(); - string standardError = process.StandardError.ReadToEnd(); - process.WaitForExit(); - if (process.ExitCode != 0 || standardOutput.IndexOf("PASS lateral-real-osqp", StringComparison.Ordinal) < 0) - { - throw new InvalidOperationException("Real lateral OSQP clean-plugin probe exited " + process.ExitCode + ": " + - standardError + standardOutput); - } - } - } - finally - { - if (Directory.Exists(pluginDirectory)) - Directory.Delete(pluginDirectory, true); - } - } - - private static void VerifiesValidatedCandidateSurvivesLaterTimeout() - { - LateralPlanningInput input = CreateInput(); - double[] valid = CreatePrimal(input, 0.02d); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, valid, 10d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 10d), - }); - - LateralPlanningResult result = new SequentialConvexOptimizer(solver).Optimize(input, CancellationToken.None); - - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "timeout after an independently validated candidate returns fallback success"); - LateralPath fallbackPath = result.Path ?? throw new InvalidOperationException("Fallback path was not returned."); - Verification.True(fallbackPath.IsIndependentlyValidated, - "fallback path remains independently validated"); - Verification.NearlyEqual(0.02d, fallbackPath.Points[1].L, - "first valid candidate remains the fallback path"); - } - - private static void VerifiesInvalidVectorsAndInaccurateResidualsNeverBecomeFallbacks() - { - LateralPlanningInput input = CreateInput(); - double[] valid = CreatePrimal(input, 0.02d); - double[] invalid = CreatePrimal(input, 0.40d); - - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, valid, 10d), - Result(QpSolveStatus.Solved, invalid, 9d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 9d), - }); - LateralPlanningResult preserved = new SequentialConvexOptimizer(solver).Optimize(input, CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, preserved.Status, - "invalid solved vector does not discard an earlier fallback"); - Verification.NearlyEqual(0.02d, preserved.Path.Points[1].L, - "invalid solved vector does not replace the fallback candidate"); - - var inaccurateResidual = new FakeQpSolver(new[] - { - Result(QpSolveStatus.SolvedInaccurate, valid, 10d, 2e-5d, 0d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 10d), - }); - LateralPlanningResult rejectedResidual = new SequentialConvexOptimizer(inaccurateResidual).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SolverTimedOut, rejectedResidual.Status, - "SolvedInaccurate above strict residual threshold is rejected"); - Verification.True(ReferenceEquals(null, rejectedResidual.Path), - "rejected inaccurate result does not publish a path"); - - var inaccurateGeometry = new FakeQpSolver(new[] - { - Result(QpSolveStatus.SolvedInaccurate, invalid, 10d, 0d, 0d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 10d), - }); - LateralPlanningResult rejectedGeometry = new SequentialConvexOptimizer(inaccurateGeometry).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SolverTimedOut, rejectedGeometry.Status, - "SolvedInaccurate still requires full independent lateral validation"); - } - - private static void VerifiesCancellationAfterStrictCandidateNeverPublishesFallback() - { - LateralPlanningInput input = CreateInput(); - double[] valid = CreatePrimal(input, 0.02d); - using var cancellation = new CancellationTokenSource(); - var solver = new CancellingQpSolver( - new FakeQpSolver(Result(QpSolveStatus.Solved, valid, 10d)), cancellation); - - LateralPlanningResult result = new SequentialConvexOptimizer(solver).Optimize(input, cancellation.Token); - - Verification.Equal(EmPlanningStatus.Cancelled, result.Status, - "cancellation after a strict LS candidate is never fallback success"); - Verification.True(result.Path == null, "cancelled LS result exposes no path"); - } - - private static void VerifiesRejectedCandidateAdvancesTheNextLateralLinearization() - { - LateralPlanningInput input = CreateInput(0.10d); - double[] strict = CreatePrimal(input, 0.01d); - double[] rejected = CreatePrimal(input, 0.02d); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, strict, 10d), - Result(QpSolveStatus.Solved, rejected, 9d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 9d), - }); - - LateralPlanningResult result = new SequentialConvexOptimizer(solver).Optimize(input, CancellationToken.None); - - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "later timeout preserves the earlier strict candidate"); - var layout = new LateralVariableLayout(input.ReferenceStations.Count); - Verification.NearlyEqual(0.02d, solver.WarmStarts[2][layout.L(1)], - "the next LS QP warm-starts from the rejected but parseable candidate"); - FindSingleVariableBounds(solver.Problems[2], layout.L(1), out double lower, out double upper); - Verification.NearlyEqual(-0.03d, lower, - "the next LS trust region is centered on the rejected candidate"); - Verification.NearlyEqual(0.07d, upper, - "the next LS trust region is centered on the rejected candidate"); - } - - private static void VerifiesTrustRegionWarmStartAndOuterIterationLimit() - { - LateralPlanningInput input = CreateInput(); - var trustSolver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, CreatePrimal(input, 0.02d), 10d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 10d), - }); - new SequentialConvexOptimizer(trustSolver).Optimize(input, CancellationToken.None); - - var layout = new LateralVariableLayout(input.ReferenceStations.Count); - FindSingleVariableBounds(trustSolver.Problems[0], layout.L(1), out double initialLower, out double initialUpper); - FindSingleVariableBounds(trustSolver.Problems[1], layout.L(1), out double nextLower, out double nextUpper); - Verification.NearlyEqual(-0.05d, initialLower, "initial trust-region lower bound"); - Verification.NearlyEqual(0.05d, initialUpper, "initial trust-region upper bound"); - Verification.NearlyEqual(-0.03d, nextLower, "trust region is centered on previous iterate"); - Verification.NearlyEqual(0.07d, nextUpper, "trust region never exceeds 0.05m around previous iterate"); - Verification.Equal(layout.VariableCount, trustSolver.WarmStarts[1].Count, - "next QP receives the complete previous primal warm start"); - Verification.NearlyEqual(0.02d, trustSolver.WarmStarts[1][layout.L(1)], - "warm start retains the prior lateral iterate"); - - var limitResults = new List(); - for (int index = 1; index <= 5; index++) - limitResults.Add(Result(QpSolveStatus.Solved, CreatePrimal(input, 0.02d * index), 100d - index)); - var limitSolver = new FakeQpSolver(limitResults); - LateralPlanningResult limited = new SequentialConvexOptimizer(limitSolver).Optimize(input, CancellationToken.None); - Verification.Equal(5, limitSolver.SolveCallCount, "outer loop stops after at most five QP calls"); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, limited.Status, - "last strict candidate is reported as fallback at outer iteration limit"); - Verification.True(limited.FailureReason.Length > 0, - "outer iteration fallback preserves a non-empty diagnostic"); - } - - private static void VerifiesCancellationAndTimeoutWithoutCandidate() - { - LateralPlanningInput input = CreateInput(); - var cancellationSolver = new FakeQpSolver(Result(QpSolveStatus.Solved, CreatePrimal(input, 0d), 1d)); - using var cancellation = new CancellationTokenSource(); - cancellation.Cancel(); - LateralPlanningResult cancelled = new SequentialConvexOptimizer(cancellationSolver).Optimize(input, - cancellation.Token); - Verification.Equal(EmPlanningStatus.Cancelled, cancelled.Status, "cancellation before a solver call is cancelled"); - Verification.Equal(0, cancellationSolver.SolveCallCount, "cancelled solve does not invoke the solver"); - - var timeoutSolver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d)); - LateralPlanningResult timeout = new SequentialConvexOptimizer(timeoutSolver).Optimize(input, CancellationToken.None); - Verification.Equal(EmPlanningStatus.SolverTimedOut, timeout.Status, - "timeout without a feasible candidate is solver timed out"); - Verification.True(ReferenceEquals(null, timeout.Path), "timeout without candidate does not publish a path"); - } - - private static void VerifiesLateralPlannerDelegatesToTheSequentialOptimizer() - { - LateralPlanningInput input = CreateInput(); - double[] zero = CreatePrimal(input, 0d); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, zero, 1d), - Result(QpSolveStatus.Solved, zero, 1d), - }); - LateralPlanningResult result = new LateralPlanner(solver).Plan(input, CancellationToken.None); - Verification.Equal(EmPlanningStatus.Success, result.Status, "lateral planner returns SQP success"); - } - - private static IReadOnlyList CreateRealOsqpScenarios() - { - return new[] - { - CreateScenario("straight-empty-forward", TravelDirection.Forward, 0d, EmTerminalType.Goal, -0.3d, 0.3d, 0d, false), - CreateScenario("straight-empty-reverse", TravelDirection.Reverse, 0d, EmTerminalType.Goal, -0.3d, 0.3d, 0d, false), - CreateScenario("gentle-curve", TravelDirection.Forward, 0.05d, EmTerminalType.Goal, -0.3d, 0.3d, 0d, false), - CreateScenario("static-obstacle-narrowing", TravelDirection.Forward, 0d, EmTerminalType.RollingSafetyStop, - -0.3d, -0.05d, -0.10d, true), - CreateScenario("gear-switch-terminal", TravelDirection.Forward, 0d, EmTerminalType.GearSwitch, -0.3d, 0.3d, 0d, false), - CreateScenario("rolling-terminal", TravelDirection.Forward, 0d, EmTerminalType.RollingSafetyStop, -0.3d, 0.3d, 0d, false), - }; - } - - private static LateralScenario CreateScenario(string name, TravelDirection direction, double geometricCurvature, - EmTerminalType terminal, double corridorMinimum, double corridorMaximum, double seedL, - bool requiresSeedConnectedInterval) - { - double[] stations = { 0d, 0.5d, 1d, 1.5d, 2d }; - var points = new List(stations.Length); - var intervals = new List(stations.Length); - var seed = new List(requiresSeedConnectedInterval ? stations.Length : 0); - for (int index = 0; index < stations.Length; index++) - { - double referenceS = stations[index]; - double travelYaw = geometricCurvature * referenceS; - double x = Math.Abs(geometricCurvature) <= 1e-12d ? referenceS : Math.Sin(travelYaw) / geometricCurvature; - double y = Math.Abs(geometricCurvature) <= 1e-12d ? 0d : (1d - Math.Cos(travelYaw)) / geometricCurvature; - double vehicleYaw = direction == TravelDirection.Forward ? travelYaw : travelYaw - Math.PI; - points.Add(new SmoothedPathPoint(x, y, vehicleYaw, vehicleYaw, referenceS, direction, geometricCurvature, - direction == TravelDirection.Forward ? geometricCurvature : -geometricCurvature, 0d, 1d, false, - SmoothedPathPointSource.Anchor)); - intervals.Add(new LateralInterval(referenceS, corridorMinimum, corridorMaximum, seedL)); - } - var segment = new DirectionSegmentView(0, direction, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, 2d, terminal == EmTerminalType.GearSwitch ? EmBoundaryType.GearSwitchApproach : EmBoundaryType.Goal, - 2d), 0d); - if (requiresSeedConnectedInterval) - { - for (int index = 0; index < stations.Length; index++) - seed.Add(new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, stations[index]), seedL, 0d, 0d)); - } - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Validation.SpatialToleranceMeters = 1e-5d; - configuration.Validation.KinematicTolerance = 1e-5d; - var vehicle = new VehicleParameters - { - LengthMeters = 0.1d, - WidthMeters = 0.1d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 1d, - }; - return new LateralScenario(name, new LateralPlanningInput(segment, new StaticCorridor(intervals), - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), seedL, 0d, 0d), terminal, vehicle, - configuration, seed), requiresSeedConnectedInterval); - } - - private static void VerifyRealScenarioResult(LateralScenario scenario, LateralPlanningResult result) - { - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - scenario.Name + " is solved or has a documented fallback: " + result.FailureReason); - LateralPath path = result.Path ?? throw new InvalidOperationException(scenario.Name + " returned no lateral path."); - Verification.True(path.IsIndependentlyValidated, scenario.Name + " path is independently validated"); - Verification.Equal(scenario.Input.ReferenceStations.Count, path.Points.Count, scenario.Name + " point count"); - double maximumCurvature = scenario.Input.Vehicle.MaximumCurvaturePerMeter.GetValueOrDefault(); - Verification.True(maximumCurvature > 0d, scenario.Name + " has a maximum vehicle curvature"); - for (int index = 0; index < path.Points.Count; index++) - { - LateralPathPoint point = path.Points[index]; - LateralInterval interval = scenario.Input.Corridor.Stations[index]; - Verification.True(point.L >= interval.MinimumL - 1e-10d && point.L <= interval.MaximumL + 1e-10d, - scenario.Name + " remains in corridor at station " + index); - Verification.True(Math.Abs(point.VehicleCurvature) <= maximumCurvature + 1e-10d, - scenario.Name + " respects vehicle curvature at station " + index); - } - Verification.NearlyEqual(scenario.Input.ReferenceStations[scenario.Input.ReferenceStations.Count - 1], - path.Points[path.Points.Count - 1].ReferenceS, scenario.Name + " ends at exact ReferenceS anchor"); - } - - private static void VerifyDeterministicResult(string name, LateralPlanningResult first, LateralPlanningResult second) - { - Verification.Equal(first.Status, second.Status, name + " deterministic status"); - LateralPath firstPath = first.Path ?? throw new InvalidOperationException(name + " first path was missing."); - LateralPath secondPath = second.Path ?? throw new InvalidOperationException(name + " second path was missing."); - Verification.Equal(firstPath.Points.Count, secondPath.Points.Count, name + " deterministic point count"); - for (int index = 0; index < firstPath.Points.Count; index++) - ComparePoint(firstPath.Points[index], secondPath.Points[index], name + " deterministic point " + index); - } - - private static void ComparePoint(LateralPathPoint left, LateralPathPoint right, string name) - { - double[] leftValues = - { - left.ReferenceS, left.PathS, left.L, left.DL, left.DDL, left.DDDL, left.X, left.Y, left.VehicleYaw, - left.GeometricCurvature, left.VehicleCurvature, left.VehicleCurvatureDerivative, - }; - double[] rightValues = - { - right.ReferenceS, right.PathS, right.L, right.DL, right.DDL, right.DDDL, right.X, right.Y, right.VehicleYaw, - right.GeometricCurvature, right.VehicleCurvature, right.VehicleCurvatureDerivative, - }; - for (int index = 0; index < leftValues.Length; index++) - Verification.True(Math.Abs(leftValues[index] - rightValues[index]) <= 1e-10d, name + " value " + index); - } - - private static LateralPlanningInput CreateInput(double maximumVehicleCurvature = 1d) - { - var points = new List - { - Point(0d, 0d), - Point(1d, 1d), - Point(2d, 2d), - }; - var segment = new DirectionSegmentView(0, TravelDirection.Forward, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, 2d, EmBoundaryType.Goal, 2d), 0d); - var corridor = new StaticCorridor(new[] - { - new LateralInterval(0d, -0.3d, 0.3d, 0d), - new LateralInterval(1d, -0.3d, 0.3d, 0d), - new LateralInterval(2d, -0.3d, 0.3d, 0d), - }); - var vehicle = new VehicleParameters - { - LengthMeters = 0.1d, - WidthMeters = 0.1d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = maximumVehicleCurvature, - }; - return new LateralPlanningInput(segment, corridor, - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0d, 0d, 0d), - EmTerminalType.Goal, vehicle, EmPlannerConfiguration.CreateDefault(), Array.Empty()); - } - - private static SmoothedPathPoint Point(double x, double pathS) - { - return new SmoothedPathPoint(x, 0d, 0d, 0d, pathS, TravelDirection.Forward, 0d, 0d, 0d, 1d, - false, SmoothedPathPointSource.Anchor); - } - - private static QpSolveResult Result(QpSolveStatus status, IReadOnlyList primal, double objective, - double primalResidual = 0d, double dualResidual = 0d) - { - return new QpSolveResult(status, primal, objective, primalResidual, dualResidual, 1, TimeSpan.Zero, - status.ToString(), string.Empty); - } - - private static double[] CreatePrimal(LateralPlanningInput input, double middleL) - { - var layout = new LateralVariableLayout(input.ReferenceStations.Count); - double c = 6d * middleL; - var primal = new double[layout.VariableCount]; - primal[layout.L(0)] = 0d; - primal[layout.L(1)] = middleL; - primal[layout.L(2)] = 0d; - primal[layout.DL(0)] = 0d; - primal[layout.DL(1)] = 0d; - primal[layout.DL(2)] = 0d; - primal[layout.DDL(0)] = c; - primal[layout.DDL(1)] = -c; - primal[layout.DDL(2)] = c; - primal[layout.DDDL(0)] = -2d * c; - primal[layout.DDDL(1)] = 2d * c; - return primal; - } - - private static void FindSingleVariableBounds(QuadraticProgram problem, int variable, out double lower, out double upper) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - int matchingEntries = 0; - double coefficient = 0d; - for (int column = 0; column < problem.ConstraintMatrix.ColumnCount; column++) - { - for (int index = problem.ConstraintMatrix.ColumnPointers[column]; - index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) - { - if (problem.ConstraintMatrix.RowIndices[index] == row) - { - matchingEntries++; - if (column == variable) - coefficient = problem.ConstraintMatrix.Values[index]; - } - } - } - if (matchingEntries == 1 && Math.Abs(coefficient - 1d) <= 1e-12d && - Math.Abs(problem.LowerBounds[row] - problem.UpperBounds[row]) > 1e-12d) - { - lower = problem.LowerBounds[row]; - upper = problem.UpperBounds[row]; - return; - } - } - throw new InvalidOperationException("Expected single-variable lateral trust-region row was not found."); - } - - private sealed class LateralScenario - { - public LateralScenario(string name, LateralPlanningInput input, bool requiresSeedConnectedInterval) - { - Name = name; - Input = input; - RequiresSeedConnectedInterval = requiresSeedConnectedInterval; - } - - public string Name { get; } - public LateralPlanningInput Input { get; } - public bool RequiresSeedConnectedInterval { get; } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs deleted file mode 100644 index a0a7146..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs +++ /dev/null @@ -1,646 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class LateralModelChecks -{ - public static void Run() - { - VerifiesDeterministicVariableLayout(); - VerifiesExactDiscreteDynamicsForUnequalStations(); - VerifiesPlanningInputBoundariesAndDefensiveCopies(); - VerifiesLateralResultPublicationContract(); - VerifiesNormalizedObjectiveAndHardConstraints(); - VerifiesVehicleCurvatureIsAHardQpConstraint(); - VerifiesAllNamedCostScales(); - VerifiesEmptyHardBoundIntersectionFailsBeforeSolve(); - VerifiesFakeSolverCapturesTheNeutralQpBoundary(); - VerifiesNonlinearGeometryInBothDirections(); - VerifiesIndependentGeometryValidationRejectsUnsafeOrTamperedPaths(); - } - - private static void VerifiesDeterministicVariableLayout() - { - LateralVariableLayout layout = CreateLayout(4); - - Verification.Equal(15, layout.VariableCount, "layout variable count"); - for (int index = 0; index < 4; index++) - { - Verification.Equal(index, layout.L(index), "l index " + index); - Verification.Equal(4 + index, layout.DL(index), "dl index " + index); - Verification.Equal(8 + index, layout.DDL(index), "ddl index " + index); - } - for (int index = 0; index < 3; index++) - Verification.Equal(12 + index, layout.DDDL(index), "dddl index " + index); - - ExpectArgumentException(() => CreateLayout(1), "layout rejects fewer than two stations"); - ExpectArgumentException(() => layout.L(4), "l index bounds check"); - ExpectArgumentException(() => layout.DL(-1), "dl index bounds check"); - ExpectArgumentException(() => layout.DDL(4), "ddl index bounds check"); - ExpectArgumentException(() => layout.DDDL(3), "dddl index bounds check"); - } - - private static void VerifiesExactDiscreteDynamicsForUnequalStations() - { - double[] stations = { 0d, 0.4d, 1.25d, 2.5d }; - double[] jerks = { 0.5d, -0.3d, 0.2d }; - LateralCandidate candidate = LateralCandidate.Integrate(stations, 0.1d, -0.2d, 0.3d, jerks); - - for (int index = 0; index < jerks.Length; index++) - { - double ds = stations[index + 1] - stations[index]; - Verification.NearlyEqual(candidate.DDL[index] + ds * candidate.DDDL[index], candidate.DDL[index + 1], - "exact ddl integration " + index); - Verification.NearlyEqual(candidate.DL[index] + ds * candidate.DDL[index] + 0.5d * ds * ds * candidate.DDDL[index], - candidate.DL[index + 1], "exact dl integration " + index); - Verification.NearlyEqual(candidate.L[index] + ds * candidate.DL[index] + 0.5d * ds * ds * candidate.DDL[index] + - ds * ds * ds * candidate.DDDL[index] / 6d, candidate.L[index + 1], "exact l integration " + index); - } - Verification.True(candidate.SatisfiesExactDiscreteDynamics(1e-12d), "integrated candidate validates exact dynamics"); - - LateralCandidate inconsistent = new LateralCandidate(new[] { 0d, 1d }, new[] { 0d, 1d }, - new[] { 0d, 0d }, new[] { 0d, 0d }, new[] { 0d }); - Verification.True(!inconsistent.SatisfiesExactDiscreteDynamics(1e-12d), "candidate detects inconsistent dynamics"); - ExpectArgumentException(() => new LateralCandidate(new[] { 0d, 0d }, new[] { 0d, 0d }, - new[] { 0d, 0d }, new[] { 0d, 0d }, new[] { 0d }), "candidate rejects non-increasing stations"); - } - - private static void VerifiesPlanningInputBoundariesAndDefensiveCopies() - { - DirectionSegmentView segment = CreateStraightSegment(); - var corridorStations = new[] - { - new LateralInterval(0d, -0.3d, 0.3d, 0d), - new LateralInterval(1d, -0.3d, 0.3d, 0d), - new LateralInterval(2d, -0.3d, 0.3d, 0d), - }; - var seeds = new[] - { - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0d, 0d, 0d), - }; - LateralPlanningInput input = new LateralPlanningInput(segment, new StaticCorridor(corridorStations), seeds[0], - EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), seeds); - corridorStations[1] = new LateralInterval(1d, -0.1d, 0.1d, 0d); - seeds[0] = new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0.2d, 0d, 0d); - - Verification.NearlyEqual(-0.3d, input.Corridor.Stations[1].MinimumL, "input copies corridor stations"); - Verification.NearlyEqual(0d, input.PreviousTrajectorySeed[0].LateralOffset, "input copies seed list"); - ExpectArgumentException(() => new LateralPlanningInput(segment, - new StaticCorridor(new[] { new LateralInterval(0d, -0.3d, 0.3d, 0d) }), - input.StartProjection, EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), - "input rejects fewer than two stations"); - ExpectArgumentException(() => new LateralPlanningInput(segment, - new StaticCorridor(new[] - { - new LateralInterval(0d, -0.3d, 0.3d, 0d), - new LateralInterval(0d, -0.3d, 0.3d, 0d), - }), input.StartProjection, EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), - "input rejects non-increasing corridor stations"); - ExpectArgumentException(() => new LateralPlanningInput(segment, - new StaticCorridor(new[] - { - new LateralInterval(0.1d, -0.3d, 0.3d, 0d), - new LateralInterval(2d, -0.3d, 0.3d, 0d), - }), input.StartProjection, EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), - "input rejects start-corridor station mismatch"); - ExpectArgumentException(() => new LateralPlanningInput(segment, - new StaticCorridor(new[] - { - new LateralInterval(0d, -0.1d, 0.1d, 0d), - new LateralInterval(2d, -0.1d, 0.1d, 0d), - }), new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0.2d, 0d, 0d), - EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), - "input rejects start projection outside first hard interval"); - } - - private static void VerifiesLateralResultPublicationContract() - { - LateralPath unvalidated = new LateralPath(new[] { CreatePathPoint(0d) }, false); - LateralPath validated = new LateralPath(new[] { CreatePathPoint(0d), CreatePathPoint(1d) }, true); - - ExpectArgumentException(() => new LateralPlanningResult(EmPlanningStatus.Success, unvalidated, string.Empty), - "success requires an independently validated path"); - ExpectArgumentException(() => new LateralPlanningResult(EmPlanningStatus.SuccessWithFallback, - new LateralPath(Array.Empty(), true), string.Empty), - "fallback requires a non-empty path"); - ExpectArgumentException(() => new LateralPlanningResult(EmPlanningStatus.LateralInfeasible, validated, string.Empty), - "failed result has no candidate"); - LateralPlanningResult result = new LateralPlanningResult(EmPlanningStatus.SuccessWithFallback, validated, "fallback"); - Verification.Equal(validated, result.Path, "fallback path is preserved"); - } - - private static void VerifiesNormalizedObjectiveAndHardConstraints() - { - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - LateralPlanningInput input = CreateModelInput(EmTerminalType.Goal, configuration, - new[] { 0.2d, -0.1d, 0.3d }); - LateralCandidate linearization = LateralCandidate.Integrate(input.ReferenceStations, 0d, 0d, 0d, - new[] { 0d, 0d }); - LateralConstraintBuilder builder = CreateConstraintBuilder(); - - Verification.True(builder.TryBuild(input, linearization, out QuadraticProgram problem, out string failureReason), - "unit-scale QP builds: " + failureReason); - var layout = new LateralVariableLayout(3); - Verification.NearlyEqual(30d, MatrixValue(problem.UpperTriangularP, layout.L(0), layout.L(0)), - "reference plus previous P coefficient"); - Verification.NearlyEqual(20d, MatrixValue(problem.UpperTriangularP, layout.DDDL(0), layout.DDDL(0)), - "jerk P coefficient"); - Verification.NearlyEqual(-2d, problem.LinearCost[layout.L(0)], "previous-seed q coefficient"); - - for (int interval = 0; interval < 2; interval++) - { - double ds = input.ReferenceStations[interval + 1] - input.ReferenceStations[interval]; - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary - { - { layout.DDL(interval), -1d }, - { layout.DDL(interval + 1), 1d }, - { layout.DDDL(interval), -ds }, - }), "ddl dynamics equality " + interval); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary - { - { layout.DL(interval), -1d }, - { layout.DL(interval + 1), 1d }, - { layout.DDL(interval), -ds }, - { layout.DDDL(interval), -0.5d * ds * ds }, - }), "dl dynamics equality " + interval); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary - { - { layout.L(interval), -1d }, - { layout.L(interval + 1), 1d }, - { layout.DL(interval), -ds }, - { layout.DDL(interval), -0.5d * ds * ds }, - { layout.DDDL(interval), -ds * ds * ds / 6d }, - }), "l dynamics equality " + interval); - } - - for (int station = 0; station < layout.StationCount; station++) - { - Verification.True(HasFiniteNonEqualityBound(problem, layout.L(station)), "finite lateral hard bound " + station); - Verification.True(HasFiniteNonEqualityBound(problem, layout.DL(station)), "finite slope hard bound " + station); - Verification.True(HasFiniteNonEqualityBound(problem, layout.DDL(station)), "finite second-derivative hard bound " + station); - } - for (int interval = 0; interval < layout.StationCount - 1; interval++) - Verification.True(HasFiniteNonEqualityBound(problem, layout.DDDL(interval)), "finite jerk hard bound " + interval); - - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary { { layout.L(2), 1d } }), - "goal terminal l equality"); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary { { layout.DL(2), 1d } }), - "goal terminal dl equality"); - - LateralPlanningInput rolling = CreateModelInput(EmTerminalType.RollingSafetyStop, configuration, - new[] { 0.2d, -0.1d, 0.3d }); - Verification.True(builder.TryBuild(rolling, linearization, out QuadraticProgram rollingProblem, out string rollingReason), - "rolling QP builds: " + rollingReason); - Verification.NearlyEqual(50d, MatrixValue(rollingProblem.UpperTriangularP, layout.L(2), layout.L(2)), - "rolling terminal adds normalized objective cost"); - Verification.Equal(0, CountExactEqualityRows(rollingProblem, new Dictionary { { layout.L(2), 1d } }), - "rolling terminal has no l equality"); - Verification.Equal(0, CountExactEqualityRows(rollingProblem, new Dictionary { { layout.DL(2), 1d } }), - "rolling terminal has no dl equality"); - } - - private static void VerifiesAllNamedCostScales() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Corridor.MaximumLateralOffsetMeters = 2d; - configuration.Lateral.MaximumLateralSlope = 4d; - configuration.Lateral.MaximumLateralSecondDerivativePerMeter = 5d; - configuration.Lateral.MaximumLateralThirdDerivativePerSquareMeter = 6d; - LateralPlanningInput input = CreateModelInput(EmTerminalType.RollingSafetyStop, configuration, - new[] { 0.2d, 0.2d, 0.2d }, referenceCurvatureDerivative: 4d, maximumVehicleCurvature: 7d); - LateralCandidate linearization = LateralCandidate.Integrate(input.ReferenceStations, 0d, 0d, 0d, - new[] { 0d, 0d }); - LateralConstraintBuilder builder = CreateConstraintBuilder(); - - Verification.True(builder.TryBuild(input, linearization, out QuadraticProgram problem, out string failureReason), - "non-unit-scale QP builds: " + failureReason); - var layout = new LateralVariableLayout(3); - Verification.NearlyEqual(7.5d, MatrixValue(problem.UpperTriangularP, layout.L(0), layout.L(0)), - "reference and previous costs divide by lateral scale squared"); - Verification.NearlyEqual(-0.5d, problem.LinearCost[layout.L(0)], - "previous target coefficient divides by lateral scale squared"); - Verification.NearlyEqual(0.125d, MatrixValue(problem.UpperTriangularP, layout.DL(0), layout.DL(0)), - "heading cost divides by slope scale squared"); - Verification.NearlyEqual(5d / 9d, MatrixValue(problem.UpperTriangularP, layout.DDDL(0), layout.DDDL(0)), - "jerk cost divides by third-derivative scale squared"); - Verification.NearlyEqual(0.4d + 10d / 49d + 3.125d, - MatrixValue(problem.UpperTriangularP, layout.DDL(0), layout.DDL(0)), - "second derivative, curvature, and curvature variation use their named scales"); - Verification.NearlyEqual(12.5d, MatrixValue(problem.UpperTriangularP, layout.L(2), layout.L(2)), - "rolling terminal cost divides by lateral scale squared"); - - EmPlannerConfiguration denominatorConfiguration = CreateUnitScaleConfiguration(); - denominatorConfiguration.Lateral.MaximumLateralStepPerIterationMeters = 0.5d; - LateralPlanningInput denominatorInput = CreateModelInput(EmTerminalType.RollingSafetyStop, denominatorConfiguration, - Array.Empty(), 0d, 2d); - Verification.True(builder.TryBuild(denominatorInput, linearization, out QuadraticProgram denominatorProblem, - out string denominatorReason), "denominator QP builds: " + denominatorReason); - Verification.True(HasBoundWithUpper(denominatorProblem, layout.L(0), 0.4d), - "Frenet denominator is intersected as a finite hard lateral bound"); - } - - private static void VerifiesVehicleCurvatureIsAHardQpConstraint() - { - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - const double maximumVehicleCurvature = 0.25d; - LateralPlanningInput input = CreateModelInput(EmTerminalType.RollingSafetyStop, configuration, - Array.Empty(), maximumVehicleCurvature: maximumVehicleCurvature); - LateralCandidate linearization = LateralCandidate.Integrate(input.ReferenceStations, 0d, 0d, 0d, - new[] { 0d, 0d }); - - Verification.True(CreateConstraintBuilder().TryBuild(input, linearization, out QuadraticProgram problem, - out string failureReason), "curvature-constrained QP builds: " + failureReason); - - var layout = new LateralVariableLayout(input.ReferenceStations.Count); - Verification.Equal(8 * layout.StationCount - 2, problem.ConstraintCount, - "each lateral station adds one curvature hard-constraint row"); - for (int station = 0; station < layout.StationCount; station++) - { - Verification.True(HasBound(problem, new Dictionary { { layout.DDL(station), 1d } }, - -maximumVehicleCurvature, maximumVehicleCurvature), - "straight-reference curvature is a hard DDL bound at station " + station); - } - } - - private static void VerifiesEmptyHardBoundIntersectionFailsBeforeSolve() - { - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - LateralPlanningInput input = CreateModelInput(EmTerminalType.RollingSafetyStop, configuration, - Array.Empty(), 0d, 0d, 0.9d, 0.9d, 1d); - LateralCandidate linearization = LateralCandidate.Integrate(input.ReferenceStations, 0d, 0d, 0d, - new[] { 0d, 0d }); - - Verification.True(!CreateConstraintBuilder().TryBuild(input, linearization, out QuadraticProgram problem, - out string failureReason), "empty corridor/trust intersection is infeasible before solve"); - Verification.True(problem == null && failureReason.Length > 0, "infeasible build returns no QP and a reason"); - } - - private static void VerifiesFakeSolverCapturesTheNeutralQpBoundary() - { - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - LateralPlanningInput input = CreateModelInput(EmTerminalType.Goal, configuration, Array.Empty()); - LateralCandidate linearization = LateralCandidate.Integrate(input.ReferenceStations, 0d, 0d, 0d, - new[] { 0d, 0d }); - Verification.True(CreateConstraintBuilder().TryBuild(input, linearization, out QuadraticProgram problem, - out string reason), "fake solver problem builds: " + reason); - var expected = new QpSolveResult(QpSolveStatus.Solved, new double[problem.VariableCount], 0d, 0d, 0d, 1, - TimeSpan.Zero, "fake", string.Empty); - var solver = new FakeQpSolver(expected); - var settings = new QpSolverSettings(10, 1e-5d, 1e-5d, TimeSpan.FromSeconds(1d), true, false, false); - QpSolveResult actual = solver.Solve(problem, settings, new[] { 1d, 2d }, CancellationToken.None); - - Verification.Equal(expected, actual, "fake solver returns configured result"); - Verification.Equal(problem, solver.LastProblem, "fake solver records QP"); - Verification.Equal(settings, solver.LastSettings, "fake solver records settings"); - Verification.NearlyEqual(2d, solver.LastWarmStart[1], "fake solver records a defensive warm-start copy"); - } - - private static void VerifiesNonlinearGeometryInBothDirections() - { - double[] stations = { 0d, 0.7d, 2d }; - LateralCandidate candidate = LateralCandidate.Integrate(stations, 0.1d, 0.1d, 0.05d, - new[] { 0.02d, -0.03d }); - LateralGeometryEvaluator evaluator = CreateGeometryEvaluator(); - LateralSolutionValidator validator = CreateGeometryValidator(); - - VerifyGeometryForDirection(TravelDirection.Forward, 0d, candidate, evaluator, validator); - VerifyGeometryForDirection(TravelDirection.Reverse, 0d, candidate, evaluator, validator); - VerifyGeometryForDirection(TravelDirection.Forward, 0.2d, candidate, evaluator, validator); - VerifyGeometryForDirection(TravelDirection.Reverse, 0.2d, candidate, evaluator, validator); - } - - private static void VerifiesIndependentGeometryValidationRejectsUnsafeOrTamperedPaths() - { - LateralGeometryEvaluator evaluator = CreateGeometryEvaluator(); - LateralSolutionValidator validator = CreateGeometryValidator(); - double[] stations = { 0d, 1d, 2d }; - - LateralPlanningInput singularInput = CreateGeometryInput(TravelDirection.Forward, 1d, stations, 0.81d, 0d, 10d); - LateralCandidate singular = LateralCandidate.Integrate(stations, 0.81d, 0d, 0d, new[] { 0d, 0d }); - Verification.True(!evaluator.TryEvaluate(singularInput, singular, out LateralPath singularPath, out string singularReason), - "Frenet denominator below 0.20 is rejected by reconstruction"); - Verification.True(singularPath == null && singularReason.Length > 0, "singular reconstruction has no path"); - - LateralPlanningInput curvatureInput = CreateGeometryInput(TravelDirection.Forward, 0d, stations, 0d, 0d, 1d); - LateralCandidate excessiveCurvature = LateralCandidate.Integrate(stations, 0d, 0d, 2d, new[] { 0d, 0d }); - Verification.True(evaluator.TryEvaluate(curvatureInput, excessiveCurvature, out LateralPath excessivePath, - out string excessiveReason), "curvature reconstruction remains geometric: " + excessiveReason); - Verification.True(!validator.TryValidate(curvatureInput, excessiveCurvature, excessivePath, - out LateralPath rejectedCurvature, out string curvatureReason), "curvature above vehicle limit is rejected"); - Verification.True(rejectedCurvature == null && curvatureReason.Length > 0, "curvature failure has no validated path"); - - LateralCandidate valid = LateralCandidate.Integrate(stations, 0d, 0d, 0d, new[] { 0d, 0d }); - Verification.True(evaluator.TryEvaluate(curvatureInput, valid, out LateralPath rawPath, out string rawReason), - "valid path reconstructs: " + rawReason); - var tamperedPoints = new List(rawPath.Points); - LateralPathPoint original = tamperedPoints[1]; - tamperedPoints[1] = new LateralPathPoint(original.ReferenceS, original.PathS, original.L, original.DL, - original.DDL, original.DDDL, original.X + 0.01d, original.Y, original.VehicleYaw, - original.GeometricCurvature, original.VehicleCurvature, original.VehicleCurvatureDerivative); - Verification.True(!validator.TryValidate(curvatureInput, valid, new LateralPath(tamperedPoints, false), - out LateralPath rejectedTampered, out string tamperedReason), - "validator independently rejects a world-coordinate mismatch"); - Verification.True(rejectedTampered == null && tamperedReason.Length > 0, "tampered path has no validated copy"); - - ExpectArgumentException(() => new LateralCandidate(stations, new[] { double.NaN, 0d, 0d }, - new[] { 0d, 0d, 0d }, new[] { 0d, 0d, 0d }, new[] { 0d, 0d }), "non-finite lateral values are rejected"); - ExpectArgumentException(() => new LateralPathPoint(0d, 0d, 0d, 0d, 0d, 0d, double.NaN, 0d, 0d, 0d, 0d, 0d), - "non-finite world geometry is rejected"); - } - - private static void VerifyGeometryForDirection(TravelDirection direction, double referenceCurvature, - LateralCandidate candidate, - LateralGeometryEvaluator evaluator, LateralSolutionValidator validator) - { - LateralPlanningInput input = CreateGeometryInput(direction, referenceCurvature, candidate.ReferenceStations, - candidate.L[0], candidate.DL[0], 10d); - Verification.True(evaluator.TryEvaluate(input, candidate, out LateralPath rawPath, out string evaluationReason), - direction + " geometry reconstructs: " + evaluationReason); - Verification.True(!rawPath.IsIndependentlyValidated, direction + " evaluator does not self-validate"); - Verification.True(validator.TryValidate(input, candidate, rawPath, out LateralPath validatedPath, - out string validationReason), direction + " geometry validates: " + validationReason); - Verification.True(validatedPath.IsIndependentlyValidated, direction + " validation creates a marked immutable path"); - - double directionSign = direction == TravelDirection.Forward ? 1d : -1d; - Verification.NearlyEqual(0d, rawPath.Points[0].PathS, direction + " PathS starts at zero"); - for (int index = 0; index < rawPath.Points.Count; index++) - { - LateralPathPoint point = rawPath.Points[index]; - FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment, point.ReferenceS); - double denominator = 1d - reference.GeometricCurvature * point.L; - double expectedX = reference.X - point.L * Math.Sin(reference.TravelYaw); - double expectedY = reference.Y + point.L * Math.Cos(reference.TravelYaw); - double expectedTravelYaw = reference.TravelYaw + Math.Atan2(point.DL, denominator); - double expectedVehicleYaw = direction == TravelDirection.Forward - ? AngleMath.NormalizeRadians(expectedTravelYaw) - : AngleMath.NormalizeRadians(expectedTravelYaw + Math.PI); - double expectedGeometricCurvature = CalculateGeometricCurvature(reference, point.L, point.DL, point.DDL, - directionSign * reference.VehicleCurvatureDerivative); - Verification.NearlyEqual(expectedX, point.X, direction + " world x " + index); - Verification.NearlyEqual(expectedY, point.Y, direction + " world y " + index); - Verification.NearlyEqual(expectedVehicleYaw, point.VehicleYaw, direction + " vehicle yaw " + index); - Verification.NearlyEqual(expectedGeometricCurvature, point.GeometricCurvature, - direction + " full Frenet curvature " + index); - Verification.NearlyEqual(directionSign * point.GeometricCurvature, point.VehicleCurvature, - direction + " vehicle curvature sign " + index); - if (index > 0) - { - LateralPathPoint previous = rawPath.Points[index - 1]; - double chord = Math.Sqrt((point.X - previous.X) * (point.X - previous.X) + - (point.Y - previous.Y) * (point.Y - previous.Y)); - Verification.NearlyEqual(previous.PathS + chord, point.PathS, direction + " actual chord PathS " + index); - Verification.True(point.PathS > previous.PathS, direction + " PathS strictly increases " + index); - } - } - - LateralPathPoint check = rawPath.Points[1]; - double signedSpeed = directionSign * 0.3d; - var trajectoryPoint = new EmTrajectoryPoint(check.X, check.Y, check.VehicleYaw, signedSpeed, 0d, - check.VehicleCurvature, 0, check.ReferenceS, check.PathS, direction, EmBoundaryType.None, 0d, 0d); - Verification.NearlyEqual(signedSpeed * check.VehicleCurvature, trajectoryPoint.YawRate, - direction + " yaw-rate identity"); - Verification.NearlyEqual(0.3d * check.GeometricCurvature, trajectoryPoint.YawRate, - direction + " travel curvature yaw-rate identity"); - } - - private static DirectionSegmentView CreateStraightSegment(double referenceCurvatureDerivative = 0d, - double referenceCurvature = 0d) - { - var points = new List - { - Point(0d, 0d, referenceCurvatureDerivative, referenceCurvature), - Point(1d, 1d, referenceCurvatureDerivative, referenceCurvature), - Point(2d, 2d, referenceCurvatureDerivative, referenceCurvature), - }; - return new DirectionSegmentView(0, TravelDirection.Forward, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, 2d, EmBoundaryType.Goal, 2d), 0d); - } - - private static SmoothedPathPoint Point(double x, double s, double curvatureDerivative = 0d, double curvature = 0d) - { - return new SmoothedPathPoint(x, 0d, 0d, 0d, s, TravelDirection.Forward, curvature, curvature, - curvatureDerivative, 1d, - false, SmoothedPathPointSource.Anchor); - } - - private static EmPlannerConfiguration CreateUnitScaleConfiguration() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Corridor.MaximumLateralOffsetMeters = 1d; - configuration.Lateral.MaximumLateralSlope = 1d; - configuration.Lateral.MaximumLateralSecondDerivativePerMeter = 1d; - configuration.Lateral.MaximumLateralThirdDerivativePerSquareMeter = 1d; - return configuration; - } - - private static LateralPlanningInput CreateModelInput(EmTerminalType terminalType, EmPlannerConfiguration configuration, - IReadOnlyList previousL, double referenceCurvatureDerivative = 0d, double referenceCurvature = 0d, - double startL = 0d, double corridorMinimum = -1d, double corridorMaximum = 1d, - double maximumVehicleCurvature = 1d) - { - DirectionSegmentView segment = CreateStraightSegment(referenceCurvatureDerivative, referenceCurvature); - double corridorSeed = Math.Max(corridorMinimum, Math.Min(corridorMaximum, 0d)); - var stations = new[] - { - new LateralInterval(0d, corridorMinimum, corridorMaximum, startL), - new LateralInterval(1d, corridorMinimum, corridorMaximum, corridorSeed), - new LateralInterval(2d, corridorMinimum, corridorMaximum, corridorSeed), - }; - var seed = new List(); - for (int index = 0; index < previousL.Count; index++) - seed.Add(new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, index), previousL[index], 0d, 0d)); - return new LateralPlanningInput(segment, new StaticCorridor(stations), - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), startL, 0d, 0d), terminalType, - CreateVehicle(maximumVehicleCurvature), configuration, seed); - } - - private static LateralPlanningInput CreateGeometryInput(TravelDirection direction, double referenceCurvature, - IReadOnlyList stations, double startL, double startDL, double maximumVehicleCurvature) - { - var points = new List(stations.Count); - for (int index = 0; index < stations.Count; index++) - { - double s = stations[index]; - double travelYaw = referenceCurvature * s; - double x = Math.Abs(referenceCurvature) <= 1e-12d ? s : Math.Sin(travelYaw) / referenceCurvature; - double y = Math.Abs(referenceCurvature) <= 1e-12d ? 0d : (1d - Math.Cos(travelYaw)) / referenceCurvature; - double vehicleYaw = direction == TravelDirection.Forward ? travelYaw : travelYaw - Math.PI; - points.Add(new SmoothedPathPoint(x, y, AngleMath.NormalizeRadians(vehicleYaw), vehicleYaw, s, direction, - referenceCurvature, direction == TravelDirection.Forward ? referenceCurvature : -referenceCurvature, - 0d, 1d, false, SmoothedPathPointSource.Anchor)); - } - double end = stations[stations.Count - 1]; - var segment = new DirectionSegmentView(0, direction, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, end, EmBoundaryType.RollingSafetyStop, end), 0d); - var corridorStations = new List(stations.Count); - for (int index = 0; index < stations.Count; index++) - corridorStations.Add(new LateralInterval(stations[index], -1d, 1d, startL)); - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - configuration.Lateral.MaximumLateralSlope = 5d; - configuration.Lateral.MaximumLateralSecondDerivativePerMeter = 5d; - configuration.Lateral.MaximumLateralThirdDerivativePerSquareMeter = 5d; - double startDenominator = 1d - referenceCurvature * startL; - return new LateralPlanningInput(segment, new StaticCorridor(corridorStations), - new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, stations[0]), startL, - Math.Atan2(startDL, startDenominator), 0d), EmTerminalType.RollingSafetyStop, - CreateVehicle(maximumVehicleCurvature), configuration, Array.Empty()); - } - - private static double CalculateGeometricCurvature(FrenetReferencePoint reference, double l, double dl, double ddl, - double referenceCurvatureDerivative) - { - double a = 1d - reference.GeometricCurvature * l; - return (a * a * reference.GeometricCurvature + a * ddl + referenceCurvatureDerivative * l * dl + - 2d * reference.GeometricCurvature * dl * dl) / Math.Pow(a * a + dl * dl, 1.5d); - } - - private static LateralGeometryEvaluator CreateGeometryEvaluator() - { - return new LateralGeometryEvaluator(); - } - - private static LateralSolutionValidator CreateGeometryValidator() - { - return new LateralSolutionValidator(); - } - - private static LateralObjectiveBuilder CreateObjectiveBuilder() - { - return new LateralObjectiveBuilder(); - } - - private static LateralConstraintBuilder CreateConstraintBuilder() - { - return new LateralConstraintBuilder(CreateObjectiveBuilder()); - } - - private static double MatrixValue(SparseCscMatrix matrix, int row, int column) - { - for (int index = matrix.ColumnPointers[column]; index < matrix.ColumnPointers[column + 1]; index++) - { - if (matrix.RowIndices[index] == row) - return matrix.Values[index]; - } - return 0d; - } - - private static int CountExactEqualityRows(QuadraticProgram problem, IReadOnlyDictionary expected) - { - int count = 0; - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (Math.Abs(problem.LowerBounds[row] - problem.UpperBounds[row]) <= 1e-12d && - RowMatches(problem.ConstraintMatrix, row, expected)) - { - count++; - } - } - return count; - } - - private static bool HasFiniteNonEqualityBound(QuadraticProgram problem, int variable) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (Math.Abs(problem.LowerBounds[row] - problem.UpperBounds[row]) > 1e-12d && - RowMatches(problem.ConstraintMatrix, row, new Dictionary { { variable, 1d } }) && - !double.IsInfinity(problem.LowerBounds[row]) && !double.IsInfinity(problem.UpperBounds[row])) - { - return true; - } - } - return false; - } - - private static bool HasBoundWithUpper(QuadraticProgram problem, int variable, double upper) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (RowMatches(problem.ConstraintMatrix, row, new Dictionary { { variable, 1d } }) && - Math.Abs(problem.UpperBounds[row] - upper) <= 1e-12d) - { - return true; - } - } - return false; - } - - private static bool HasBound(QuadraticProgram problem, IReadOnlyDictionary expected, - double lower, double upper) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (RowMatches(problem.ConstraintMatrix, row, expected) && - Math.Abs(problem.LowerBounds[row] - lower) <= 1e-12d && - Math.Abs(problem.UpperBounds[row] - upper) <= 1e-12d) - { - return true; - } - } - return false; - } - - private static bool RowMatches(SparseCscMatrix matrix, int targetRow, IReadOnlyDictionary expected) - { - var actual = new Dictionary(); - for (int column = 0; column < matrix.ColumnCount; column++) - { - for (int index = matrix.ColumnPointers[column]; index < matrix.ColumnPointers[column + 1]; index++) - { - if (matrix.RowIndices[index] == targetRow) - actual[column] = matrix.Values[index]; - } - } - if (actual.Count != expected.Count) - return false; - foreach (KeyValuePair expectedEntry in expected) - { - if (!actual.TryGetValue(expectedEntry.Key, out double value) || Math.Abs(value - expectedEntry.Value) > 1e-12d) - return false; - } - return true; - } - - private static VehicleParameters CreateVehicle(double maximumCurvature = 1d) - { - return new VehicleParameters - { - LengthMeters = 0.1d, - WidthMeters = 0.1d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = maximumCurvature, - }; - } - - private static LateralPathPoint CreatePathPoint(double referenceS) - { - return new LateralPathPoint(referenceS, referenceS, 0d, 0d, 0d, 0d, referenceS, 0d, 0d, 0d, 0d, 0d); - } - - private static LateralVariableLayout CreateLayout(int stationCount) - { - return new LateralVariableLayout(stationCount); - } - - private static void ExpectArgumentException(Action action, string name) - { - try - { - action(); - } - catch (ArgumentException) - { - return; - } - throw new InvalidOperationException(name + " did not throw ArgumentException."); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs deleted file mode 100644 index 889076c..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs +++ /dev/null @@ -1,829 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class LongitudinalIntegrationChecks -{ - public static void Run() - { - VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed(); - VerifiesFullDirectionScheduleIsIndependentFromPublicationCadence(); - VerifiesFullDirectionStaticStartMakesProgress(); - VerifiesFullDirectionNearZeroSpeedUsesStaticStartSeed(); - VerifiesFullDirectionCurvedNearZeroStartUsesStaticStartSeed(); - VerifiesFullDirectionNearZeroScheduleDurationIsReasonable(); - VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold(); - VerifiesExactStopIncludesAStabilizationTail(); - VerifiesLastStrictCandidateSurvivesLaterTimeout(); - VerifiesInvalidAndInaccurateCandidatesNeverBecomeFallbacks(); - VerifiesEnvelopeLinearizationAdvancesAfterStrictRejection(); - VerifiesExactStopTailDoesNotDistortEnvelope(); - VerifiesValidatedEndpointsAreCanonical(); - VerifiesWarmStartAndFiveIterationLimit(); - VerifiesNonzeroSpeedSeedIsStrictlyFeasible(); - VerifiesCancellationInfeasibilityAndPlannerDelegation(); - RunRealOsqpInCleanPluginBundle(); - } - - private static void VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.DistanceHorizonMeters = 5d; - configuration.Scheduling.TimeHorizonSeconds = 2d; - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.20d; - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, 2.5d, 0d), - Point(2d, 5d, 0d), - }, true); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.10d, 0d, - EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, configuration, - Array.Empty(), Array.Empty()); - var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d)); - - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "rolling seed remains a strict timeout fallback: " + result.FailureReason); - LongitudinalCandidate candidate = result.Candidate ?? - throw new InvalidOperationException("Rolling timeout fallback candidate was missing."); - Verification.Equal(21, candidate.KnotTimes.Count, "two-second ST emits twenty-one knots"); - Verification.True(candidate.S[candidate.S.Count - 1] < input.PathUpperBoundS, - "two-second ST does not consume the five-metre LS window"); - Verification.True(candidate.U[candidate.U.Count - 1] > 0.01d, - "rolling ST keeps nonzero terminal speed"); - } - - private static void VerifiesFullDirectionScheduleIsIndependentFromPublicationCadence() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.TimeHorizonSeconds = 10d; - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Scheduling.MaximumOptimizationTimeStepSeconds = 0.20d; - configuration.Scheduling.MaximumOptimizationSpatialStepMeters = 0.10d; - configuration.Scheduling.MaximumOptimizationKnotCount = 401; - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, 1d, 0d), - Point(2d, 2d, 0d), - }, true); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.10d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "full schedule envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.10d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule coarsePublication, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "full schedule with 0.10 s publication: " + failureReason); - - EmPlannerConfiguration densePublicationConfiguration = configuration.Copy(); - densePublicationConfiguration.Scheduling.OutputTimeStepSeconds = 0.05d; - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.10d, 0d, - densePublicationConfiguration.Longitudinal.DesiredForwardSpeedMetersPerSecond, densePublicationConfiguration, - out LongitudinalKnotSchedule densePublication, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "full schedule with 0.05 s publication: " + failureReason); - Verification.Equal(coarsePublication.KnotTimes.Count, densePublication.KnotTimes.Count, - "publication cadence does not determine full-segment optimization knot count"); - - var publicationCandidate = new LongitudinalCandidate(new[] { 0d, 0.50d, 1d }, - new[] { 0d, 1d / 60d, 1d / 60d }, new[] { 0.10d, 0d, 0d }, new[] { -0.40d, 0d, 0d }, - new[] { 0.80d, 0d }); - var publicationResult = new LongitudinalPlanningResult(EmPlanningStatus.Success, publicationCandidate, string.Empty); - DateTimeOffset now = DateTimeOffset.UtcNow; - var metadata = new EmTrajectoryMetadata("publication-cadence", now, now, 1L, "publication-path", 1L, - string.Empty, 0, TravelDirection.Forward, EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, - EmPlanningScope.FullDirectionSegment); - configuration.Longitudinal.ZeroSpeedHoldSeconds = 0d; - densePublicationConfiguration.Longitudinal.ZeroSpeedHoldSeconds = 0d; - EmTrajectory coarseTrajectory = new EmTrajectoryAssembler(configuration).Assemble(path, publicationResult, metadata); - EmTrajectory denseTrajectory = new EmTrajectoryAssembler(densePublicationConfiguration).Assemble(path, - publicationResult, metadata); - Verification.Equal(2 * (coarseTrajectory.Points.Count - 1), denseTrajectory.Points.Count - 1, - "halving publication cadence doubles emitted trajectory intervals without changing optimization knots"); - } - - private static void VerifiesFullDirectionStaticStartMakesProgress() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 1d; - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(2.5d, 2.5d, 0d), - Point(5d, 5d, 0d), - }, true); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "static-start envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule schedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "static-start schedule: " + failureReason); - - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, schedule, Array.Empty(), Array.Empty()); - LongitudinalPlanningResult result = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(input, - CancellationToken.None); - - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - "static start succeeds: " + result.FailureReason); - LongitudinalCandidate candidate = result.Candidate ?? - throw new InvalidOperationException("static start must expose a successful candidate."); - Verification.True(HasValueGreaterThan(candidate.S, 0.05d), "static start makes measurable progress"); - Verification.True(HasValueGreaterThan(candidate.U, 0.05d), "static start accelerates"); - Verification.NearlyEqual(0d, candidate.U[candidate.U.Count - 1], - "full segment stops at the terminal boundary"); - } - - private static void VerifiesFullDirectionNearZeroSpeedUsesStaticStartSeed() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.SolverTimeoutSeconds = 5d; - configuration.Solver.MaximumOsqpIterations = 100000; - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(2.5d, 2.5d, 0d), - Point(5d, 5d, 0d), - }, true); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.01d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "near-zero envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.01d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule schedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "near-zero schedule: " + failureReason); - - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.01d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, schedule, Array.Empty(), Array.Empty()); - LongitudinalPlanningResult result = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(input, - CancellationToken.None); - - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - "near-zero full-direction start succeeds: " + result.FailureReason); - LongitudinalCandidate candidate = result.Candidate ?? - throw new InvalidOperationException("Near-zero full-direction candidate was missing."); - Verification.NearlyEqual(path.Points[path.Points.Count - 1].PathS, candidate.S[candidate.S.Count - 1], - "near-zero start reaches the terminal boundary"); - Verification.NearlyEqual(0d, candidate.U[candidate.U.Count - 1], - "near-zero start stops at the terminal boundary"); - } - - private static void VerifiesFullDirectionCurvedNearZeroStartUsesStaticStartSeed() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.SolverTimeoutSeconds = 5d; - configuration.Solver.MaximumOsqpIterations = 100000; - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, 2.25d, 0.1d), - Point(2d, 4.5d, 0d), - }, true); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.0001d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "curved near-zero envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.0001d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule schedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "curved near-zero schedule: " + failureReason); - - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.0001d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, schedule, Array.Empty(), Array.Empty()); - LongitudinalPlanningResult result = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(input, - CancellationToken.None); - - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - "curved near-zero full-direction start succeeds: " + result.FailureReason); - LongitudinalCandidate candidate = result.Candidate ?? - throw new InvalidOperationException("Curved near-zero full-direction candidate was missing."); - Verification.NearlyEqual(path.Points[path.Points.Count - 1].PathS, candidate.S[candidate.S.Count - 1], - "curved near-zero start reaches the terminal boundary"); - Verification.NearlyEqual(0d, candidate.U[candidate.U.Count - 1], - "curved near-zero start stops at the terminal boundary"); - } - - private static void VerifiesFullDirectionNearZeroScheduleDurationIsReasonable() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, 2.25d, 0.1d), - Point(2d, 4.5d, 0d), - }, true); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.0001d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "near-zero duration envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.0001d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule schedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "near-zero duration schedule: " + failureReason); - - double duration = schedule.KnotTimes[schedule.KnotTimes.Count - 1]; - Verification.True(duration < 120d, - "near-zero schedule duration is normal: " + duration.ToString("R", System.Globalization.CultureInfo.InvariantCulture)); - } - - private static void VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Longitudinal.ZeroSpeedHoldSeconds = 0.20d; - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, 0.0075d, 0d), - }, true); - var candidate = new LongitudinalCandidate(new[] { 0d, 0.10d, 0.20d, 0.40d }, - new[] { 0d, 0.005d, 0.0075d, 0.0075d }, new[] { 0.05d, 0.025d, 0d, 0d }, - new[] { 0d, -0.5d, 0d, 0d }, new[] { -5d, 5d, 0d }); - var result = new LongitudinalPlanningResult(EmPlanningStatus.Success, candidate, string.Empty); - DateTimeOffset now = DateTimeOffset.UtcNow; - var metadata = new EmTrajectoryMetadata("full-hold", now, now, 1L, "hold-path", 1L, string.Empty, 0, - TravelDirection.Forward, EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, - EmPlanningScope.FullDirectionSegment); - - EmTrajectory trajectory = new EmTrajectoryAssembler(configuration).Assemble(path, result, metadata); - - Verification.NearlyEqual(0.40d, trajectory.Points[trajectory.Points.Count - 1].TimeFromStart, - "full-scope publication reuses its candidate hold instead of appending a second hold"); - Verification.Equal(3, CountStationaryTerminalPoints(trajectory), - "full-scope publication emits the candidate's single nonzero terminal hold"); - } - - private static void VerifiesExactStopIncludesAStabilizationTail() - { - EmPlannerConfiguration configuration = CreateExactStopSeedConfiguration(); - LateralPath path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, 0.00375d, 0d), - Point(2d, 0.0075d, 0d), - }, true); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.05d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - Array.Empty(), Array.Empty()); - var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d)); - - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - - IReadOnlyList times = LongitudinalCandidate.CreateKnotTimes( - input.Configuration.Scheduling.TimeHorizonSeconds, input.Configuration.Scheduling.OutputTimeStepSeconds); - LongitudinalCandidate seed = FromPrimal(times, solver.WarmStarts[0]); - EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "exact-stop seed envelope: " + speedFailure); - Verification.True(new LongitudinalSolutionValidator().TryValidate(input, envelope, seed, out _, - out string validationFailure), "exact-stop seed is strictly feasible: " + validationFailure); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "exact-stop seed remains a strict timeout fallback"); - LongitudinalCandidate candidate = result.Candidate ?? - throw new InvalidOperationException("Exact-stop timeout fallback candidate was missing."); - int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - candidate.KnotTimes, input.Configuration.Scheduling.OutputTimeStepSeconds); - for (int index = stabilizationStart; index < candidate.S.Count; index++) - { - Verification.NearlyEqual(input.StopBoundaryPathS, candidate.S[index], "stop-tail S " + index); - Verification.NearlyEqual(0d, candidate.U[index], "stop-tail U " + index); - Verification.NearlyEqual(0d, candidate.A[index], "stop-tail A " + index); - } - } - - private static int CountStationaryTerminalPoints(EmTrajectory trajectory) - { - double terminalPathS = trajectory.Points[trajectory.Points.Count - 1].PathS; - int count = 0; - for (int index = 0; index < trajectory.Points.Count; index++) - { - EmTrajectoryPoint point = trajectory.Points[index]; - if (Math.Abs(point.PathS - terminalPathS) <= 1e-12d && - Math.Abs(point.SignedLongitudinalVelocity) <= 1e-12d) - { - count++; - } - } - return count; - } - - private static bool HasValueGreaterThan(IReadOnlyList values, double threshold) - { - for (int index = 0; index < values.Count; index++) - { - if (values[index] > threshold) - return true; - } - return false; - } - - public static void RunRealOsqp() - { - foreach (LongitudinalScenario scenario in CreateRealOsqpScenarios()) - { - LongitudinalPlanningResult first = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(scenario.Input, - CancellationToken.None); - LongitudinalPlanningResult second = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(scenario.Input, - CancellationToken.None); - VerifyRealScenario(scenario, first); - VerifyRealScenario(scenario, second); - VerifyDeterministicResult(scenario.Name, first, second); - } - } - - private static void RunRealOsqpInCleanPluginBundle() - { - string pluginDirectory = Path.Combine(Path.GetTempPath(), "em-planner-longitudinal-real-" + Guid.NewGuid().ToString("N")); - try - { - Directory.CreateDirectory(pluginDirectory); - foreach (string sourcePath in Directory.GetFiles(AppContext.BaseDirectory)) - File.Copy(sourcePath, Path.Combine(pluginDirectory, Path.GetFileName(sourcePath)), false); - string nativeSource = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", - "ThirdParty", "OSQP", "win-x64", "osqp.dll")); - Verification.True(File.Exists(nativeSource), "pinned OSQP DLL is available for the real longitudinal bundle"); - File.Copy(nativeSource, Path.Combine(pluginDirectory, "osqp.dll"), true); - - var startInfo = new ProcessStartInfo - { - FileName = Path.Combine(pluginDirectory, "EMPlannerVerificationHost.exe"), - Arguments = "longitudinal-real-osqp-probe", - WorkingDirectory = pluginDirectory, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - using (var process = new Process { StartInfo = startInfo }) - { - process.Start(); - string standardOutput = process.StandardOutput.ReadToEnd(); - string standardError = process.StandardError.ReadToEnd(); - process.WaitForExit(); - if (process.ExitCode != 0 || standardOutput.IndexOf("PASS longitudinal-real-osqp", StringComparison.Ordinal) < 0) - { - throw new InvalidOperationException("Real longitudinal OSQP clean-plugin probe exited " + process.ExitCode + ": " + - standardError + standardOutput); - } - } - } - finally - { - if (Directory.Exists(pluginDirectory)) - Directory.Delete(pluginDirectory, true); - } - } - - private static void VerifiesLastStrictCandidateSurvivesLaterTimeout() - { - LongitudinalPlanningInput input = CreateFakeInput(out LongitudinalCandidate valid); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, ToPrimal(valid), 10d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 10d), - }); - - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "timeout after strict candidate returns fallback success"); - LongitudinalCandidate fallback = result.Candidate ?? throw new InvalidOperationException("Fallback candidate was missing."); - Verification.NearlyEqual(valid.S[1], fallback.S[1], "last strict candidate remains the fallback"); - } - - private static void VerifiesInvalidAndInaccurateCandidatesNeverBecomeFallbacks() - { - LongitudinalPlanningInput input = CreateFakeInput(out LongitudinalCandidate valid); - double[] invalid = ToPrimal(valid); - var layout = new LongitudinalVariableLayout(valid.KnotTimes.Count); - invalid[layout.U(1)] = 10d; - var invalidSolver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, invalid, 1d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d), - }); - LongitudinalPlanningResult invalidResult = new SequentialLongitudinalOptimizer(invalidSolver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, invalidResult.Status, - "invalid solver vector cannot replace the strict initial fallback"); - Verification.NearlyEqual(valid.U[1], invalidResult.Candidate?.U[1] ?? double.NaN, - "invalid solver vector does not become the fallback candidate"); - - var inaccurateSolver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.SolvedInaccurate, ToPrimal(valid), 1d, 2e-5d, 0d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d), - }); - LongitudinalPlanningResult inaccurateResult = new SequentialLongitudinalOptimizer(inaccurateSolver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, inaccurateResult.Status, - "inaccurate residual candidate cannot replace the strict initial fallback"); - Verification.NearlyEqual(valid.U[1], inaccurateResult.Candidate?.U[1] ?? double.NaN, - "inaccurate residual does not become the fallback candidate"); - double[] inaccuratePrimal = ToPrimal(valid); - for (int index = 0; index < inaccuratePrimal.Length; index++) - { - Verification.NearlyEqual(inaccuratePrimal[index], inaccurateSolver.WarmStarts[1][index], - "inaccurate finite primal only warms the next ST QP " + index); - } - } - - private static void VerifiesWarmStartAndFiveIterationLimit() - { - LongitudinalPlanningInput input = CreateFakeInput(out LongitudinalCandidate valid); - var firstSolveOnly = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d)); - new SequentialLongitudinalOptimizer(firstSolveOnly).Optimize(input, CancellationToken.None); - Verification.Equal(true, firstSolveOnly.LastSettings != null && firstSolveOnly.LastSettings.EnableWarmStart, - "first ST solve enables a dynamics-consistent native warm start"); - Verification.True(FromPrimal(valid.KnotTimes, firstSolveOnly.WarmStarts[0]).SatisfiesExactDiscreteDynamics(1e-12d), - "first ST warm start satisfies exact constant-jerk dynamics"); - - var results = new List(); - for (int index = 0; index < 5; index++) - results.Add(Result(QpSolveStatus.Solved, ToPrimal(valid), 100d - 10d * index)); - var solver = new FakeQpSolver(results); - - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.Success, result.Status, "five solved iterations publish success"); - Verification.Equal(5, solver.SolveCallCount, "ST has a hard five-envelope-iteration maximum"); - Verification.Equal(new LongitudinalVariableLayout(valid.KnotTimes.Count).VariableCount, solver.WarmStarts[0].Count, - "first ST linearization seed remains a complete primal vector"); - Verification.Equal(true, solver.LastSettings != null && solver.LastSettings.EnableWarmStart, - "later ST solves enable native warm start"); - for (int index = 0; index < solver.WarmStarts[1].Count; index++) - Verification.NearlyEqual(ToPrimal(valid)[index], solver.WarmStarts[1][index], "strict candidate warms the next QP " + index); - } - - private static void VerifiesEnvelopeLinearizationAdvancesAfterStrictRejection() - { - LongitudinalPlanningInput baseline = CreateFakeInput(out LongitudinalCandidate candidate); - var curvedPath = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, candidate.S[1], 10000d), - Point(2d, baseline.PathUpperBoundS, 0d), - }, true); - var input = new LongitudinalPlanningInput(curvedPath, TravelDirection.Forward, - baseline.InitialProgressSpeedMetersPerSecond, baseline.InitialAccelerationMetersPerSecondSquared, - baseline.TerminalType, baseline.Mode, baseline.Configuration, Array.Empty(), Array.Empty()); - EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "curved-envelope setup: " + speedFailure); - Verification.True(candidate.U[1] > envelope.MaximumSpeedAt(candidate.S[1]), - "scripted candidate violates its own curvature speed envelope"); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, ToPrimal(candidate), 2d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 2d), - }); - - new SequentialLongitudinalOptimizer(solver).Optimize(input, CancellationToken.None); - Verification.Equal(2, solver.SolveCallCount, "rejected candidate reaches the next envelope iteration"); - var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count); - FindSingleVariableBounds(solver.Problems[1], layout.U(1), out _, out double secondUpper); - Verification.True(Math.Abs(secondUpper - envelope.MaximumSpeedAt(candidate.S[1])) > 1e-12d, - "scripted rejection advances the ST PathS envelope probe"); - } - - private static void VerifiesExactStopTailDoesNotDistortEnvelope() - { - LongitudinalPlanningInput baseline = CreateFakeInput(out LongitudinalCandidate valid); - double[] perturbedProgress = new double[valid.S.Count]; - for (int index = 0; index < perturbedProgress.Length; index++) - perturbedProgress[index] = valid.S[index]; - perturbedProgress[perturbedProgress.Length - 1] += 0.05d; - var toleranceCandidate = new LongitudinalCandidate(valid.KnotTimes, perturbedProgress, valid.U, valid.A, valid.J); - var curvedPath = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, valid.S[1], 10000d), - Point(2d, baseline.PathUpperBoundS, 0d), - }, true); - var input = new LongitudinalPlanningInput(curvedPath, TravelDirection.Forward, - baseline.InitialProgressSpeedMetersPerSecond, baseline.InitialAccelerationMetersPerSecondSquared, - baseline.TerminalType, baseline.Mode, baseline.Configuration, Array.Empty(), Array.Empty()); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, ToPrimal(toleranceCandidate), 2d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 2d), - }); - - new SequentialLongitudinalOptimizer(solver).Optimize(input, CancellationToken.None); - Verification.Equal(2, solver.SolveCallCount, "rejected terminal PathS still reaches a new envelope iteration"); - var layout = new LongitudinalVariableLayout(valid.KnotTimes.Count); - FindSingleVariableBounds(solver.Problems[0], layout.U(1), out _, out double firstUpper); - FindSingleVariableBounds(solver.Problems[1], layout.U(1), out _, out double secondUpper); - Verification.NearlyEqual(firstUpper, secondUpper, - "exact stop-tail perturbations do not distort a moving-knot envelope sample"); - } - - private static void VerifiesCancellationInfeasibilityAndPlannerDelegation() - { - LongitudinalPlanningInput input = CreateFakeInput(out LongitudinalCandidate valid); - using (var cancellation = new CancellationTokenSource()) - { - cancellation.Cancel(); - var cancellationSolver = new FakeQpSolver(Result(QpSolveStatus.Solved, ToPrimal(valid), 1d)); - LongitudinalPlanningResult cancelled = new SequentialLongitudinalOptimizer(cancellationSolver).Optimize(input, - cancellation.Token); - Verification.Equal(EmPlanningStatus.Cancelled, cancelled.Status, "cancellation before QP solve"); - Verification.Equal(0, cancellationSolver.SolveCallCount, "cancelled ST does not call the QP solver"); - } - - using (var cancellation = new CancellationTokenSource()) - { - var solver = new CancellingQpSolver( - new FakeQpSolver(Result(QpSolveStatus.Solved, ToPrimal(valid), 1d)), cancellation); - LongitudinalPlanningResult cancelledAfterStrictCandidate = new SequentialLongitudinalOptimizer(solver).Optimize( - input, cancellation.Token); - Verification.Equal(EmPlanningStatus.Cancelled, cancelledAfterStrictCandidate.Status, - "cancellation after a strict ST candidate is never fallback success"); - Verification.True(cancelledAfterStrictCandidate.Candidate == null, - "cancelled ST result exposes no candidate"); - } - - var infeasibleSolver = new FakeQpSolver(Result(QpSolveStatus.PrimalInfeasible, Array.Empty(), 1d)); - LongitudinalPlanningResult infeasible = new SequentialLongitudinalOptimizer(infeasibleSolver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, infeasible.Status, - "QP infeasibility preserves the strict mode-specific seed"); - Verification.True(infeasible.Candidate != null, "QP infeasibility retains a safe fallback profile"); - - var plannerSolver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, ToPrimal(valid), 2d), - Result(QpSolveStatus.Solved, ToPrimal(valid), 1d), - Result(QpSolveStatus.Solved, ToPrimal(valid), 1d), - }); - LongitudinalPlanningResult delegated = new LongitudinalPlanner(plannerSolver).Plan(input, CancellationToken.None); - Verification.Equal(EmPlanningStatus.Success, delegated.Status, "LongitudinalPlanner delegates to the ST optimizer"); - } - - private static void VerifiesNonzeroSpeedSeedIsStrictlyFeasible() - { - LongitudinalPlanningInput input = CreateFakeInput(out _); - var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty(), 1d)); - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - IReadOnlyList times = LongitudinalCandidate.CreateKnotTimes( - input.Configuration.Scheduling.TimeHorizonSeconds, input.Configuration.Scheduling.OutputTimeStepSeconds); - Verification.True(solver.WarmStarts.Count > 0, - "nonzero-speed seed reaches the ST solver: " + result.Status + " " + result.FailureReason); - LongitudinalCandidate seed = FromPrimal(times, solver.WarmStarts[0]); - EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "nonzero-speed seed envelope: " + speedFailure); - Verification.True(new LongitudinalSolutionValidator().TryValidate(input, envelope, seed, out _, - out string validationFailure), "nonzero-speed seed is strictly feasible: " + validationFailure); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "strictly validated initial seed survives an immediate solver timeout"); - Verification.True(result.Candidate != null, - "strictly validated initial seed is retained as the timeout fallback"); - } - - private static void VerifiesValidatedEndpointsAreCanonical() - { - LongitudinalPlanningInput input = CreateFakeInput(out LongitudinalCandidate valid); - double toleranceOffset = 0.5d * input.Configuration.Validation.KinematicTolerance; - double[] progress = new double[valid.S.Count]; - double[] speed = new double[valid.U.Count]; - for (int index = 0; index < progress.Length; index++) - { - progress[index] = valid.S[index]; - speed[index] = valid.U[index]; - } - progress[progress.Length - 1] += toleranceOffset; - speed[speed.Length - 1] += toleranceOffset; - var toleranceCandidate = new LongitudinalCandidate(valid.KnotTimes, progress, speed, valid.A, valid.J); - var solver = new FakeQpSolver(new[] - { - Result(QpSolveStatus.Solved, ToPrimal(toleranceCandidate), 2d), - Result(QpSolveStatus.TimeLimit, Array.Empty(), 2d), - }); - - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "canonical strict candidate remains the timeout fallback"); - LongitudinalCandidate canonical = result.Candidate ?? - throw new InvalidOperationException("Canonical fallback candidate was missing."); - Verification.Equal(input.StopBoundaryPathS, canonical.S[canonical.S.Count - 1], - "validated terminal PathS is canonicalized exactly"); - Verification.Equal(0d, canonical.U[canonical.U.Count - 1], - "validated terminal speed is canonicalized exactly"); - } - - private static IReadOnlyList CreateRealOsqpScenarios() - { - return new[] - { - CreateRealScenario("forward", TravelDirection.Forward, 0.50d, 0d, 0d, 0d), - CreateRealScenario("reverse", TravelDirection.Reverse, 0.50d, 0d, 0d, 0d), - CreateRealScenario("curvature-limited", TravelDirection.Forward, 0.35d, 20d, 0d, 0d), - CreateRealScenario("jerk-limited-stop", TravelDirection.Forward, 0.50d, 0d, 0.05d, 0d), - CreateRealScenario("short-segment", TravelDirection.Forward, 0.05d, 0d, 0d, 0d), - CreateRealScenario("zero-start-speed", TravelDirection.Forward, 0.50d, 0d, 0d, 0d), - }; - } - - private static LongitudinalScenario CreateRealScenario(string name, TravelDirection direction, double terminalPathS, - double middleCurvature, double initialSpeed, double initialAcceleration) - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Validation.KinematicTolerance = 1e-5d; - var points = new[] - { - Point(0d, 0d, 0d), - Point(1d, terminalPathS * 0.5d, middleCurvature), - Point(2d, terminalPathS, 0d), - }; - return new LongitudinalScenario(name, new LongitudinalPlanningInput(new LateralPath(points, true), direction, - initialSpeed, initialAcceleration, EmTerminalType.Goal, EmLongitudinalMode.ApproachStopBoundary, - configuration, Array.Empty(), Array.Empty())); - } - - private static void VerifyRealScenario(LongitudinalScenario scenario, LongitudinalPlanningResult result) - { - Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, - scenario.Name + " returns a strict profile: " + result.FailureReason); - LongitudinalCandidate candidate = result.Candidate ?? throw new InvalidOperationException(scenario.Name + " candidate missing."); - Verification.True(candidate.S[candidate.S.Count - 1] <= scenario.Input.PathUpperBoundS, - scenario.Name + " remains inside the PathS window"); - PathSpeedLimitBuilder builder = new PathSpeedLimitBuilder(); - EmPlanningStatus speedStatus = builder.Build(scenario.Input, out PathSpeedLimit envelope, out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, scenario.Name + " envelope: " + speedFailure); - Verification.True(new LongitudinalSolutionValidator().TryValidate(scenario.Input, envelope, candidate, - out _, out string validationFailure), scenario.Name + " strict physical validation: " + validationFailure); - } - - private static void VerifyDeterministicResult(string name, LongitudinalPlanningResult first, LongitudinalPlanningResult second) - { - Verification.Equal(first.Status, second.Status, name + " deterministic status"); - LongitudinalCandidate left = first.Candidate ?? throw new InvalidOperationException(name + " first candidate missing."); - LongitudinalCandidate right = second.Candidate ?? throw new InvalidOperationException(name + " second candidate missing."); - for (int index = 0; index < left.S.Count; index++) - { - Verification.NearlyEqual(left.S[index], right.S[index], name + " deterministic S " + index); - Verification.NearlyEqual(left.U[index], right.U[index], name + " deterministic U " + index); - Verification.NearlyEqual(left.A[index], right.A[index], name + " deterministic A " + index); - } - for (int index = 0; index < left.J.Count; index++) - Verification.NearlyEqual(left.J[index], right.J[index], name + " deterministic J " + index); - } - - private static LongitudinalPlanningInput CreateFakeInput(out LongitudinalCandidate valid) - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.TimeHorizonSeconds = 1d; - configuration.Scheduling.OutputTimeStepSeconds = 0.25d; - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 4d; - configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; - IReadOnlyList times = LongitudinalCandidate.CreateKnotTimes(1d, 0.25d); - valid = LongitudinalCandidate.Integrate(times, 0d, 0.10d, 0d, - new[] { -0.8d, 0d, 0.8d, 0d }); - double terminalPathS = valid.S[valid.S.Count - 1]; - var path = new LateralPath(new[] - { - Point(0d, 0d, 0d), - Point(1d, terminalPathS * 0.5d, 0d), - Point(2d, terminalPathS, 0d), - }, true); - return new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.10d, 0d, EmTerminalType.Goal, - EmLongitudinalMode.ExactStopAtBoundary, configuration, Array.Empty(), Array.Empty()); - } - - private static EmPlannerConfiguration CreateExactStopSeedConfiguration() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.TimeHorizonSeconds = 0.40d; - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Scheduling.SolverTimeoutSeconds = 1d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 20d; - configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; - return configuration; - } - - private static LateralPathPoint Point(double referenceS, double pathS, double curvature) - { - return new LateralPathPoint(referenceS, pathS, 0d, 0d, 0d, 0d, pathS, 0d, 0d, curvature, curvature, 0d); - } - - private static QpSolveResult Result(QpSolveStatus status, IReadOnlyList primal, double objective, - double primalResidual = 0d, double dualResidual = 0d) - { - return new QpSolveResult(status, primal, objective, primalResidual, dualResidual, 1, TimeSpan.Zero, - status.ToString(), string.Empty); - } - - private static double[] ToPrimal(LongitudinalCandidate candidate) - { - var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count); - var primal = new double[layout.VariableCount]; - for (int index = 0; index < layout.KnotCount; index++) - { - primal[layout.S(index)] = candidate.S[index]; - primal[layout.U(index)] = candidate.U[index]; - primal[layout.A(index)] = candidate.A[index]; - } - for (int index = 0; index < layout.KnotCount - 1; index++) - primal[layout.J(index)] = candidate.J[index]; - return primal; - } - - private static LongitudinalCandidate FromPrimal(IReadOnlyList times, IReadOnlyList primal) - { - var layout = new LongitudinalVariableLayout(times.Count); - var s = new double[layout.KnotCount]; - var u = new double[layout.KnotCount]; - var a = new double[layout.KnotCount]; - var j = new double[layout.KnotCount - 1]; - for (int index = 0; index < layout.KnotCount; index++) - { - s[index] = primal[layout.S(index)]; - u[index] = primal[layout.U(index)]; - a[index] = primal[layout.A(index)]; - } - for (int index = 0; index < j.Length; index++) - j[index] = primal[layout.J(index)]; - return new LongitudinalCandidate(times, s, u, a, j); - } - - private static void FindSingleVariableBounds(QuadraticProgram problem, int variable, out double lower, out double upper) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - int found = 0; - double coefficient = 0d; - for (int column = 0; column < problem.ConstraintMatrix.ColumnCount; column++) - { - for (int index = problem.ConstraintMatrix.ColumnPointers[column]; - index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) - { - if (problem.ConstraintMatrix.RowIndices[index] == row) - { - found++; - if (column == variable) - coefficient = problem.ConstraintMatrix.Values[index]; - } - } - } - if (found == 1 && Math.Abs(coefficient - 1d) <= 1e-12d) - { - lower = problem.LowerBounds[row]; - upper = problem.UpperBounds[row]; - return; - } - } - throw new InvalidOperationException("No single-variable bounds were found for ST variable " + variable + "."); - } - - private sealed class LongitudinalScenario - { - public LongitudinalScenario(string name, LongitudinalPlanningInput input) - { - Name = name; - Input = input; - } - - public string Name { get; } - - public LongitudinalPlanningInput Input { get; } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs deleted file mode 100644 index 4dce759..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs +++ /dev/null @@ -1,988 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class LongitudinalModelChecks -{ - public static void Run() - { - VerifiesJerkLimitedStoppingProfileEndsAtRest(); - VerifiesStoppedReachabilityUsesTheSameJerkModel(); - VerifiesRollingEnvelopeDoesNotStopAtWindowEnd(); - VerifiesFinitePathSIndexedSpeedEnvelope(); - VerifiesStoppingEnvelopeIsRefinedOnActualPathS(); - VerifiesStoppingEnvelopeUsesJerkLimitedStoppingMath(); - VerifiesStoppingPrecheckOnlyAppliesToRealStopBoundaries(); - VerifiesReferenceHorizonSelectionSeparatesSpaceAndTime(); - VerifiesFullDirectionScopeSelectsActualSegmentBoundary(); - VerifiesFullDirectionScheduleDerivesDurationAndAdaptiveBreakpoints(); - VerifiesFullDirectionInitialFeasibilityProjectionAndFallbackSemantics(); - VerifiesTimeKnotLayoutDynamicsObjectiveAndHardConstraints(); - VerifiesModeSpecificSolutionValidation(); - VerifiesFullDirectionNoProgressValidation(); - VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically(); - } - - private static void VerifiesJerkLimitedStoppingProfileEndsAtRest() - { - Verification.True(JerkLimitedStoppingMath.TryCalculate( - 0.20d, 0d, 0.30d, 0.50d, - out JerkLimitedStoppingProfile profile, out string failure), - "jerk-limited stop builds: " + failure); - Verification.True(profile.DistanceMeters > 0d, "stop distance is positive"); - Verification.True(profile.DurationSeconds > 0d, "stop duration is positive"); - Verification.NearlyEqual(0d, profile.FinalSpeedMetersPerSecond, - "stop ends at zero speed"); - Verification.NearlyEqual(0d, profile.FinalAccelerationMetersPerSecondSquared, - "stop releases acceleration to zero"); - - Verification.True(JerkLimitedStoppingMath.TryCalculate( - 0.20d, 0.20d, 0.30d, 0.50d, - out JerkLimitedStoppingProfile accelerating, out failure), - "positive-acceleration stop builds: " + failure); - Verification.True(accelerating.DistanceMeters > profile.DistanceMeters, - "positive initial acceleration needs more stopping distance"); - Verification.NearlyEqual(0d, accelerating.FinalAccelerationMetersPerSecondSquared, - "positive-acceleration stop also releases acceleration"); - } - - private static void VerifiesStoppedReachabilityUsesTheSameJerkModel() - { - double maximumDistance = JerkLimitedStoppingMath.CalculateMaximumStoppedDistance( - 0d, 0d, 0.20d, 0.20d, 0.30d, 0.50d, 2d); - Verification.True(maximumDistance > 0d && maximumDistance < 0.40d, - "two-second stopped reach is finite and below unconstrained cruise distance"); - - double cap = JerkLimitedStoppingMath.MaximumInitialSpeedForDistance( - maximumDistance, 0.20d, 0.30d, 0.50d, 0.20d); - Verification.True(cap >= 0d && cap <= 0.20d, - "distance inversion stays inside the direction speed range"); - } - - private static void VerifiesRollingEnvelopeDoesNotStopAtWindowEnd() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - LateralPath path = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(1d, 1d, 0d, 0d), - }); - var rolling = new LongitudinalPlanningInput(path, TravelDirection.Forward, - 0d, 0d, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, configuration, - Array.Empty(), Array.Empty()); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build( - rolling, out PathSpeedLimit envelope, out string failure); - - Verification.Equal(EmPlanningStatus.Success, status, "rolling envelope: " + failure); - Verification.True(envelope.MaximumSpeedAt(rolling.PathUpperBoundS) > 0d, - "rolling window end keeps a nonzero speed allowance"); - Verification.True(!envelope.HasStopBoundary, "rolling envelope has no stop boundary"); - Verification.NearlyEqual(rolling.PathUpperBoundS, envelope.PathUpperBoundS, - "rolling envelope reports its PathS upper bound"); - - var approach = new LongitudinalPlanningInput(path, TravelDirection.Forward, - 0d, 0d, EmTerminalType.Goal, - EmLongitudinalMode.ApproachStopBoundary, configuration, - Array.Empty(), Array.Empty()); - status = new PathSpeedLimitBuilder().Build(approach, out PathSpeedLimit approachEnvelope, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "approach envelope: " + failure); - Verification.True(approachEnvelope.HasStopBoundary, "approach envelope retains its real stop boundary"); - Verification.NearlyEqual(0d, approachEnvelope.StoppingLimitAt(approach.StopBoundaryPathS), - "approach stop boundary has an exact zero stopping limit"); - } - - private static void VerifiesFinitePathSIndexedSpeedEnvelope() - { - LateralPath directionPath = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(1d, 1d, 0d, 0d), - }); - var directionInput = new LongitudinalPlanningInput(directionPath, TravelDirection.Forward, 0d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, EmPlannerConfiguration.CreateDefault(), - Array.Empty(), Array.Empty()); - EmPlanningStatus directionStatus = new PathSpeedLimitBuilder().Build(directionInput, - out PathSpeedLimit directionEnvelope, out string directionFailureReason); - Verification.Equal(EmPlanningStatus.Success, directionStatus, "default direction speed limit status: " + - directionFailureReason); - Verification.NearlyEqual(1d, directionEnvelope.DirectionMaximumSpeedMetersPerSecond, - "default direction speed limit"); - - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - LateralPath path = CreatePath(new[] - { - new PathFixture(10d, 0d, 0d, 0d), - new PathFixture(20d, 2d, 2d, 4d), - new PathFixture(20.5d, 4d, 20d, 0d), - new PathFixture(21d, 5d, 0d, 0d), - }); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - Array.Empty(), Array.Empty()); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "speed envelope status: " + failureReason); - Verification.NearlyEqual(1d, envelope.DirectionMaximumSpeedMetersPerSecond, "overridden direction speed limit"); - Verification.NearlyEqual(Math.Sqrt(0.20d / 2d), envelope.LateralAccelerationLimitAt(2d), - "curvature lateral-acceleration limit"); - Verification.NearlyEqual(0.50d / 4d, envelope.CurvatureRateLimitAt(2d), "curvature-rate limit"); - Verification.True(double.IsFinite(envelope.LateralAccelerationLimitAt(0d)) && - double.IsFinite(envelope.CurvatureRateLimitAt(0d)), "zero curvature limits stay finite"); - Verification.NearlyEqual(MaximumJerkLimitedStopSpeed(input, 4d), envelope.StoppingLimitAt(4d), - "stopping speed limit uses the complete jerk-limited model"); - Verification.NearlyEqual(Math.Sqrt(0.20d / 20d), envelope.MaximumSpeedAt(4d), - "combined limit chooses the finite minimum"); - double interpolationQueryPathS = 1.013d; - int upperStation = 1; - while (envelope.PathS[upperStation] < interpolationQueryPathS) - upperStation++; - double lowerPathS = envelope.PathS[upperStation - 1]; - double upperPathS = envelope.PathS[upperStation]; - double fraction = (interpolationQueryPathS - lowerPathS) / (upperPathS - lowerPathS); - double expectedInterpolatedSpeed = envelope.MaximumSpeedMetersPerSecond[upperStation - 1] + - (envelope.MaximumSpeedMetersPerSecond[upperStation] - envelope.MaximumSpeedMetersPerSecond[upperStation - 1]) * - fraction; - Verification.NearlyEqual(expectedInterpolatedSpeed, envelope.MaximumSpeedAt(interpolationQueryPathS), - "speed envelope interpolates by PathS rather than ReferenceS"); - Verification.NearlyEqual(0d, envelope.MaximumSpeedAt(5d), "terminal speed is exactly zero"); - } - - private static void VerifiesStoppingEnvelopeIsRefinedOnActualPathS() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - LateralPath path = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(100d, 2d, 0d, 0d), - }); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - Array.Empty(), Array.Empty()); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "refined stopping envelope status: " + failureReason); - Verification.True(envelope.PathS.Count > path.Points.Count, "stopping envelope inserts actual-PathS refinement stations"); - Verification.NearlyEqual(MaximumJerkLimitedStopSpeed(input, 1.5d), envelope.MaximumSpeedAt(1.5d), - "refined stopping envelope uses the complete jerk-limited stopping cap"); - } - - private static void VerifiesStoppingEnvelopeUsesJerkLimitedStoppingMath() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - LateralPath path = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(100d, 2d, 0d, 0d), - }); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - Array.Empty(), Array.Empty()); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "jerk-limited stopping-tail status: " + failureReason); - double nearBoundaryPathS = input.StopBoundaryPathS - 0.005d; - Verification.True(envelope.StoppingLimitAt(nearBoundaryPathS) > 0d && - envelope.StoppingLimitAt(nearBoundaryPathS) <= - MaximumJerkLimitedStopSpeed(input, nearBoundaryPathS) + 1e-12d, - "near-boundary speed cap conservatively interpolates jerk-limited distance inversion"); - Verification.NearlyEqual(0d, envelope.StoppingLimitAt(input.StopBoundaryPathS), - "real stop boundary keeps an exact zero stopping cap"); - } - - private static void VerifiesStoppingPrecheckOnlyAppliesToRealStopBoundaries() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - LateralPath shortPath = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(100d, 0.01d, 0d, 0d), - }); - var rolling = new LongitudinalPlanningInput(shortPath, TravelDirection.Forward, 0.20d, 0.20d, - EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, configuration, - Array.Empty(), Array.Empty()); - - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(rolling, out PathSpeedLimit envelope, - out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, - "rolling windows do not require a stop inside their local PathS extent: " + failureReason); - Verification.True(envelope != null, "rolling speed envelope is created despite the short local window"); - - var approach = new LongitudinalPlanningInput(shortPath, TravelDirection.Forward, 0.20d, 0.20d, - EmTerminalType.Goal, EmLongitudinalMode.ApproachStopBoundary, configuration, - Array.Empty(), Array.Empty()); - status = new PathSpeedLimitBuilder().Build(approach, out envelope, out failureReason); - Verification.Equal(EmPlanningStatus.StoppingDistanceInsufficient, status, - "real stop-boundary jerk/deceleration stopping precheck status"); - Verification.True(envelope == null, "stopping-distance failure does not create a speed envelope"); - Verification.True(failureReason.Length != 0, "stopping-distance failure explains the rejection"); - } - - private static void VerifiesReferenceHorizonSelectionSeparatesSpaceAndTime() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.DistanceHorizonMeters = 1d; - configuration.Scheduling.TimeHorizonSeconds = 2d; - - DirectionSegmentView longSegment = CreateSegment(4d, EmBoundaryType.Goal); - EmPlanningStatus status = new PlanningHorizonSelector().Select( - longSegment, 0d, 0d, 0d, EmPlanningScope.RollingHorizon, configuration, - out PlanningHorizonSelection rolling, out string failure); - Verification.Equal(EmPlanningStatus.Success, status, "rolling selection: " + failure); - Verification.NearlyEqual(1d, rolling.WindowEndReferenceS, - "distance horizon defines the LS window"); - Verification.Equal(EmLongitudinalMode.RollingContinuation, - rolling.LongitudinalMode, "far boundary rolls"); - - DirectionSegmentView visibleButFar = CreateSegment(0.35d, EmBoundaryType.Goal); - status = new PlanningHorizonSelector().Select( - visibleButFar, 0d, 0d, 0d, EmPlanningScope.RollingHorizon, configuration, - out PlanningHorizonSelection approach, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "approach selection: " + failure); - Verification.Equal(EmLongitudinalMode.ApproachStopBoundary, - approach.LongitudinalMode, "visible unreachable boundary approaches"); - - DirectionSegmentView reachableGoal = CreateSegment(0.10d, EmBoundaryType.Goal); - status = new PlanningHorizonSelector().Select( - reachableGoal, 0d, 0d, 0d, EmPlanningScope.RollingHorizon, configuration, - out PlanningHorizonSelection exact, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "exact selection: " + failure); - Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, - exact.LongitudinalMode, "reachable goal stops exactly"); - Verification.Equal(EmTerminalType.Goal, exact.TerminalType, - "exact stop preserves Goal identity"); - - IReadOnlyList regularTimes = LongitudinalCandidate.CreateKnotTimes(2d, 0.1d); - Verification.Equal(19, LongitudinalTerminalSchedule.GetStabilizationStartIndex( - regularTimes, 0.1d), "regular exact stop reserves t=1.9..2.0"); - Verification.Equal(1, LongitudinalTerminalSchedule.GetStabilizationStartIndex( - new[] { 0d, 0.1d, 0.2d, 0.25d }, 0.1d), - "short final interval moves the stop anchor earlier"); - } - - private static void VerifiesFullDirectionScopeSelectsActualSegmentBoundary() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.DistanceHorizonMeters = 1d; - DirectionSegmentView tenMeterGoal = CreateSegment(10d, EmBoundaryType.Goal); - var selector = new PlanningHorizonSelector(); - EmPlanningStatus status = selector.Select(tenMeterGoal, 3d, 0d, 0d, - EmPlanningScope.FullDirectionSegment, configuration, out PlanningHorizonSelection full, out string failure); - Verification.Equal(EmPlanningStatus.Success, status, "full selection: " + failure); - Verification.NearlyEqual(10d, full.WindowEndReferenceS, "full selection reaches actual segment end"); - Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, full.LongitudinalMode, - "full selection stops at its real boundary"); - Verification.True(full.HasStopBoundary, "full selection retains the real stop boundary"); - - status = selector.Select(tenMeterGoal, 3d, 0d, 0d, - EmPlanningScope.RollingHorizon, configuration, out PlanningHorizonSelection rolling, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "rolling selection: " + failure); - Verification.NearlyEqual(4d, rolling.WindowEndReferenceS, "rolling selection retains the one-metre window"); - - DirectionSegmentView gearSwitch = CreateSegment(10d, EmBoundaryType.GearSwitchApproach); - status = selector.Select(gearSwitch, 3d, 0d, 0d, - EmPlanningScope.FullDirectionSegment, configuration, out PlanningHorizonSelection gear, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "gear full selection: " + failure); - Verification.Equal(EmTerminalType.GearSwitch, gear.TerminalType, "gear full selection preserves switch terminal"); - Verification.NearlyEqual(10d, gear.WindowEndReferenceS, "gear full selection stops before the next segment"); - } - - private static void VerifiesFullDirectionScheduleDerivesDurationAndAdaptiveBreakpoints() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.TimeHorizonSeconds = 10d; - configuration.Scheduling.DistanceHorizonMeters = 0.25d; - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Scheduling.MaximumOptimizationTimeStepSeconds = 0.20d; - configuration.Scheduling.MaximumOptimizationSpatialStepMeters = 0.10d; - configuration.Scheduling.MaximumOptimizationKnotCount = 401; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 0.50d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 0.50d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 1d; - - LateralPath shortPath = CreateStraightPath(0.50d); - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(shortPath, TravelDirection.Forward, 0.10d, - EmTerminalType.Goal, configuration, out PathSpeedLimit shortLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "short full-segment envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(shortPath, shortLimit, 0.10d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule shortSchedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "short full-segment schedule: " + failureReason); - Verification.True(shortSchedule.TotalDurationSeconds < 10d, "short segment derives its own T_end"); - - LateralPath longPath = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(1.50d, 1.50d, 2d, 0d), - new PathFixture(3d, 3d, 0d, 0d), - }); - status = new PathSpeedLimitBuilder().Build(longPath, TravelDirection.Forward, 0.10d, - EmTerminalType.Goal, configuration, out PathSpeedLimit longLimit, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "long full-segment envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(longPath, longLimit, 0.10d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule longSchedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "long full-segment schedule: " + failureReason); - Verification.True(longSchedule.TotalDurationSeconds > shortSchedule.TotalDurationSeconds, - "duration grows from s_end and limits"); - Verification.True(longSchedule.KnotTimes.Count <= configuration.Scheduling.MaximumOptimizationKnotCount, - "adaptive schedule respects knot cap"); - Verification.True(longSchedule.IsAdaptive, "full segment produces an adaptive knot schedule"); - Verification.True(longSchedule.ReferencePathS.Count > longPath.Points.Count, - "curvature and stopping envelopes add schedule breakpoints"); - Verification.NearlyEqual(longPath.Points[longPath.Points.Count - 1].PathS, - longSchedule.ReferencePathS[longSchedule.ReferencePathS.Count - 1], "schedule reaches s_end"); - Verification.NearlyEqual(0d, - longSchedule.ReferenceSpeedMetersPerSecond[longSchedule.ReferenceSpeedMetersPerSecond.Count - 1], - "schedule stops at s_end"); - - EmPlannerConfiguration constrained = configuration.Copy(); - constrained.Scheduling.MaximumOptimizationKnotCount = 4; - status = new FullDirectionSegmentScheduleBuilder().TryBuild(longPath, longLimit, 0.10d, 0d, - constrained.Longitudinal.DesiredForwardSpeedMetersPerSecond, constrained, - out LongitudinalKnotSchedule rejected, out failureReason); - Verification.Equal(EmPlanningStatus.FullSegmentResourceLimitExceeded, status, - "undersized full-segment knot cap rejects rather than truncates"); - Verification.True(rejected == null, "resource rejection produces no partial schedule"); - Verification.True(failureReason.IndexOf("required", StringComparison.OrdinalIgnoreCase) >= 0 && - failureReason.IndexOf("configured", StringComparison.OrdinalIgnoreCase) >= 0, - "resource rejection reports required and configured knots"); - } - - private static void VerifiesFullDirectionInitialFeasibilityProjectionAndFallbackSemantics() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.MaximumOptimizationTimeStepSeconds = 0.20d; - configuration.Scheduling.MaximumOptimizationSpatialStepMeters = 0.10d; - configuration.Scheduling.MaximumOptimizationKnotCount = 401; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 20d; - LateralPath path = CreateStraightPath(0.0075d); - EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.05d, - EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "feasible-reference envelope: " + failureReason); - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.05d, 0d, - configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, - out LongitudinalKnotSchedule schedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "feasible-reference schedule: " + failureReason); - - Verification.True(typeof(LongitudinalKnotSchedule).GetProperty("ReferenceCandidate") == null, - "adaptive schedule is only a knot/reference/hold contract"); - Verification.True(schedule.TerminalHoldStartIndex > 0 && - schedule.TerminalHoldStartIndex < schedule.KnotTimes.Count, - "adaptive reference explicitly identifies its terminal hold boundary"); - Verification.True(schedule.TerminalHoldStartIndex >= 3, - "adaptive exact-stop schedule reserves three independent motion jerk intervals"); - LongitudinalCandidate strictProjection = CreateStrictNonuniformExactStopCandidate(); - var projectionSchedule = new LongitudinalKnotSchedule(strictProjection.KnotTimes, - new[] { 0d, 0.003d, 0.006d, 0.0075d, 0.0075d }, new[] { 0.05d, 0.025d, 0.01d, 0d, 0d }, true, 3); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.05d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, projectionSchedule, Array.Empty(), Array.Empty()); - Verification.True(new LongitudinalSolutionValidator().TryValidate(input, speedLimit, strictProjection, - out _, out failureReason), "nonuniform strict projection fixture is physically feasible: " + failureReason); - - var constraintBuilder = new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()); - Verification.True(constraintBuilder.TryBuildInitialFeasibilityProjection(input, speedLimit, - out QuadraticProgram projectionProblem, out failureReason), - "full exact-stop feasibility projection builds: " + failureReason); - var layout = new LongitudinalVariableLayout(projectionSchedule.KnotTimes.Count); - Verification.True(Math.Abs(projectionProblem.LinearCost[layout.S(1)]) > 1e-12d, - "feasibility projection tracks scheduled PathS"); - Verification.True(Math.Abs(projectionProblem.LinearCost[layout.U(1)]) > 1e-12d, - "feasibility projection tracks scheduled speed"); - Verification.Equal(9 * layout.KnotCount - 3 + - 3 * (layout.KnotCount - projectionSchedule.TerminalHoldStartIndex), projectionProblem.ConstraintCount, - "feasibility projection carries a PathS-linearized speed-envelope row for each motion knot"); - - var initialTimeoutSolver = new FakeQpSolver(new QpSolveResult(QpSolveStatus.TimeLimit, Array.Empty(), 0d, 0d, - 0d, 0, TimeSpan.Zero, "time limit", string.Empty)); - LongitudinalPlanningResult initialTimeout = new SequentialLongitudinalOptimizer(initialTimeoutSolver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SolverTimedOut, initialTimeout.Status, - "initial feasibility timeout cannot publish a fallback"); - Verification.True(initialTimeout.Candidate == null, "initial feasibility timeout publishes no candidate"); - - var solver = new FakeQpSolver(new[] - { - new QpSolveResult(QpSolveStatus.Solved, ToPrimal(strictProjection), 0d, 0d, 0d, 1, - TimeSpan.Zero, "solved", string.Empty), - new QpSolveResult(QpSolveStatus.TimeLimit, Array.Empty(), 0d, 0d, 0d, 0, - TimeSpan.Zero, "time limit", string.Empty), - }); - LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, - CancellationToken.None); - Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, - "strict feasibility projection permits a later exact-stop fallback: " + result.FailureReason); - Verification.Equal(2, solver.SolveCallCount, - "full scope consumes strict feasibility projection before the objective timeout"); - Verification.True(new LongitudinalSolutionValidator().TryValidate(input, speedLimit, - result.Candidate ?? throw new InvalidOperationException("Adaptive fallback was missing."), out _, - out failureReason), "adaptive fallback is strict-feasible: " + failureReason); - - EmPlannerConfiguration denserPublication = configuration.Copy(); - denserPublication.Scheduling.OutputTimeStepSeconds = 0.05d; - status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.05d, 0d, - denserPublication.Longitudinal.DesiredForwardSpeedMetersPerSecond, denserPublication, - out LongitudinalKnotSchedule sameOptimizationSchedule, out failureReason); - Verification.Equal(EmPlanningStatus.Success, status, "independent-cadence schedule: " + failureReason); - Verification.Equal(schedule.KnotTimes.Count, sameOptimizationSchedule.KnotTimes.Count, - "publication cadence does not change adaptive knot count"); - Verification.Equal(schedule.TerminalHoldStartIndex, sameOptimizationSchedule.TerminalHoldStartIndex, - "publication cadence does not change the terminal hold boundary"); - } - - private static LongitudinalCandidate CreateStrictNonuniformExactStopCandidate() - { - double[] times = { 0d, 0.09d, 0.19d, 0.30d, 0.50d }; - double[] motionTimes = { 0d, 0.09d, 0.19d, 0.30d }; - var influence = new double[3, 3]; - for (int interval = 0; interval < 3; interval++) - { - var basis = new double[3]; - basis[interval] = 1d; - LongitudinalCandidate response = LongitudinalCandidate.Integrate(motionTimes, 0d, 0d, 0d, basis); - int last = response.S.Count - 1; - influence[0, interval] = response.A[last]; - influence[1, interval] = response.U[last]; - influence[2, interval] = response.S[last]; - } - double[] jerkMotion = SolveThreeByThree(influence, new[] { 0d, -0.05d, -0.0075d }); - var jerk = new[] { jerkMotion[0], jerkMotion[1], jerkMotion[2], 0d }; - LongitudinalCandidate integrated = LongitudinalCandidate.Integrate(times, 0d, 0.05d, 0d, jerk); - var pathS = new[] { integrated.S[0], integrated.S[1], integrated.S[2], 0.0075d, 0.0075d }; - var speed = new[] { integrated.U[0], integrated.U[1], integrated.U[2], 0d, 0d }; - var acceleration = new[] { integrated.A[0], integrated.A[1], integrated.A[2], 0d, 0d }; - return new LongitudinalCandidate(times, pathS, speed, acceleration, jerk); - } - - private static double[] ToPrimal(LongitudinalCandidate candidate) - { - var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count); - var primal = new double[layout.VariableCount]; - for (int index = 0; index < layout.KnotCount; index++) - { - primal[layout.S(index)] = candidate.S[index]; - primal[layout.U(index)] = candidate.U[index]; - primal[layout.A(index)] = candidate.A[index]; - } - for (int index = 0; index < layout.KnotCount - 1; index++) - primal[layout.J(index)] = candidate.J[index]; - return primal; - } - - private static double[] SolveThreeByThree(double[,] matrix, IReadOnlyList rightHandSide) - { - var augmented = new double[3, 4]; - for (int row = 0; row < 3; row++) - { - for (int column = 0; column < 3; column++) - augmented[row, column] = matrix[row, column]; - augmented[row, 3] = rightHandSide[row]; - } - for (int pivot = 0; pivot < 3; pivot++) - { - int bestRow = pivot; - for (int row = pivot + 1; row < 3; row++) - { - if (Math.Abs(augmented[row, pivot]) > Math.Abs(augmented[bestRow, pivot])) - bestRow = row; - } - for (int column = pivot; column < 4; column++) - { - double temporary = augmented[pivot, column]; - augmented[pivot, column] = augmented[bestRow, column]; - augmented[bestRow, column] = temporary; - } - double divisor = augmented[pivot, pivot]; - for (int column = pivot; column < 4; column++) - augmented[pivot, column] /= divisor; - for (int row = 0; row < 3; row++) - { - if (row == pivot) - continue; - double factor = augmented[row, pivot]; - for (int column = pivot; column < 4; column++) - augmented[row, column] -= factor * augmented[pivot, column]; - } - } - return new[] { augmented[0, 3], augmented[1, 3], augmented[2, 3] }; - } - - private static void VerifiesTimeKnotLayoutDynamicsObjectiveAndHardConstraints() - { - var layout = new LongitudinalVariableLayout(5); - Verification.Equal(19, layout.VariableCount, "ST variable count"); - for (int index = 0; index < 5; index++) - { - Verification.Equal(index, layout.S(index), "s index " + index); - Verification.Equal(5 + index, layout.U(index), "u index " + index); - Verification.Equal(10 + index, layout.A(index), "a index " + index); - } - for (int index = 0; index < 4; index++) - Verification.Equal(15 + index, layout.J(index), "j index " + index); - - double[] times = { 0d, 0.05d, 0.10d, 0.15d, 0.20d }; - double[] jerk = { 0.30d, -0.10d, 0.20d, -0.20d }; - LongitudinalCandidate integrated = LongitudinalCandidate.Integrate(times, 0d, 0.10d, 0.02d, jerk); - for (int index = 0; index < jerk.Length; index++) - { - double dt = times[index + 1] - times[index]; - Verification.NearlyEqual(integrated.A[index] + dt * integrated.J[index], integrated.A[index + 1], - "exact ST acceleration dynamics " + index); - Verification.NearlyEqual(integrated.U[index] + dt * integrated.A[index] + 0.5d * dt * dt * integrated.J[index], - integrated.U[index + 1], "exact ST speed dynamics " + index); - Verification.NearlyEqual(integrated.S[index] + dt * integrated.U[index] + - 0.5d * dt * dt * integrated.A[index] + dt * dt * dt * integrated.J[index] / 6d, - integrated.S[index + 1], "exact ST progress dynamics " + index); - } - Verification.True(integrated.SatisfiesExactDiscreteDynamics(1e-12d), "integrated ST candidate validates dynamics"); - - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - LateralPath path = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(1d, 1d, 0d, 0d), - new PathFixture(2d, 2d, 0d, 0d), - }); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.10d, 0.02d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - new[] { 0d, 0.10d, 0.20d, 0.30d, 0.40d }, - new[] { 0.20d, 0.20d, 0.20d, 0.20d, 0.20d }); - EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, - out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "unit-scale speed envelope: " + speedFailure); - - Verification.True(new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()).TryBuild(input, envelope, - integrated, out QuadraticProgram problem, out string failureReason), "ST QP builds: " + failureReason); - var rollingInput = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.10d, 0.02d, - EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, configuration, - new[] { 0d, 0.10d, 0.20d, 0.30d, 0.40d }, - new[] { 0.20d, 0.20d, 0.20d, 0.20d, 0.20d }); - EmPlanningStatus rollingSpeedStatus = new PathSpeedLimitBuilder().Build(rollingInput, - out PathSpeedLimit rollingEnvelope, out string rollingSpeedFailure); - Verification.Equal(EmPlanningStatus.Success, rollingSpeedStatus, - "unit-scale rolling speed envelope: " + rollingSpeedFailure); - Verification.True(new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()).TryBuild( - rollingInput, rollingEnvelope, integrated, out QuadraticProgram rollingProblem, out string rollingFailure), - "rolling ST QP builds: " + rollingFailure); - Verification.NearlyEqual(30d, MatrixValue(problem.UpperTriangularP, layout.U(0), layout.U(0)), - "normalized speed and previous-U P coefficient"); - Verification.NearlyEqual(2d, MatrixValue(problem.UpperTriangularP, layout.A(0), layout.A(0)), - "normalized acceleration P coefficient"); - Verification.NearlyEqual(20d, MatrixValue(problem.UpperTriangularP, layout.J(0), layout.J(0)), - "normalized jerk P coefficient"); - Verification.NearlyEqual(2.5d, MatrixValue(problem.UpperTriangularP, layout.S(0), layout.S(0)), - "normalized previous-S P coefficient"); - Verification.NearlyEqual(0d, MatrixValue(problem.UpperTriangularP, layout.S(4), layout.S(4)), - "fixed terminal S has no progress-reward coefficient"); - - FindSingleVariableBounds(problem, layout.S(0), out double sLower, out double sUpper); - Verification.NearlyEqual(0d, sLower, "S lower bound"); - Verification.NearlyEqual(2d, sUpper, "S upper bound"); - FindSingleVariableBounds(problem, layout.U(1), out double uLower, out double uUpper); - Verification.NearlyEqual(0d, uLower, "U nonnegative bound"); - Verification.NearlyEqual(input.DirectionMaximumSpeedMetersPerSecond, uUpper, - "U retains its direction hard bound alongside the PathS envelope"); - int envelopeSegment = 0; - while (envelopeSegment < envelope.PathS.Count - 2 && integrated.S[1] > envelope.PathS[envelopeSegment + 1]) - envelopeSegment++; - double envelopeSlope = (envelope.MaximumSpeedMetersPerSecond[envelopeSegment + 1] - - envelope.MaximumSpeedMetersPerSecond[envelopeSegment]) / - (envelope.PathS[envelopeSegment + 1] - envelope.PathS[envelopeSegment]); - double envelopeIntercept = envelope.MaximumSpeedMetersPerSecond[envelopeSegment] - - envelopeSlope * envelope.PathS[envelopeSegment]; - var envelopeRow = new Dictionary { { layout.U(1), 1d } }; - if (Math.Abs(envelopeSlope) > 1e-12d) - envelopeRow.Add(layout.S(1), -envelopeSlope); - Verification.Equal(1, CountBoundedRow(problem, envelopeRow, - -QuadraticProgram.MaximumFiniteBound, envelopeIntercept), - "U upper bound linearly re-evaluates the actual PathS envelope"); - FindSingleVariableBounds(problem, layout.A(1), out double aLower, out double aUpper); - Verification.NearlyEqual(-1d, aLower, "deceleration lower bound"); - Verification.NearlyEqual(1d, aUpper, "acceleration upper bound"); - FindSingleVariableBounds(problem, layout.J(1), out double jLower, out double jUpper); - Verification.NearlyEqual(-1d, jLower, "jerk lower bound"); - Verification.NearlyEqual(1d, jUpper, "jerk upper bound"); - - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary { { layout.S(0), 1d } }, 0d), - "exact initial S"); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary { { layout.U(0), 1d } }, 0.10d), - "exact initial U"); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary { { layout.A(0), 1d } }, 0.02d), - "exact initial A"); - Verification.Equal(0, CountExactEqualityRows(rollingProblem, - new Dictionary { { layout.S(4), 1d } }, rollingInput.PathUpperBoundS), - "rolling has no exact terminal S"); - Verification.Equal(0, CountExactEqualityRows(rollingProblem, - new Dictionary { { layout.U(4), 1d } }, 0d), - "rolling has no exact terminal U"); - Verification.Equal(0, CountExactEqualityRows(rollingProblem, - new Dictionary { { layout.A(4), 1d } }, 0d), - "rolling has no exact terminal A"); - int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - integrated.KnotTimes, configuration.Scheduling.OutputTimeStepSeconds); - for (int index = stabilizationStart; index < layout.KnotCount; index++) - { - Verification.Equal(1, CountExactEqualityRows(problem, - new Dictionary { { layout.S(index), 1d } }, input.StopBoundaryPathS), - "stop tail exact S " + index); - Verification.Equal(1, CountExactEqualityRows(problem, - new Dictionary { { layout.U(index), 1d } }, 0d), - "stop tail exact U " + index); - Verification.Equal(1, CountExactEqualityRows(problem, - new Dictionary { { layout.A(index), 1d } }, 0d), - "stop tail exact A " + index); - } - Verification.Equal(1, CountBoundedRow(problem, new Dictionary - { - { layout.S(1), 1d }, { layout.S(0), -1d }, - }, 0d, QuadraticProgram.MaximumFiniteBound), "monotonic S hard constraint"); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary - { - { layout.A(1), 1d }, { layout.A(0), -1d }, { layout.J(0), -0.05d }, - }, 0d), "exact ST acceleration equation"); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary - { - { layout.U(1), 1d }, { layout.U(0), -1d }, { layout.A(0), -0.05d }, { layout.J(0), -0.00125d }, - }, 0d), "exact ST speed equation"); - Verification.Equal(1, CountExactEqualityRows(problem, new Dictionary - { - { layout.S(1), 1d }, { layout.S(0), -1d }, { layout.U(0), -0.05d }, { layout.A(0), -0.00125d }, - { layout.J(0), -0.000020833333333333333d }, - }, 0d), "exact ST progress equation"); - } - - private static void VerifiesModeSpecificSolutionValidation() - { - EmPlannerConfiguration configuration = CreateTaskFourConfiguration(); - IReadOnlyList exactTimes = LongitudinalCandidate.CreateKnotTimes(0.30d, 0.10d); - LongitudinalCandidate nonstationaryExact = LongitudinalCandidate.Integrate( - exactTimes, 0d, 0.10d, 0d, new[] { -4d, 0d, 0d }); - LateralPath exactPath = CreateStraightPath(nonstationaryExact.S[nonstationaryExact.S.Count - 1]); - var exactInput = new LongitudinalPlanningInput(exactPath, TravelDirection.Forward, 0.10d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - Array.Empty(), Array.Empty()); - var rollingForExact = new LongitudinalPlanningInput(exactPath, TravelDirection.Forward, 0.10d, 0d, - EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, configuration, - Array.Empty(), Array.Empty()); - EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(rollingForExact, - out PathSpeedLimit rollingEnvelope, out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "exact-validation rolling envelope: " + speedFailure); - - var validator = new LongitudinalSolutionValidator(); - Verification.True(!validator.TryValidate(exactInput, rollingEnvelope, nonstationaryExact, - out _, out string exactFailure), "exact stops reject a nonstationary internal tail"); - Verification.True(exactFailure.IndexOf("exact stabilized", StringComparison.Ordinal) >= 0, - "exact-stop failure identifies the stabilized tail: " + exactFailure); - - LateralPath rollingPath = CreateStraightPath(0.10d); - var rollingInput = new LongitudinalPlanningInput(rollingPath, TravelDirection.Forward, 0.10d, 0d, - EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, configuration, - Array.Empty(), Array.Empty()); - speedStatus = new PathSpeedLimitBuilder().Build(rollingInput, out PathSpeedLimit openEnvelope, out speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "rolling-validation envelope: " + speedFailure); - LongitudinalCandidate rollingCandidate = LongitudinalCandidate.Integrate( - exactTimes, 0d, 0.10d, 0d, new[] { 0d, 0d, 0d }); - Verification.True(validator.TryValidate(rollingInput, openEnvelope, rollingCandidate, - out _, out string rollingFailure), "rolling nonzero terminal speed validates: " + rollingFailure); - - EmPlannerConfiguration approachConfiguration = CreateTaskFourConfiguration(); - approachConfiguration.Scheduling.TimeHorizonSeconds = 0.15d; - approachConfiguration.Scheduling.OutputTimeStepSeconds = 0.05d; - IReadOnlyList approachTimes = LongitudinalCandidate.CreateKnotTimes(0.15d, 0.05d); - LongitudinalCandidate unstoppablyFastApproach = LongitudinalCandidate.Integrate( - approachTimes, 0d, 0.20d, 0d, new[] { 0d, -10d, 10d }); - LateralPath approachPath = CreateStraightPath(0.035d); - var approachInput = new LongitudinalPlanningInput(approachPath, TravelDirection.Forward, 0.20d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ApproachStopBoundary, approachConfiguration, - Array.Empty(), Array.Empty()); - var rollingForApproach = new LongitudinalPlanningInput(approachPath, TravelDirection.Forward, 0.20d, 0d, - EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, approachConfiguration, - Array.Empty(), Array.Empty()); - speedStatus = new PathSpeedLimitBuilder().Build(rollingForApproach, - out PathSpeedLimit approachEnvelope, out speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "approach-validation rolling envelope: " + speedFailure); - Verification.True(!validator.TryValidate(approachInput, approachEnvelope, unstoppablyFastApproach, - out _, out string approachFailure), "approach candidates outside the stoppable set are rejected"); - Verification.True(approachFailure.IndexOf("stoppable set", StringComparison.Ordinal) >= 0, - "approach failure identifies the jerk-limited stoppable set"); - } - - private static void VerifiesFullDirectionNoProgressValidation() - { - EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); - double[] times = { 0d, 0.10d, 0.20d, 0.30d, 0.50d }; - var fullSchedule = new LongitudinalKnotSchedule(times, - new[] { 0d, 0.04d, 0.08d, 0.10d, 0.10d }, new[] { 0d, 0.20d, 0.10d, 0d, 0d }, true, 3); - LateralPath path = CreateStraightPath(0.10d); - var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, fullSchedule, Array.Empty(), Array.Empty()); - EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit speedLimit, - out string speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "no-progress envelope: " + speedFailure); - LongitudinalCandidate stationary = LongitudinalCandidate.Integrate(times, 0d, 0d, 0d, - new[] { 0d, 0d, 0d, 0d }); - - var validator = new LongitudinalSolutionValidator(); - Verification.True(!validator.TryValidate(input, speedLimit, stationary, out _, out EmPlanningStatus failureStatus, - out string failureReason), "nonterminal stationary full segment is rejected"); - Verification.Equal(EmPlanningStatus.NoProgress, failureStatus, "stationary full segment has exact status"); - Verification.True(failureReason.IndexOf("NoProgress", StringComparison.Ordinal) >= 0, - "stationary full segment has exact diagnostic"); - Verification.True(new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()).TryBuild(input, - speedLimit, stationary, out QuadraticProgram fullProblem, out string buildFailure), - "full no-progress objective builds: " + buildFailure); - var fullLayout = new LongitudinalVariableLayout(times.Length); - double expectedReferenceLinearCost = -2d * configuration.Longitudinal.Weights.ReferenceSpeed * - fullSchedule.ReferenceSpeedMetersPerSecond[1] / - (input.DirectionMaximumSpeedMetersPerSecond * input.DirectionMaximumSpeedMetersPerSecond); - Verification.NearlyEqual(expectedReferenceLinearCost, fullProblem.LinearCost[fullLayout.U(1)], - "full direction objective tracks adaptive speed reference"); - - LateralPath nearTerminalPath = CreateStraightPath(0.02d); - var nearTerminalSchedule = new LongitudinalKnotSchedule(times, - new[] { 0d, 0.01d, 0.02d, 0.02d, 0.02d }, new[] { 0d, 0d, 0d, 0d, 0d }, true, 3); - var nearTerminalInput = new LongitudinalPlanningInput(nearTerminalPath, TravelDirection.Forward, 0d, 0d, - EmTerminalType.GearSwitch, EmLongitudinalMode.ExactStopAtBoundary, configuration, - EmPlanningScope.FullDirectionSegment, nearTerminalSchedule, Array.Empty(), Array.Empty()); - speedStatus = new PathSpeedLimitBuilder().Build(nearTerminalInput, out PathSpeedLimit nearTerminalLimit, - out speedFailure); - Verification.Equal(EmPlanningStatus.Success, speedStatus, "near-terminal no-progress envelope: " + speedFailure); - Verification.True(!validator.TryValidate(nearTerminalInput, nearTerminalLimit, stationary, out _, - out failureStatus, out _), "near-terminal stationary full segment is rejected"); - Verification.Equal(EmPlanningStatus.NoProgress, failureStatus, - "near-terminal gear-switch stop is classified as no progress"); - } - - private static void VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically() - { - LateralPath path = CreateStraightPath(1d); - DateTimeOffset previousEffectiveAtUtc = DateTimeOffset.UnixEpoch.AddSeconds(10d); - EmTrajectory previous = CreatePreviousTrajectory(previousEffectiveAtUtc, TravelDirection.Forward, 3); - var builder = new LongitudinalPreviousTrajectorySeedBuilder(); - LongitudinalPreviousTrajectorySeed seed = builder.Build(previous, path, previousEffectiveAtUtc.AddSeconds(0.20d), - new[] { 0d, 0.10d, 0.20d }, 3, TravelDirection.Forward); - - Verification.Equal(3, seed.PathS.Count, "previous seed path-S count"); - Verification.Equal(3, seed.ProgressSpeedMetersPerSecond.Count, "previous seed speed count"); - Verification.NearlyEqual(0.20d, seed.PathS[0], "previous seed begins at new absolute effective time"); - Verification.True(seed.PathS[1] >= seed.PathS[0] && seed.PathS[2] >= seed.PathS[1], - "previous seed progress is monotone"); - Verification.NearlyEqual(0.10d, seed.ProgressSpeedMetersPerSecond[0], - "previous seed uses absolute progress speed"); - - Verification.Equal(0, builder.Build(previous, path, previousEffectiveAtUtc.AddSeconds(0.20d), - new[] { 0d, 0.10d, 0.20d }, 3, TravelDirection.Reverse).PathS.Count, - "different direction returns an empty seed"); - Verification.Equal(0, builder.Build(previous, path, previousEffectiveAtUtc.AddSeconds(0.20d), - new[] { 0d, 0.10d, 0.20d }, 4, TravelDirection.Forward).PathS.Count, - "different segment returns an empty seed"); - Verification.Equal(0, builder.Build(previous, path, previousEffectiveAtUtc.AddSeconds(0.40d), - new[] { 0d, 0.10d }, 3, TravelDirection.Forward).PathS.Count, - "out-of-range absolute sampling returns an empty seed"); - } - - private static LateralPath CreatePath(IReadOnlyList fixtures) - { - var points = new List(fixtures.Count); - for (int index = 0; index < fixtures.Count; index++) - { - PathFixture fixture = fixtures[index]; - points.Add(new LateralPathPoint(fixture.ReferenceS, fixture.PathS, 0d, 0d, 0d, 0d, fixture.PathS, 0d, - 0d, fixture.Curvature, fixture.Curvature, fixture.CurvatureDerivative)); - } - return new LateralPath(points, true); - } - - private static EmTrajectory CreatePreviousTrajectory(DateTimeOffset effectiveAtUtc, TravelDirection direction, - int segmentIndex) - { - var metadata = new EmTrajectoryMetadata("previous-seed", effectiveAtUtc, effectiveAtUtc, 1L, - "previous-reference", 1L, string.Empty, segmentIndex, direction, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, EmPlanningScope.RollingHorizon); - double sign = direction == TravelDirection.Forward ? 1d : -1d; - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(0d, 0d, 0d, sign * 0.10d, 0d, 0d, segmentIndex, 0d, 0d, - direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.10d, 0d, 0d, sign * 0.10d, 0.10d, 0d, segmentIndex, 0.10d, 0.10d, - direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.20d, 0d, 0d, sign * 0.10d, 0.20d, 0d, segmentIndex, 0.20d, 0.20d, - direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.30d, 0d, 0d, sign * 0.10d, 0.30d, 0d, segmentIndex, 0.30d, 0.30d, - direction, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(sign * 0.40d, 0d, 0d, sign * 0.10d, 0.40d, 0d, segmentIndex, 0.40d, 0.40d, - direction, EmBoundaryType.None, 0d, 0d), - }); - } - - private static DirectionSegmentView CreateSegment(double length, EmBoundaryType endBoundaryType) - { - var points = new List - { - Point(0d, 0d), - Point(length, length), - }; - return new DirectionSegmentView(0, TravelDirection.Forward, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), - new ReferenceBoundary(0, length, endBoundaryType, length), 0d); - } - - private static SmoothedPathPoint Point(double x, double s) - { - return new SmoothedPathPoint(x, 0d, 0d, 0d, s, TravelDirection.Forward, 0d, 0d, 0d, 1d, false, - SmoothedPathPointSource.Anchor); - } - - private static EmPlannerConfiguration CreateUnitScaleConfiguration() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.TimeHorizonSeconds = 0.20d; - configuration.Scheduling.OutputTimeStepSeconds = 0.05d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 1d; - configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; - return configuration; - } - - private static EmPlannerConfiguration CreateTaskFourConfiguration() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.TimeHorizonSeconds = 0.30d; - configuration.Scheduling.OutputTimeStepSeconds = 0.10d; - configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; - configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d; - configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 10d; - configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; - configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; - return configuration; - } - - private static LateralPath CreateStraightPath(double pathUpperBoundS) - { - return CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(pathUpperBoundS, pathUpperBoundS, 0d, 0d), - }); - } - - private static double MaximumJerkLimitedStopSpeed(LongitudinalPlanningInput input, double pathS) - { - LongitudinalConfiguration limits = input.Configuration.Longitudinal; - return JerkLimitedStoppingMath.MaximumInitialSpeedForDistance( - Math.Max(0d, input.StopBoundaryPathS - pathS), - Math.Max(0d, input.InitialAccelerationMetersPerSecondSquared), - limits.MaximumDecelerationMetersPerSecondSquared, - limits.MaximumJerkMetersPerSecondCubed, - input.DirectionMaximumSpeedMetersPerSecond); - } - - private static double MatrixValue(SparseCscMatrix matrix, int row, int column) - { - for (int index = matrix.ColumnPointers[column]; index < matrix.ColumnPointers[column + 1]; index++) - { - if (matrix.RowIndices[index] == row) - return matrix.Values[index]; - } - return 0d; - } - - private static void FindSingleVariableBounds(QuadraticProgram problem, int variable, out double lower, out double upper) - { - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (RowMatches(problem.ConstraintMatrix, row, new Dictionary { { variable, 1d } })) - { - lower = problem.LowerBounds[row]; - upper = problem.UpperBounds[row]; - return; - } - } - throw new InvalidOperationException("No single-variable bounds were found for variable " + variable + "."); - } - - private static int CountExactEqualityRows(QuadraticProgram problem, IReadOnlyDictionary expected, - double bound) - { - return CountBoundedRow(problem, expected, bound, bound); - } - - private static int CountBoundedRow(QuadraticProgram problem, IReadOnlyDictionary expected, - double lower, double upper) - { - int count = 0; - for (int row = 0; row < problem.ConstraintCount; row++) - { - if (Math.Abs(problem.LowerBounds[row] - lower) <= 1e-12d && - Math.Abs(problem.UpperBounds[row] - upper) <= 1e-12d && RowMatches(problem.ConstraintMatrix, row, expected)) - { - count++; - } - } - return count; - } - - private static bool RowMatches(SparseCscMatrix matrix, int targetRow, IReadOnlyDictionary expected) - { - var actual = new Dictionary(); - for (int column = 0; column < matrix.ColumnCount; column++) - { - for (int index = matrix.ColumnPointers[column]; index < matrix.ColumnPointers[column + 1]; index++) - { - if (matrix.RowIndices[index] == targetRow) - actual[column] = matrix.Values[index]; - } - } - if (actual.Count != expected.Count) - return false; - foreach (KeyValuePair pair in expected) - { - if (!actual.TryGetValue(pair.Key, out double actualValue) || Math.Abs(actualValue - pair.Value) > 1e-12d) - return false; - } - return true; - } - - private sealed class PathFixture - { - public PathFixture(double referenceS, double pathS, double curvature, double curvatureDerivative) - { - ReferenceS = referenceS; - PathS = pathS; - Curvature = curvature; - CurvatureDerivative = curvatureDerivative; - } - - public double ReferenceS { get; } - public double PathS { get; } - public double Curvature { get; } - public double CurvatureDerivative { get; } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/OptimizationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/OptimizationChecks.cs deleted file mode 100644 index 3dcc75a..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/OptimizationChecks.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class OptimizationChecks -{ - public static void Run() - { - VerifyCanonicalCscAssembly(); - VerifyInvalidTripletsAreRejected(); - VerifyUpperTriangularHessianStorage(); - VerifyQuadraticProgramValidation(); - VerifyQuadraticProgramDefensivelyCopiesInputs(); - } - - private static void VerifyCanonicalCscAssembly() - { - var builder = new SparseTripletBuilder(3, 2); - builder.Add(2, 1, 1.5d); - builder.Add(0, 0, 2d); - builder.Add(1, 1, 3d); - builder.Add(2, 1, 0.5d); - builder.Add(1, 0, -2d); - builder.Add(0, 1, 7d); - builder.Add(0, 1, -7d); - - SparseCscMatrix matrix = builder.Build(); - - EMPlannerVerificationHost.Verification.Equal(3, matrix.ColumnPointers.Count, "CSC column-pointer length"); - EMPlannerVerificationHost.Verification.Equal(0, matrix.ColumnPointers[0], "CSC first pointer"); - EMPlannerVerificationHost.Verification.Equal(2, matrix.ColumnPointers[1], "CSC second pointer"); - EMPlannerVerificationHost.Verification.Equal(4, matrix.ColumnPointers[2], "CSC final pointer"); - EMPlannerVerificationHost.Verification.Equal(4, matrix.Values.Count, "CSC nonzero count"); - EMPlannerVerificationHost.Verification.Equal(0, matrix.RowIndices[0], "CSC first row"); - EMPlannerVerificationHost.Verification.Equal(1, matrix.RowIndices[1], "CSC second row"); - EMPlannerVerificationHost.Verification.Equal(1, matrix.RowIndices[2], "CSC third row"); - EMPlannerVerificationHost.Verification.Equal(2, matrix.RowIndices[3], "CSC fourth row"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, matrix.Values[0], "CSC first value"); - EMPlannerVerificationHost.Verification.NearlyEqual(-2d, matrix.Values[1], "CSC second value"); - EMPlannerVerificationHost.Verification.NearlyEqual(3d, matrix.Values[2], "CSC third value"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, matrix.Values[3], "CSC duplicate sum"); - } - - private static void VerifyInvalidTripletsAreRejected() - { - var builder = new SparseTripletBuilder(2, 2); - - AssertArgumentOutOfRange(() => builder.Add(-1, 0, 1d), "negative row"); - AssertArgumentOutOfRange(() => builder.Add(0, -1, 1d), "negative column"); - AssertArgumentOutOfRange(() => builder.Add(2, 0, 1d), "row outside matrix"); - AssertArgumentOutOfRange(() => builder.Add(0, 2, 1d), "column outside matrix"); - AssertArgumentOutOfRange(() => builder.Add(0, 0, double.NaN), "NaN triplet"); - AssertArgumentOutOfRange(() => builder.Add(0, 0, double.PositiveInfinity), "infinite triplet"); - } - - private static void VerifyUpperTriangularHessianStorage() - { - var upperBuilder = new SparseTripletBuilder(2, 2, true); - upperBuilder.Add(0, 0, 1d); - upperBuilder.Add(0, 1, 2d); - AssertArgumentOutOfRange(() => upperBuilder.Add(1, 0, 3d), "lower-triangular Hessian entry"); - - SparseCscMatrix upperHessian = upperBuilder.Build(); - var constraints = new SparseTripletBuilder(1, 2); - constraints.Add(0, 0, 1d); - constraints.Add(0, 1, 1d); - - var lowerTriangleBuilder = new SparseTripletBuilder(2, 2); - lowerTriangleBuilder.Add(0, 0, 1d); - lowerTriangleBuilder.Add(1, 0, 3d); - AssertArgumentOutOfRange( - () => new QuadraticProgram(lowerTriangleBuilder.Build(), new[] { 0d, 0d }, constraints.Build(), new[] { 0d }, new[] { 1d }), - "lower-triangular quadratic-program Hessian"); - - EMPlannerVerificationHost.Verification.Equal(2, upperHessian.ColumnCount, "upper Hessian column count"); - } - - private static void VerifyQuadraticProgramValidation() - { - var nonSquareHessian = new SparseCscMatrix(1, 2, new double[0], new int[0], new[] { 0, 0, 0 }); - var oneVariableHessian = new SparseTripletBuilder(1, 1, true); - oneVariableHessian.Add(0, 0, 1d); - var oneConstraint = new SparseTripletBuilder(1, 1); - oneConstraint.Add(0, 0, 1d); - - AssertArgumentOutOfRange( - () => new QuadraticProgram(nonSquareHessian, new[] { 0d, 0d }, oneConstraint.Build(), new[] { 0d }, new[] { 1d }), - "non-square Hessian"); - AssertArgumentOutOfRange( - () => new QuadraticProgram(oneVariableHessian.Build(), new[] { 0d }, oneConstraint.Build(), new[] { 2d }, new[] { 1d }), - "inverted constraint bounds"); - AssertArgumentOutOfRange( - () => new QuadraticProgram(oneVariableHessian.Build(), new[] { double.NaN }, oneConstraint.Build(), new[] { 0d }, new[] { 1d }), - "non-finite linear cost"); - AssertArgumentOutOfRange( - () => new QuadraticProgram(oneVariableHessian.Build(), new[] { 0d }, oneConstraint.Build(), new[] { -1e30d }, new[] { 2e30d }), - "out-of-range finite bound"); - } - - private static void VerifyQuadraticProgramDefensivelyCopiesInputs() - { - var hessianBuilder = new SparseTripletBuilder(1, 1, true); - hessianBuilder.Add(0, 0, 1d); - var constraintBuilder = new SparseTripletBuilder(1, 1); - constraintBuilder.Add(0, 0, 1d); - - var linearCost = new List { -2d }; - var lowerBounds = new List { 0d }; - var upperBounds = new List { 1d }; - QuadraticProgram problem = new QuadraticProgram( - hessianBuilder.Build(), - linearCost, - constraintBuilder.Build(), - lowerBounds, - upperBounds); - - linearCost[0] = 100d; - lowerBounds[0] = -100d; - upperBounds[0] = 100d; - - EMPlannerVerificationHost.Verification.Equal(1, problem.VariableCount, "micro problem variable count"); - EMPlannerVerificationHost.Verification.Equal(1, problem.ConstraintCount, "micro problem constraint count"); - EMPlannerVerificationHost.Verification.NearlyEqual(-2d, problem.LinearCost[0], "copied linear cost"); - EMPlannerVerificationHost.Verification.NearlyEqual(0d, problem.LowerBounds[0], "copied lower bound"); - EMPlannerVerificationHost.Verification.NearlyEqual(1d, problem.UpperBounds[0], "copied upper bound"); - } - - private static void AssertArgumentOutOfRange(Action action, string name) - { - try - { - action(); - } - catch (ArgumentOutOfRangeException) - { - return; - } - - throw new InvalidOperationException(name + " did not throw ArgumentOutOfRangeException."); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs deleted file mode 100644 index 26b60f1..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class OsqpChecks -{ - public static void Run() - { - var pluginDirectories = new List(); - try - { - string missingNativeDirectory = CreatePluginDirectory(); - pluginDirectories.Add(missingNativeDirectory); - ProbeResult missingNative = RunProbe(missingNativeDirectory); - EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.SolverUnavailable, missingNative.Status, "missing native status"); - EMPlannerVerificationHost.Verification.True(!string.IsNullOrEmpty(missingNative.Diagnostic), "missing native diagnostic"); - - string corruptNativeDirectory = CreatePluginDirectory(); - pluginDirectories.Add(corruptNativeDirectory); - File.WriteAllText(Path.Combine(corruptNativeDirectory, "osqp.dll"), "not a native library"); - ProbeResult corruptNative = RunProbe(corruptNativeDirectory); - EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.SolverUnavailable, corruptNative.Status, "corrupt native status"); - EMPlannerVerificationHost.Verification.True(!string.IsNullOrEmpty(corruptNative.Diagnostic), "corrupt native diagnostic"); - EMPlannerVerificationHost.Verification.True(corruptNative.RawOutput.IndexOf("BadImageFormatException", StringComparison.Ordinal) < 0, - "corrupt native does not escape BadImageFormatException"); - - string realNativeDirectory = CreatePluginDirectory(); - pluginDirectories.Add(realNativeDirectory); - string realNativePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ThirdParty", "OSQP", "win-x64", "osqp.dll")); - EMPlannerVerificationHost.Verification.True(File.Exists(realNativePath), "pinned native package exists"); - File.Copy(realNativePath, Path.Combine(realNativeDirectory, "osqp.dll"), false); - ProbeResult realNative = RunProbe(realNativeDirectory); - EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.Solved, realNative.Status, "real native status"); - EMPlannerVerificationHost.Verification.Equal("1.0.0", realNative.Version, "real native version"); - EMPlannerVerificationHost.Verification.True(realNative.ModuleHandle != IntPtr.Zero, "real native module handle"); - EMPlannerVerificationHost.Verification.Equal(true, realNative.ConcurrentHandleStable, "concurrent native module handle"); - EMPlannerVerificationHost.Verification.Equal(true, realNative.SolveChecksPassed, "native solve checks"); - Console.WriteLine("PASS osqp-solve"); - } - finally - { - for (int index = 0; index < pluginDirectories.Count; index++) - { - if (Directory.Exists(pluginDirectories[index])) - Directory.Delete(pluginDirectories[index], true); - } - } - } - - public static void RunProbe() - { - var results = new OsqpNativeLoadResult[16]; - Parallel.For(0, results.Length, index => results[index] = OsqpNativeLoader.Load()); - - OsqpNativeLoadResult first = results[0]; - bool sameHandle = true; - for (int index = 1; index < results.Length; index++) - { - sameHandle = sameHandle && results[index].Status == first.Status && results[index].ModuleHandle == first.ModuleHandle; - } - - Console.WriteLine("STATUS=" + first.Status); - Console.WriteLine("VERSION=" + first.Version); - Console.WriteLine("HANDLE=" + first.ModuleHandle.ToInt64()); - Console.WriteLine("DIAGNOSTIC=" + first.Diagnostic.Replace('\r', ' ').Replace('\n', ' ')); - Console.WriteLine("CONCURRENT_HANDLE_STABLE=" + sameHandle); - - if (first.Status == QpSolveStatus.Solved) - { - VerifyNativeSolveLifecycle(); - Console.WriteLine("SOLVE_CHECKS=PASS"); - } - } - - private static string CreatePluginDirectory() - { - string sourceDirectory = AppContext.BaseDirectory; - string destinationDirectory = Path.Combine(Path.GetTempPath(), "em-planner-osqp-loader-" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(destinationDirectory); - - string[] hostFiles = Directory.GetFiles(sourceDirectory); - for (int index = 0; index < hostFiles.Length; index++) - { - if (string.Equals(Path.GetFileName(hostFiles[index]), "osqp.dll", StringComparison.OrdinalIgnoreCase)) - continue; - string destinationPath = Path.Combine(destinationDirectory, Path.GetFileName(hostFiles[index])); - File.Copy(hostFiles[index], destinationPath, false); - } - - EMPlannerVerificationHost.Verification.True(File.Exists(Path.Combine(destinationDirectory, "ClumsyPilot.dll")), "copied plugin ClumsyPilot.dll"); - return destinationDirectory; - } - - private static void VerifyNativeSolveLifecycle() - { - var solver = new OsqpNativeSolver(); - - QpSolveResult bounded = solver.Solve( - CreateBoundedOptimumProblem(), - CreateSettings(TimeSpan.FromMilliseconds(100)), - new[] { 0d }, - CancellationToken.None); - EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.Solved, bounded.Status, "bounded optimum status"); - AssertPopulatedResult(bounded, "bounded optimum"); - EMPlannerVerificationHost.Verification.Equal(1, bounded.Primal.Count, "bounded optimum primal count"); - AssertClose(1d, bounded.Primal[0], 1e-5d, "bounded optimum primal"); - AssertClose(-1.5d, bounded.Objective, 1e-5d, "bounded optimum objective"); - EMPlannerVerificationHost.Verification.True(bounded.PrimalResidual <= 1e-5d, "bounded optimum primal residual"); - EMPlannerVerificationHost.Verification.True(bounded.DualResidual <= 1e-5d, "bounded optimum dual residual"); - - QpSolveResult equality = solver.Solve( - CreateEqualityOptimumProblem(), - CreateSettings(TimeSpan.FromMilliseconds(100)), - new[] { 0.5d, 0.5d }, - CancellationToken.None); - EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.Solved, equality.Status, "equality optimum status"); - AssertPopulatedResult(equality, "equality optimum"); - EMPlannerVerificationHost.Verification.Equal(2, equality.Primal.Count, "equality optimum primal count"); - AssertClose(0.5d, equality.Primal[0], 1e-5d, "equality optimum first primal"); - AssertClose(0.5d, equality.Primal[1], 1e-5d, "equality optimum second primal"); - AssertClose(0.5d, equality.Objective, 1e-5d, "equality optimum objective"); - EMPlannerVerificationHost.Verification.True(equality.PrimalResidual <= 1e-5d, "equality optimum primal residual"); - EMPlannerVerificationHost.Verification.True(equality.DualResidual <= 1e-5d, "equality optimum dual residual"); - - QpSolveResult infeasible = solver.Solve( - CreateInfeasibleProblem(), - CreateSettings(TimeSpan.FromMilliseconds(100)), - null, - CancellationToken.None); - EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.PrimalInfeasible, infeasible.Status, "infeasible status"); - AssertPopulatedResult(infeasible, "infeasible"); - - QpSolveResult tinyTimeLimit = solver.Solve( - CreateEqualityOptimumProblem(), - CreateSettings(TimeSpan.FromTicks(1)), - null, - CancellationToken.None); - EMPlannerVerificationHost.Verification.True( - tinyTimeLimit.Status == QpSolveStatus.TimeLimit || - tinyTimeLimit.Status == QpSolveStatus.Solved || - tinyTimeLimit.Status == QpSolveStatus.SolvedInaccurate, - "tiny time-limit status maps to a time limit or solved state"); - AssertPopulatedResult(tinyTimeLimit, "tiny time-limit"); - } - - private static QuadraticProgram CreateBoundedOptimumProblem() - { - var hessian = new SparseTripletBuilder(1, 1, true); - hessian.Add(0, 0, 1d); - var constraints = new SparseTripletBuilder(1, 1); - constraints.Add(0, 0, 1d); - return new QuadraticProgram(hessian.Build(), new[] { -2d }, constraints.Build(), new[] { 0d }, new[] { 1d }); - } - - private static QuadraticProgram CreateEqualityOptimumProblem() - { - var hessian = new SparseTripletBuilder(2, 2, true); - hessian.Add(0, 0, 2d); - hessian.Add(1, 1, 2d); - var constraints = new SparseTripletBuilder(1, 2); - constraints.Add(0, 0, 1d); - constraints.Add(0, 1, 1d); - return new QuadraticProgram(hessian.Build(), new[] { 0d, 0d }, constraints.Build(), new[] { 1d }, new[] { 1d }); - } - - private static QuadraticProgram CreateInfeasibleProblem() - { - var hessian = new SparseTripletBuilder(1, 1, true); - hessian.Add(0, 0, 1d); - var constraints = new SparseTripletBuilder(2, 1); - constraints.Add(0, 0, 1d); - constraints.Add(1, 0, 1d); - return new QuadraticProgram( - hessian.Build(), - new[] { 0d }, - constraints.Build(), - new[] { 1d, -QuadraticProgram.MaximumFiniteBound }, - new[] { QuadraticProgram.MaximumFiniteBound, 0d }); - } - - private static QpSolverSettings CreateSettings(TimeSpan timeLimit) - { - return new QpSolverSettings(4000, 1e-6d, 1e-6d, timeLimit, true, true, false); - } - - private static void AssertPopulatedResult(QpSolveResult result, string name) - { - EMPlannerVerificationHost.Verification.True(result.Iterations >= 0, name + " iterations"); - EMPlannerVerificationHost.Verification.True(result.SolveTime >= TimeSpan.Zero, name + " solve time"); - EMPlannerVerificationHost.Verification.True(!double.IsNaN(result.Objective) && !double.IsInfinity(result.Objective), name + " objective"); - EMPlannerVerificationHost.Verification.True(!double.IsNaN(result.PrimalResidual) && !double.IsInfinity(result.PrimalResidual), name + " primal residual"); - EMPlannerVerificationHost.Verification.True(!double.IsNaN(result.DualResidual) && !double.IsInfinity(result.DualResidual), name + " dual residual"); - EMPlannerVerificationHost.Verification.True(!string.IsNullOrWhiteSpace(result.NativeStatus), name + " native status"); - } - - private static void AssertClose(double expected, double actual, double tolerance, string name) - { - EMPlannerVerificationHost.Verification.True(Math.Abs(expected - actual) <= tolerance, name + " expected " + expected + " but was " + actual); - } - - private static ProbeResult RunProbe(string pluginDirectory) - { - var startInfo = new ProcessStartInfo - { - FileName = Path.Combine(pluginDirectory, "EMPlannerVerificationHost.exe"), - Arguments = "osqp-probe", - WorkingDirectory = pluginDirectory, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - - using (var process = new Process { StartInfo = startInfo }) - { - process.Start(); - string standardOutput = process.StandardOutput.ReadToEnd(); - string standardError = process.StandardError.ReadToEnd(); - process.WaitForExit(); - if (process.ExitCode != 0) - throw new InvalidOperationException("OSQP loader probe exited " + process.ExitCode + ": " + standardError + standardOutput); - return ProbeResult.Parse(standardOutput + standardError); - } - } - - private sealed class ProbeResult - { - private ProbeResult(QpSolveStatus status, string version, IntPtr moduleHandle, string diagnostic, bool concurrentHandleStable, bool solveChecksPassed, string rawOutput) - { - Status = status; - Version = version; - ModuleHandle = moduleHandle; - Diagnostic = diagnostic; - ConcurrentHandleStable = concurrentHandleStable; - SolveChecksPassed = solveChecksPassed; - RawOutput = rawOutput; - } - - public QpSolveStatus Status { get; } - - public string Version { get; } - - public IntPtr ModuleHandle { get; } - - public string Diagnostic { get; } - - public bool ConcurrentHandleStable { get; } - - public bool SolveChecksPassed { get; } - - public string RawOutput { get; } - - public static ProbeResult Parse(string output) - { - string statusText = ReadValue(output, "STATUS="); - string version = ReadValue(output, "VERSION="); - string handleText = ReadValue(output, "HANDLE="); - string diagnostic = ReadValue(output, "DIAGNOSTIC="); - string stableText = ReadValue(output, "CONCURRENT_HANDLE_STABLE="); - string solveChecksText = ReadOptionalValue(output, "SOLVE_CHECKS="); - - QpSolveStatus status; - if (!Enum.TryParse(statusText, out status)) - throw new InvalidOperationException("Loader probe reported an invalid status: " + statusText); - long handleValue; - if (!long.TryParse(handleText, out handleValue)) - throw new InvalidOperationException("Loader probe reported an invalid handle: " + handleText); - bool concurrentHandleStable; - if (!bool.TryParse(stableText, out concurrentHandleStable)) - throw new InvalidOperationException("Loader probe reported an invalid concurrency flag: " + stableText); - - return new ProbeResult(status, version, new IntPtr(handleValue), diagnostic, concurrentHandleStable, - string.Equals(solveChecksText, "PASS", StringComparison.Ordinal), output); - } - - private static string ReadValue(string output, string prefix) - { - string[] lines = output.Replace("\r", string.Empty).Split('\n'); - for (int index = 0; index < lines.Length; index++) - { - if (lines[index].StartsWith(prefix, StringComparison.Ordinal)) - return lines[index].Substring(prefix.Length); - } - - throw new InvalidOperationException("Loader probe did not report " + prefix + ". Output: " + output); - } - - private static string ReadOptionalValue(string output, string prefix) - { - string[] lines = output.Replace("\r", string.Empty).Split('\n'); - for (int index = 0; index < lines.Length; index++) - { - if (lines[index].StartsWith(prefix, StringComparison.Ordinal)) - return lines[index].Substring(prefix.Length); - } - - return string.Empty; - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs deleted file mode 100644 index 942ee5b..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Security.Cryptography; - -namespace EMPlannerVerificationHost; - -internal static class PluginPackagingChecks -{ - public static void Run() - { - string repositoryRoot = Directory.GetCurrentDirectory(); - string scriptPath = Path.Combine(repositoryRoot, "ClumsyPilot", "scripts", "Publish-ClumsyPilotPlugin.ps1"); - Verification.True(File.Exists(scriptPath), "plugin publish script exists"); - - string managedDll = Path.Combine(AppContext.BaseDirectory, "ClumsyPilot.dll"); - Verification.True(File.Exists(managedDll), "verification host has managed ClumsyPilot.dll"); - VerifyBuildOutputMetadata(); - - string packageRoot = Path.Combine(Path.GetTempPath(), "em-planner-plugin-package-" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(packageRoot); - try - { - Publish(scriptPath, managedDll, packageRoot); - VerifyPackage(repositoryRoot, packageRoot); - - Publish(scriptPath, managedDll, packageRoot); - VerifyPackage(repositoryRoot, packageRoot); - AssertNoStaleSiblingDirectories(packageRoot); - } - finally - { - if (Directory.Exists(packageRoot)) - Directory.Delete(packageRoot, true); - } - } - - private static void Publish(string scriptPath, string managedDll, string packageRoot) - { - var startInfo = new ProcessStartInfo - { - FileName = "powershell.exe", - WorkingDirectory = Directory.GetCurrentDirectory(), - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - startInfo.ArgumentList.Add("-NoProfile"); - startInfo.ArgumentList.Add("-ExecutionPolicy"); - startInfo.ArgumentList.Add("Bypass"); - startInfo.ArgumentList.Add("-File"); - startInfo.ArgumentList.Add(scriptPath); - startInfo.ArgumentList.Add("-ManagedDll"); - startInfo.ArgumentList.Add(managedDll); - startInfo.ArgumentList.Add("-OutputDirectory"); - startInfo.ArgumentList.Add(packageRoot); - - using (var process = new Process { StartInfo = startInfo }) - { - process.Start(); - string standardOutput = process.StandardOutput.ReadToEnd(); - string standardError = process.StandardError.ReadToEnd(); - process.WaitForExit(); - if (process.ExitCode != 0) - { - throw new InvalidOperationException("plugin publish exited " + process.ExitCode + ": " + - standardOutput + standardError); - } - } - } - - private static void VerifyPackage(string repositoryRoot, string packageRoot) - { - string pluginsDirectory = Path.Combine(packageRoot, "plugins"); - Verification.True(Directory.Exists(pluginsDirectory), "plugins directory exists"); - - var relativeFiles = new List(); - string[] files = Directory.GetFiles(pluginsDirectory, "*", SearchOption.AllDirectories); - for (int index = 0; index < files.Length; index++) - relativeFiles.Add(Path.GetRelativePath(packageRoot, files[index]).Replace('\\', '/')); - relativeFiles.Sort(StringComparer.Ordinal); - - Verification.Equal("plugins/ClumsyPilot.dll|plugins/TrajectoryPlanningVisualization.dll|" + - "plugins/licenses/OSQP-LICENSE.txt|plugins/licenses/OSQP-NOTICE.txt|plugins/licenses/OSQP-VERSION.txt|" + - "plugins/osqp.dll", string.Join("|", relativeFiles), - "plugin tree contains exactly the managed DLLs, native DLL, and OSQP license files"); - - string managedPlugin = Path.Combine(pluginsDirectory, "ClumsyPilot.dll"); - AssemblyName managedAssembly = AssemblyName.GetAssemblyName(managedPlugin); - Verification.True(!string.IsNullOrWhiteSpace(managedAssembly.Name), "packaged ClumsyPilot.dll is managed"); - - string visualizationPlugin = Path.Combine(pluginsDirectory, "TrajectoryPlanningVisualization.dll"); - AssemblyName visualizationAssembly = AssemblyName.GetAssemblyName(visualizationPlugin); - Verification.Equal("TrajectoryPlanningVisualization", visualizationAssembly.Name, - "packaged visualization DLL has the expected managed assembly name"); - - string nativePlugin = Path.Combine(pluginsDirectory, "osqp.dll"); - Verification.Equal((ushort)0x8664, ReadPeMachine(nativePlugin), "packaged OSQP binary is x64"); - Verification.Equal(ReadExpectedOsqpHash(repositoryRoot), GetSha256(nativePlugin), - "packaged OSQP hash matches SHA256SUMS"); - } - - private static void AssertNoStaleSiblingDirectories(string packageRoot) - { - string parentDirectory = Path.GetDirectoryName(packageRoot) ?? - throw new InvalidOperationException("temporary package root has no parent directory"); - string prefix = Path.GetFileName(packageRoot); - string[] matchingDirectories = Directory.GetDirectories(parentDirectory, prefix + "*"); - Verification.Equal(1, matchingDirectories.Length, "publish leaves no stale sibling staging directory"); - Verification.Equal(packageRoot, matchingDirectories[0], "publish keeps only the requested temporary test root"); - } - - private static ushort ReadPeMachine(string path) - { - byte[] bytes = File.ReadAllBytes(path); - Verification.True(bytes.Length > 0x40 && bytes[0] == 'M' && bytes[1] == 'Z', "OSQP has DOS header"); - int peOffset = BitConverter.ToInt32(bytes, 0x3c); - Verification.True(peOffset >= 0 && peOffset + 6 <= bytes.Length && bytes[peOffset] == 'P' && - bytes[peOffset + 1] == 'E', "OSQP has PE header"); - return BitConverter.ToUInt16(bytes, peOffset + 4); - } - - private static string ReadExpectedOsqpHash(string repositoryRoot) - { - string manifestPath = Path.Combine(repositoryRoot, "ClumsyPilot", "ThirdParty", "OSQP", "SHA256SUMS"); - string[] tokens = File.ReadAllText(manifestPath).Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - Verification.True(tokens.Length >= 2 && string.Equals(tokens[1], "win-x64/osqp.dll", StringComparison.Ordinal), - "OSQP hash manifest describes the pinned x64 runtime"); - return tokens[0].ToLowerInvariant(); - } - - private static string GetSha256(string path) - { - return Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant(); - } - - private static void VerifyBuildOutputMetadata() - { - string outputDirectory = AppContext.BaseDirectory; - Verification.True(File.Exists(Path.Combine(outputDirectory, "osqp.dll")), - "build output contains the pinned OSQP runtime"); - Verification.True(File.Exists(Path.Combine(outputDirectory, "licenses", "OSQP-LICENSE.txt")), - "build output contains the OSQP license"); - Verification.True(File.Exists(Path.Combine(outputDirectory, "licenses", "OSQP-NOTICE.txt")), - "build output contains the OSQP notice"); - Verification.True(File.Exists(Path.Combine(outputDirectory, "licenses", "OSQP-VERSION.txt")), - "build output contains the OSQP version"); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs deleted file mode 100644 index 02670f1..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; - -namespace EMPlannerVerificationHost; - -internal static class Program -{ - private static int Main(string[] args) - { - if (args.Length != 1 || (args[0] != "foundation" && args[0] != "segmentation" && args[0] != "frenet" && - args[0] != "corridor" && args[0] != "optimization" && args[0] != "osqp" && args[0] != "osqp-loader" && args[0] != "osqp-probe" && - args[0] != "all-foundation" && args[0] != "lateral-model" && args[0] != "lateral-integration" && - args[0] != "lateral-real-osqp" && args[0] != "lateral-real-osqp-probe" && args[0] != "lateral-all" && - args[0] != "longitudinal-model" && args[0] != "longitudinal-integration" && - args[0] != "longitudinal-real-osqp-probe" && args[0] != "trajectory" && - args[0] != "em-planning-service" && args[0] != "em-core-all" && args[0] != "coordinator" && - args[0] != "executor" && args[0] != "plugin-package" && args[0] != "trajectory-observation" && args[0] != "em-all")) - { - Console.Error.WriteLine("Usage: EMPlannerVerificationHost foundation|segmentation|frenet|corridor|optimization|osqp|osqp-loader|all-foundation|lateral-model|lateral-integration|lateral-real-osqp|lateral-all|longitudinal-model|longitudinal-integration|em-core-all|coordinator|executor|plugin-package|trajectory-observation|em-all"); - return 2; - } - - try - { - if (args[0] == "foundation" || args[0] == "all-foundation" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.FoundationChecks.Run(); - Console.WriteLine("PASS foundation"); - } - if (args[0] == "segmentation" || args[0] == "all-foundation" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.SegmentationChecks.Run(); - Console.WriteLine("PASS segmentation"); - } - if (args[0] == "frenet" || args[0] == "all-foundation" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.FrenetChecks.Run(); - Console.WriteLine("PASS frenet"); - } - if (args[0] == "corridor" || args[0] == "all-foundation" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.CorridorChecks.Run(); - Console.WriteLine("PASS corridor"); - } - if (args[0] == "optimization" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.OptimizationChecks.Run(); - Console.WriteLine("PASS optimization"); - } - if (args[0] == "osqp" || args[0] == "osqp-loader" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.OsqpChecks.Run(); - Console.WriteLine("PASS osqp-loader"); - } - if (args[0] == "osqp-probe") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.OsqpChecks.RunProbe(); - } - if (args[0] == "lateral-model" || args[0] == "lateral-all" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LateralModelChecks.Run(); - Console.WriteLine("PASS lateral-model"); - } - if (args[0] == "lateral-integration" || args[0] == "lateral-all" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LateralIntegrationChecks.Run(); - Console.WriteLine("PASS lateral-integration"); - } - if (args[0] == "lateral-real-osqp-probe") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LateralIntegrationChecks.RunRealOsqp(); - Console.WriteLine("PASS lateral-real-osqp"); - } - if (args[0] == "lateral-real-osqp" || args[0] == "lateral-all" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LateralIntegrationChecks.RunRealOsqpInCleanPluginBundle(); - Console.WriteLine("PASS lateral-real-osqp"); - } - if (args[0] == "longitudinal-model") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LongitudinalModelChecks.Run(); - Console.WriteLine("PASS longitudinal-model"); - } - if (args[0] == "longitudinal-integration") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LongitudinalIntegrationChecks.Run(); - Console.WriteLine("PASS longitudinal-integration"); - } - if (args[0] == "longitudinal-real-osqp-probe") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LongitudinalIntegrationChecks.RunRealOsqp(); - Console.WriteLine("PASS longitudinal-real-osqp"); - } - if (args[0] == "trajectory") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.TrajectoryChecks.Run(); - Console.WriteLine("PASS trajectory"); - } - if (args[0] == "em-planning-service" || args[0] == "em-core-all" || args[0] == "em-all") - { - if (args[0] == "em-core-all" || args[0] == "em-all") - { - MultiWheelC.TrajectoryPlanning.EMPlanner.LongitudinalModelChecks.Run(); - Console.WriteLine("PASS longitudinal-model"); - MultiWheelC.TrajectoryPlanning.EMPlanner.LongitudinalIntegrationChecks.Run(); - Console.WriteLine("PASS longitudinal-integration"); - MultiWheelC.TrajectoryPlanning.EMPlanner.TrajectoryChecks.Run(); - Console.WriteLine("PASS trajectory"); - } - MultiWheelC.TrajectoryPlanning.EMPlanner.EmPlanningServiceChecks.Run(); - Console.WriteLine("PASS em-planning-service"); - } - if (args[0] == "coordinator" || args[0] == "em-all") - { - CoordinatorChecks.Run(); - Console.WriteLine("PASS coordinator"); - } - if (args[0] == "executor" || args[0] == "em-all") - { - ExecutorChecks.Run(); - Console.WriteLine("PASS executor"); - } - if (args[0] == "plugin-package" || args[0] == "em-all") - { - PluginPackagingChecks.Run(); - Console.WriteLine("PASS plugin-package"); - } - if (args[0] == "trajectory-observation" || args[0] == "em-all") - { - TrajectoryObservationChecks.Run(); - Console.WriteLine("PASS trajectory-observation"); - } - if (args[0] == "em-all") - { - CoordinatorChecks.RunRollingEndToEnd(); - Console.WriteLine("PASS rolling-end-to-end"); - ExecutorChecks.RunRollingEndToEnd(); - Console.WriteLine("PASS rolling-execution-tail"); - } - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine(exception); - return 1; - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/SegmentationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/SegmentationChecks.cs deleted file mode 100644 index bc62cce..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/SegmentationChecks.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MultiWheelC.TrajectoryPlanning.EMPlanner; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class SegmentationChecks -{ - public static void Run() - { - var referencePath = EMPlannerVerificationHost.EmFixtureFactory.CreateGearPairReferencePath(); - var segments = ReferencePathSegmenter.Create(referencePath); - - EMPlannerVerificationHost.Verification.Equal(2, segments.Count, "segment count"); - EMPlannerVerificationHost.Verification.Equal(EmBoundaryType.GearSwitchApproach, - segments[0].EndBoundary.BoundaryType, "forward end boundary"); - EMPlannerVerificationHost.Verification.Equal(EmBoundaryType.GearSwitchDeparture, - segments[1].StartBoundary.BoundaryType, "reverse start boundary"); - EMPlannerVerificationHost.Verification.True(!segments[0].EndBoundary.Equals(segments[1].StartBoundary), - "gear-pair boundary identities differ"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, referencePath.Path[2].ArcLength, "forward source arc length"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, referencePath.Path[3].ArcLength, "reverse source arc length"); - EMPlannerVerificationHost.Verification.NearlyEqual(0d, segments[1].Points[0].ArcLength, "rebased reverse start"); - - ReferenceHorizonSlice rolling = ReferenceHorizonSlicer.Slice(segments[0], 1.95d); - EMPlannerVerificationHost.Verification.Equal(EmBoundaryType.RollingSafetyStop, - rolling.TerminalBoundary.BoundaryType, "rolling terminal type"); - EMPlannerVerificationHost.Verification.NearlyEqual(1.95d, rolling.TerminalBoundary.SegmentLocalS, - "rolling terminal local s"); - EMPlannerVerificationHost.Verification.NearlyEqual(1.95d, rolling.Points[rolling.Points.Count - 1].ArcLength, - "rolling anchor path s"); - EMPlannerVerificationHost.Verification.NearlyEqual(1.95d, rolling.Points[rolling.Points.Count - 1].X, - "rolling anchor x"); - - ReferenceHorizonSlice gearSwitch = ReferenceHorizonSlicer.Slice(segments[0], 2.05d); - EMPlannerVerificationHost.Verification.Equal(EmBoundaryType.GearSwitchApproach, - gearSwitch.TerminalBoundary.BoundaryType, "gear terminal type"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, gearSwitch.TerminalBoundary.SegmentLocalS, - "gear terminal local s"); - EMPlannerVerificationHost.Verification.NearlyEqual(2d, gearSwitch.Points[gearSwitch.Points.Count - 1].ArcLength, - "gear anchor path s"); - for (int index = 0; index < gearSwitch.Points.Count; index++) - { - EMPlannerVerificationHost.Verification.Equal(segments[0].Direction, gearSwitch.Points[index].Direction, - "horizon point direction"); - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs deleted file mode 100644 index 88618a5..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs +++ /dev/null @@ -1,551 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using EMPlannerVerificationHost; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; - -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -internal static class TrajectoryChecks -{ - public static void Run() - { - VerifiesForwardFieldsExactTerminalAndHold(); - VerifiesSegmentLocalReferenceStationIsPublished(); - VerifiesReverseTravelVelocityAndUnwrappedYaw(); - VerifiesPublishedListsAreImmutable(); - VerifiesRollingTrajectoryHasNoSyntheticStopTail(); - VerifiesExactStopHoldHasNoJerkDiscontinuity(); - VerifiesWorldSpacePublicationMutationsAreRejected(); - VerifiesTerminalPoseAndSampleLimitPublicationGates(); - VerifiesReverseSpeedLimitUsesReverseConfiguration(); - VerifiesFullScopeNormalizesToleranceSizedPathSResiduals(); - VerifiesFullScopeRejectsMaterialPathSRegressionWithDiagnostics(); - } - - private static void VerifiesForwardFieldsExactTerminalAndHold() - { - EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( - CreatePath(TravelDirection.Forward, 0d, Math.PI / 2d), - CreateLongitudinalResult(), - CreateMetadata(TravelDirection.Forward, EmTerminalType.Goal)); - - VerifyKinematicFields(trajectory, TravelDirection.Forward, "forward"); - VerifyTerminalAndHold(trajectory, EmBoundaryType.Goal, "forward"); - } - - private static void VerifiesReverseTravelVelocityAndUnwrappedYaw() - { - EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( - CreatePath(TravelDirection.Reverse, 3.10d, -3.10d), - CreateLongitudinalResult(), - CreateMetadata(TravelDirection.Reverse, EmTerminalType.GearSwitch)); - - VerifyKinematicFields(trajectory, TravelDirection.Reverse, "reverse"); - VerifyTerminalAndHold(trajectory, EmBoundaryType.GearSwitchApproach, "reverse"); - EmTrajectoryPoint moving = trajectory.Points[1]; - Verification.True(moving.SignedLongitudinalVelocity < 0d, "reverse signed velocity is negative"); - Verification.True(moving.VelocityX * Math.Cos(moving.Yaw) + moving.VelocityY * Math.Sin(moving.Yaw) < 0d, - "reverse world velocity points opposite the vehicle yaw"); - Verification.True(Math.Abs(Math.Abs(moving.Yaw) - Math.PI) < 0.1d, - "reverse yaw interpolation unwraps across the pi boundary"); - } - - private static void VerifiesSegmentLocalReferenceStationIsPublished() - { - EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( - CreatePath(TravelDirection.Forward, 0d, 0d), CreateLongitudinalResult(), - CreateMetadata(TravelDirection.Forward, EmTerminalType.Goal)); - - EmTrajectoryPoint terminal = trajectory.Points[trajectory.Points.Count - 1]; - Verification.NearlyEqual(1d, terminal.SegmentLocalS, - "published SegmentLocalS retains the lateral path reference station"); - Verification.NearlyEqual(0.12d, terminal.PathS, - "published PathS remains the longitudinal actual path distance"); - - ValidationContext context = CreateValidationContext(); - EmTrajectory validationTrajectory = CreateValidationTrajectory(TravelDirection.Forward); - EmTrajectoryValidationResult accepted = new EmTrajectoryValidator().Validate(validationTrajectory, context.EmptyMap, - context.Vehicle, context.Configuration, 2, 1d, 0.0055d, EmBoundaryType.Goal); - Verification.True(accepted.IsValid, - "publication validates reference and actual path bounds independently: " + accepted.Message); - EmTrajectoryValidationResult segmentExceeded = new EmTrajectoryValidator().Validate(validationTrajectory, - context.EmptyMap, context.Vehicle, context.Configuration, 2, 0.5d, 0.0055d, EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.SegmentBoundaryExceeded, segmentExceeded.Failure, - "segment-local reference bound is checked independently of PathS"); - } - - private static void VerifiesPublishedListsAreImmutable() - { - EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( - CreatePath(TravelDirection.Forward, 0d, 0d), CreateLongitudinalResult(), - CreateMetadata(TravelDirection.Forward, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation)); - Verification.True(!(trajectory.Points is IList mutable) || mutable.IsReadOnly, - "trajectory public point list is immutable"); - } - - private static void VerifiesRollingTrajectoryHasNoSyntheticStopTail() - { - EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( - CreatePath(TravelDirection.Forward, 0d, 0d), CreateRollingLongitudinalResult(), - CreateMetadata(TravelDirection.Forward, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation)); - - Verification.Equal(21, trajectory.Points.Count, "rolling trajectory keeps only ST knots"); - EmTrajectoryPoint terminal = trajectory.Points[trajectory.Points.Count - 1]; - Verification.Equal(EmBoundaryType.None, terminal.BoundaryType, - "rolling horizon end is not a boundary anchor"); - Verification.True(terminal.SignedLongitudinalVelocity > 0d, - "rolling terminal speed stays nonzero"); - Verification.Equal(EmLongitudinalMode.RollingContinuation, trajectory.Metadata.LongitudinalMode, - "rolling longitudinal mode is published"); - } - - private static void VerifiesExactStopHoldHasNoJerkDiscontinuity() - { - ValidationContext context = CreateValidationContext(); - EmTrajectory trajectory = CreateValidationTrajectory(TravelDirection.Forward); - EmTrajectoryValidationResult result = new EmTrajectoryValidator().Validate(trajectory, context.EmptyMap, - context.Vehicle, context.Configuration, 2, 0.0055d, EmBoundaryType.Goal); - Verification.True(result.IsValid, "exact stop with internal tail and external hold is publishable: " + result.Message); - - int anchorIndex = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - CreateValidationCandidate().KnotTimes, context.Configuration.Scheduling.OutputTimeStepSeconds); - EmTrajectoryPoint previousMoving = trajectory.Points[anchorIndex - 1]; - EmTrajectoryPoint anchor = trajectory.Points[anchorIndex]; - EmTrajectoryPoint stabilization = trajectory.Points[anchorIndex + 1]; - EmTrajectoryPoint firstExternalHold = trajectory.Points[anchorIndex + 2]; - Verification.Equal(EmBoundaryType.Goal, anchor.BoundaryType, "exact stop marks only the real boundary anchor"); - Verification.Equal(EmBoundaryType.None, stabilization.BoundaryType, - "QP stabilization point is not a duplicate boundary anchor"); - Verification.Equal(EmBoundaryType.None, firstExternalHold.BoundaryType, - "external hold is not a duplicate boundary anchor"); - - double terminalFiniteDifferenceAcceleration = - (anchor.SignedLongitudinalVelocity - previousMoving.SignedLongitudinalVelocity) / - (anchor.TimeFromStart - previousMoving.TimeFromStart); - double stabilizationAcceleration = - (stabilization.SignedLongitudinalVelocity - anchor.SignedLongitudinalVelocity) / - (stabilization.TimeFromStart - anchor.TimeFromStart); - double externalHoldAcceleration = - (firstExternalHold.SignedLongitudinalVelocity - stabilization.SignedLongitudinalVelocity) / - (firstExternalHold.TimeFromStart - stabilization.TimeFromStart); - double jerkIntoStabilization = (stabilizationAcceleration - terminalFiniteDifferenceAcceleration) / - (stabilization.TimeFromStart - anchor.TimeFromStart); - double jerkIntoExternalHold = (externalHoldAcceleration - stabilizationAcceleration) / - (firstExternalHold.TimeFromStart - stabilization.TimeFromStart); - Verification.True(Math.Abs(jerkIntoStabilization) <= 0.5d, - "jerk into QP stabilization stays within the limit"); - Verification.NearlyEqual(0d, jerkIntoExternalHold, "jerk into the external hold is zero"); - } - - private static void VerifiesWorldSpacePublicationMutationsAreRejected() - { - ValidationContext context = CreateValidationContext(); - EmTrajectory valid = CreateValidationTrajectory(TravelDirection.Forward); - EmTrajectoryValidationResult accepted = new EmTrajectoryValidator().Validate(valid, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.0055d, EmBoundaryType.Goal); - Verification.True(accepted.IsValid, "valid world-space trajectory is publishable: " + accepted.Message); - - AssertRejected(context, CorruptDouble(valid, 1, "X", double.NaN), EmTrajectoryValidationFailure.NonFinite, 1, - "non-finite point"); - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], timeFromStart: valid.Points[0].TimeFromStart)), - EmTrajectoryValidationFailure.TimeNotStrictlyIncreasing, 1, "non-increasing time"); - AssertRejected(context, Replace(valid, 2, Clone(valid.Points[2], pathS: 0.0005d)), - EmTrajectoryValidationFailure.PathSDecreased, 2, "decreasing PathS"); - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], signedSpeed: -valid.Points[1].Speed)), - EmTrajectoryValidationFailure.DirectionSignMismatch, 1, "direction sign"); - AssertRejected(context, CorruptDouble(valid, 1, "Speed", valid.Points[1].Speed + 0.01d), - EmTrajectoryValidationFailure.RedundantSpeedMismatch, 1, "redundant speed"); - AssertRejected(context, CorruptDouble(valid, 1, "VelocityX", valid.Points[1].VelocityX + 0.01d), - EmTrajectoryValidationFailure.WorldVelocityMismatch, 1, "world velocity"); - AssertRejected(context, CorruptDouble(valid, 1, "YawRate", valid.Points[1].YawRate + 0.01d), - EmTrajectoryValidationFailure.YawRateMismatch, 1, "yaw rate"); - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], signedSpeed: 1.10d)), - EmTrajectoryValidationFailure.SpeedLimitExceeded, 1, "speed limit"); - AssertRejected(context, Replace(valid, 2, Clone(valid.Points[2], signedSpeed: 0.19d)), - EmTrajectoryValidationFailure.AccelerationLimitExceeded, 2, "acceleration limit"); - - EmTrajectory jerkMutated = Replace(valid, 1, Clone(valid.Points[1], signedSpeed: 0.035d)); - jerkMutated = Replace(jerkMutated, 2, Clone(jerkMutated.Points[2], signedSpeed: 0.03d)); - AssertRejected(context, jerkMutated, EmTrajectoryValidationFailure.JerkLimitExceeded, 2, "jerk limit"); - EmTrajectoryValidationResult jerkResult = new EmTrajectoryValidator().Validate( - jerkMutated, context.EmptyMap, context.Vehicle, context.Configuration, 2, 0.0055d, EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.JerkLimitExceeded, jerkResult.Failure, - "jerk diagnostic failure code"); - Verification.Equal(2, jerkResult.PointIndex, "jerk diagnostic point index"); - foreach (string field in new[] - { - "time=", "dt=", "previousAcceleration=", "acceleration=", "jerk=", "limit=", "excess=", - "storedPreviousJerk=", "storedCurrentJerk=", - }) - { - Verification.True(jerkResult.Message.Contains(field), "jerk diagnostic includes " + field); - } - - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], vehicleCurvature: 2d)), - EmTrajectoryValidationFailure.CurvatureLimitExceeded, 1, "curvature limit"); - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], vehicleCurvature: 0.75d)), - EmTrajectoryValidationFailure.CurvatureRateLimitExceeded, 1, "curvature-rate limit"); - - int terminalIndex = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - CreateValidationCandidate().KnotTimes, context.Configuration.Scheduling.OutputTimeStepSeconds); - AssertRejected(context, Replace(valid, terminalIndex, Clone(valid.Points[terminalIndex], boundaryType: EmBoundaryType.None)), - EmTrajectoryValidationFailure.MissingTerminalAnchor, terminalIndex - 1, "missing exact terminal anchor"); - AssertRejected(context, Replace(valid, terminalIndex, Clone(valid.Points[terminalIndex], signedSpeed: 0.01d)), - EmTrajectoryValidationFailure.TerminalSpeedNotZero, terminalIndex, "terminal speed"); - AssertRejected(context, CorruptDouble(valid, terminalIndex, "LongitudinalAcceleration", 0.01d), - EmTrajectoryValidationFailure.TerminalAccelerationNotZero, terminalIndex, "terminal acceleration"); - AssertRejected(context, CorruptDouble(valid, terminalIndex, "YawRate", 0.01d), - EmTrajectoryValidationFailure.TerminalYawRateNotZero, terminalIndex, "terminal yaw rate"); - - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], x: 1d)), context.PoseCollisionMap, - EmTrajectoryValidationFailure.PoseCollision, 1, "pose collision"); - AssertRejected(context, Replace(valid, 1, Clone(valid.Points[1], x: 1d)), context.SweptCollisionMap, - EmTrajectoryValidationFailure.SweptCollision, 1, "swept collision"); - EmTrajectoryValidationResult beyondSegment = new EmTrajectoryValidator().Validate(valid, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.004d, EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.SegmentBoundaryExceeded, beyondSegment.Failure, - "segment-boundary failure code"); - Verification.Equal(4, beyondSegment.PointIndex, "segment-boundary first point"); - } - - private static void VerifiesTerminalPoseAndSampleLimitPublicationGates() - { - ValidationContext context = CreateValidationContext(); - EmTrajectory valid = CreateValidationTrajectory(TravelDirection.Forward); - int terminalIndex = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - CreateValidationCandidate().KnotTimes, context.Configuration.Scheduling.OutputTimeStepSeconds); - EmTrajectoryPoint terminal = valid.Points[terminalIndex]; - var validator = new EmTrajectoryValidator(); - - EmTrajectoryValidationResult exactPosition = validator.Validate(valid, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, terminal.Yaw), - EmBoundaryType.Goal); - Verification.True(exactPosition.IsValid, "exact terminal position is accepted"); - EmTrajectoryValidationResult outsidePosition = validator.Validate(valid, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.0055d, new Pose2D(terminal.X + 0.001d, terminal.Y, terminal.Yaw), - EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.TerminalPoseMismatch, outsidePosition.Failure, - "1-millimetre terminal position error is rejected"); - - double degrees = Math.PI / 180d; - EmTrajectory wrappedYaw = Replace(valid, terminalIndex, Clone(terminal, yaw: Math.PI)); - EmTrajectoryValidationResult wrapped = validator.Validate(wrappedYaw, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -Math.PI), - EmBoundaryType.Goal); - Verification.True(wrapped.IsValid, "PI and -PI use normalized yaw error"); - EmTrajectoryValidationResult outsideYaw = validator.Validate(valid, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -0.1d * degrees), - EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.TerminalPoseMismatch, outsideYaw.Failure, - "0.1-degree terminal yaw error is rejected"); - - EmTrajectory rolling = new EmTrajectoryAssembler().Assemble( - CreatePath(TravelDirection.Forward, 0d, 0d), CreateRollingLongitudinalResult(), - CreateMetadata(TravelDirection.Forward, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation)); - EmTrajectoryValidationResult rollingResult = validator.Validate(rolling, context.EmptyMap, context.Vehicle, - context.Configuration, 2, 0.08d, new Pose2D(99d, 99d, Math.PI), EmBoundaryType.RollingSafetyStop); - Verification.True(rollingResult.IsValid, "rolling-safety window end does not use the terminal-pose gate"); - - EmPlannerConfiguration limitedConfiguration = EmPlannerConfiguration.CreateDefault(); - limitedConfiguration.Scheduling.MaximumPublishedSampleCount = 8; - EmPlanningStatus assemblyStatus = new EmTrajectoryAssembler(limitedConfiguration).TryAssemble( - CreatePath(TravelDirection.Forward, 0d, 0d), CreateLongitudinalResult(), - CreateMetadata(TravelDirection.Forward, EmTerminalType.Goal), out EmTrajectory limitedTrajectory, - out string assemblyFailure); - Verification.Equal(EmPlanningStatus.FullSegmentResourceLimitExceeded, assemblyStatus, - "sample schedule one above the limit is rejected"); - Verification.True(limitedTrajectory == null, "sample-limit rejection does not assemble a partial trajectory"); - Verification.True(assemblyFailure.IndexOf("MaximumPublishedSampleCount", StringComparison.Ordinal) >= 0, - "sample-limit rejection preserves a diagnostic"); - Verification.True(assemblyFailure.IndexOf("required-at-least=9", StringComparison.Ordinal) >= 0, - "sample-limit fixture exceeds the cap by exactly one sample"); - } - - private static void VerifiesReverseSpeedLimitUsesReverseConfiguration() - { - ValidationContext context = CreateValidationContext(); - context.Configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.20d; - context.Configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 0.02d; - EmTrajectoryValidationResult result = new EmTrajectoryValidator().Validate( - CreateValidationTrajectory(TravelDirection.Reverse), context.EmptyMap, context.Vehicle, context.Configuration, - 2, 0.0055d, EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.SpeedLimitExceeded, result.Failure, - "reverse speed uses the configured reverse limit"); - Verification.Equal(0, result.PointIndex, "reverse speed first over-limit point"); - } - - private static void AssertRejected(ValidationContext context, EmTrajectory trajectory, - EmTrajectoryValidationFailure expectedFailure, int expectedIndex, string name) - { - AssertRejected(context, trajectory, context.EmptyMap, expectedFailure, expectedIndex, name); - } - - private static void AssertRejected(ValidationContext context, EmTrajectory trajectory, PlanningGridMap map, - EmTrajectoryValidationFailure expectedFailure, int expectedIndex, string name) - { - EmTrajectoryValidationResult result = new EmTrajectoryValidator().Validate(trajectory, map, context.Vehicle, - context.Configuration, 2, 0.0055d, EmBoundaryType.Goal); - Verification.True(!result.IsValid, name + " is rejected"); - Verification.Equal(expectedFailure, result.Failure, name + " failure code"); - Verification.Equal(expectedIndex, result.PointIndex, name + " failure index"); - } - - private static ValidationContext CreateValidationContext() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - return new ValidationContext(configuration, new VehicleParameters - { - LengthMeters = 0.01d, - WidthMeters = 0.01d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 1d, - }, - CreateValidationMap(Array.Empty()), - CreateValidationMap(new IMapObstacle[] { new AxisAlignedRectangleObstacle(990f, 1010f, -10f, 10f) }), - CreateValidationMap(new IMapObstacle[] { new AxisAlignedRectangleObstacle(490f, 510f, -10f, 10f) })); - } - - private static PlanningGridMap CreateValidationMap(IReadOnlyList obstacles) - { - IMapObstacleSource[] sources = obstacles.Count == 0 - ? Array.Empty() - : new IMapObstacleSource[] { new ManualObstacleSource("trajectory-validator", 1L, true, obstacles) }; - PlanningMapBuildResult result = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(-1000f, 3000f, -1000f, 1000f), - ResolutionMm = 20f, - ObstacleSources = sources, - AllowExplicitEmptyMap = obstacles.Count == 0, - }); - Verification.True(result.Succeeded && result.Map != null && result.Map.PlanningReady, - "trajectory-validator map builds: " + result.FailureReason); - return result.Map!; - } - - private static EmTrajectory CreateValidationTrajectory(TravelDirection direction) - { - LongitudinalCandidate candidate = CreateValidationCandidate(); - var result = new LongitudinalPlanningResult(EmPlanningStatus.Success, candidate, string.Empty); - var path = new LateralPath(new[] - { - new LateralPathPoint(0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d), - new LateralPathPoint(1d, 0.0055d, 0d, 0d, 0d, 0d, 0.0055d, 0d, 0d, 0d, 0d, 0d), - }, true); - return new EmTrajectoryAssembler().Assemble(path, result, CreateMetadata(direction, EmTerminalType.Goal)); - } - - private static LongitudinalCandidate CreateValidationCandidate() - { - double[] times = { 0d, 0.05d, 0.10d, 0.15d, 0.20d, 0.25d, 0.30d, 0.35d, 0.40d, 0.45d, 0.50d }; - double[] pathS = { 0d, 0.0014375d, 0.00271875d, 0.00378125d, 0.0045625d, 0.0050625d, 0.00534375d, - 0.00546875d, 0.0055d, 0.0055d, 0.0055d }; - double[] speed = { 0.03d, 0.0275d, 0.02375d, 0.01875d, 0.0125d, 0.0075d, 0.00375d, 0.00125d, 0d, 0d, 0d }; - double[] acceleration = { -0.05d, -0.075d, -0.10d, -0.125d, -0.10d, -0.075d, -0.05d, -0.025d, 0d, 0d, 0d }; - double[] jerk = { -0.5d, -0.5d, -0.5d, 0.5d, 0.5d, 0.5d, 0.5d, 0.5d, 0d, 0d }; - return new LongitudinalCandidate(times, pathS, speed, acceleration, jerk); - } - - private static EmTrajectory Replace(EmTrajectory trajectory, int index, EmTrajectoryPoint replacement) - { - var points = new List(trajectory.Points); - points[index] = replacement; - return new EmTrajectory(trajectory.Metadata, points); - } - - private static EmTrajectory CorruptDouble(EmTrajectory trajectory, int index, string propertyName, double value) - { - EmTrajectoryPoint replacement = Clone(trajectory.Points[index]); - FieldInfo field = typeof(EmTrajectoryPoint).GetField("<" + propertyName + ">k__BackingField", - BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("Missing backing field " + propertyName); - field.SetValue(replacement, value); - return Replace(trajectory, index, replacement); - } - - private static EmTrajectoryPoint Clone(EmTrajectoryPoint point, double? x = null, double? y = null, double? yaw = null, - double? timeFromStart = null, double? signedSpeed = null, double? pathS = null, double? vehicleCurvature = null, - EmBoundaryType? boundaryType = null) - { - return new EmTrajectoryPoint(x ?? point.X, y ?? point.Y, yaw ?? point.Yaw, signedSpeed ?? point.SignedLongitudinalVelocity, - timeFromStart ?? point.TimeFromStart, vehicleCurvature ?? point.VehicleCurvature, point.SegmentIndex, - pathS ?? point.SegmentLocalS, pathS ?? point.PathS, point.Direction, boundaryType ?? point.BoundaryType, - 0d, 0d); - } - - private sealed class ValidationContext - { - public ValidationContext(EmPlannerConfiguration configuration, VehicleParameters vehicle, PlanningGridMap emptyMap, - PlanningGridMap poseCollisionMap, PlanningGridMap sweptCollisionMap) - { - Configuration = configuration; - Vehicle = vehicle; - EmptyMap = emptyMap; - PoseCollisionMap = poseCollisionMap; - SweptCollisionMap = sweptCollisionMap; - } - - public EmPlannerConfiguration Configuration { get; } - public VehicleParameters Vehicle { get; } - public PlanningGridMap EmptyMap { get; } - public PlanningGridMap PoseCollisionMap { get; } - public PlanningGridMap SweptCollisionMap { get; } - } - - private static void VerifyKinematicFields(EmTrajectory trajectory, TravelDirection direction, string name) - { - double directionSign = direction == TravelDirection.Forward ? 1d : -1d; - double previousTime = double.NegativeInfinity; - double previousPathS = double.NegativeInfinity; - for (int index = 0; index < trajectory.Points.Count; index++) - { - EmTrajectoryPoint point = trajectory.Points[index]; - Verification.NearlyEqual(Math.Abs(point.SignedLongitudinalVelocity), point.Speed, - name + " speed field " + index); - Verification.NearlyEqual(point.SignedLongitudinalVelocity * Math.Cos(point.Yaw), point.VelocityX, - name + " velocity X field " + index); - Verification.NearlyEqual(point.SignedLongitudinalVelocity * Math.Sin(point.Yaw), point.VelocityY, - name + " velocity Y field " + index); - Verification.NearlyEqual(point.SignedLongitudinalVelocity * point.VehicleCurvature, point.YawRate, - name + " yaw-rate field " + index); - if (index < 4) - Verification.NearlyEqual(directionSign * CreateLongitudinalResult().Candidate.U[index], - point.SignedLongitudinalVelocity, name + " signed speed field " + index); - Verification.True(point.TimeFromStart > previousTime, name + " time strictly increases " + index); - Verification.True(point.PathS >= previousPathS, name + " PathS never decreases " + index); - previousTime = point.TimeFromStart; - previousPathS = point.PathS; - } - } - - private static void VerifyTerminalAndHold(EmTrajectory trajectory, EmBoundaryType terminalBoundary, string name) - { - const int terminalIndex = 3; - EmTrajectoryPoint terminal = trajectory.Points[terminalIndex]; - Verification.Equal(terminalBoundary, terminal.BoundaryType, name + " exact terminal boundary type"); - Verification.NearlyEqual(0d, terminal.SignedLongitudinalVelocity, name + " exact terminal signed speed"); - Verification.NearlyEqual(0d, terminal.YawRate, name + " exact terminal yaw rate"); - Verification.NearlyEqual(0.15d, terminal.TimeFromStart, name + " exact terminal stabilization time"); - Verification.Equal(terminalIndex + 6, trajectory.Points.Count, name + " terminal plus 0.20-second hold samples"); - - for (int index = terminalIndex + 1; index < trajectory.Points.Count; index++) - { - EmTrajectoryPoint hold = trajectory.Points[index]; - Verification.NearlyEqual(terminal.TimeFromStart + (index - terminalIndex) * 0.05d, - hold.TimeFromStart, name + " hold timing " + index); - Verification.NearlyEqual(terminal.X, hold.X, name + " hold X " + index); - Verification.NearlyEqual(terminal.Y, hold.Y, name + " hold Y " + index); - Verification.NearlyEqual(terminal.Yaw, hold.Yaw, name + " hold yaw " + index); - Verification.NearlyEqual(0d, hold.SignedLongitudinalVelocity, name + " hold signed speed " + index); - Verification.NearlyEqual(0d, hold.YawRate, name + " hold yaw rate " + index); - } - } - - private static LateralPath CreatePath(TravelDirection direction, double firstYaw, double lastYaw) - { - return new LateralPath(new[] - { - new LateralPathPoint(0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, firstYaw, 0d, 0.5d, 0d), - new LateralPathPoint(1d, 0.12d, 0d, 0d, 0d, 0d, 0.12d, 0d, lastYaw, 0d, 0.5d, 0d), - }, true); - } - - private static LongitudinalPlanningResult CreateLongitudinalResult() - { - var candidate = new LongitudinalCandidate( - new[] { 0d, 0.05d, 0.10d, 0.15d, 0.20d }, - new[] { 0d, 0.04d, 0.08d, 0.12d, 0.12d }, - new[] { 0.8d, 0.8d, 0.2d, 0d, 0d }, - new[] { 0d, 0d, 0d, 0d, 0d }, - new[] { 0d, 0d, 0d, 0d }); - return new LongitudinalPlanningResult(EmPlanningStatus.Success, candidate, string.Empty); - } - - private static LongitudinalPlanningResult CreateRollingLongitudinalResult() - { - var times = new double[21]; - var pathS = new double[21]; - var speed = new double[21]; - var acceleration = new double[21]; - var jerk = new double[20]; - for (int index = 0; index < times.Length; index++) - { - times[index] = index * 0.1d; - pathS[index] = index * 0.004d; - speed[index] = 0.04d; - } - return new LongitudinalPlanningResult(EmPlanningStatus.Success, - new LongitudinalCandidate(times, pathS, speed, acceleration, jerk), string.Empty); - } - - private static void VerifiesFullScopeNormalizesToleranceSizedPathSResiduals() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.OutputTimeStepSeconds = 0.1d; - var candidate = new LongitudinalCandidate( - new[] { 0d, 0.1d, 0.2d, 0.3d }, - new[] { 0d, 0.05d, 0.05d - 0.5d * configuration.Validation.KinematicTolerance, 0.10d }, - new[] { 0.5d, 0d, 0d, 0d }, - new[] { 0d, 0d, 0d, 0d }, - new[] { 0d, 0d, 0d }); - - EmTrajectory trajectory = new EmTrajectoryAssembler(configuration).Assemble( - CreatePath(TravelDirection.Forward, 0d, 0d), - new LongitudinalPlanningResult(EmPlanningStatus.Success, candidate, string.Empty), - CreateMetadata(TravelDirection.Forward, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, EmPlanningScope.FullDirectionSegment)); - - for (int index = 1; index < trajectory.Points.Count; index++) - { - Verification.True(trajectory.Points[index].PathS >= trajectory.Points[index - 1].PathS, - "full-scope numerical residual is normalized at sample " + index); - } - Verification.True(trajectory.Points[trajectory.Points.Count - 1].PathS <= candidate.S[candidate.S.Count - 1], - "full-scope publication remains within terminal PathS"); - } - - private static void VerifiesFullScopeRejectsMaterialPathSRegressionWithDiagnostics() - { - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.OutputTimeStepSeconds = 0.1d; - var candidate = new LongitudinalCandidate( - new[] { 0d, 0.1d, 0.2d, 0.3d }, - new[] { 0d, 0.05d, 0.04d, 0.10d }, - new[] { 0.5d, 0d, 0d, 0d }, - new[] { 0d, 0d, 0d, 0d }, - new[] { 0d, 0d, 0d }); - - try - { - new EmTrajectoryAssembler(configuration).Assemble( - CreatePath(TravelDirection.Forward, 0d, 0d), - new LongitudinalPlanningResult(EmPlanningStatus.Success, candidate, string.Empty), - CreateMetadata(TravelDirection.Forward, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, EmPlanningScope.FullDirectionSegment)); - throw new InvalidOperationException("Material PathS regression was accepted."); - } - catch (ArgumentException exception) - { - Verification.True(exception.Message.Contains("sampleIndex=2"), - "PathS diagnostic contains sample index"); - Verification.True(exception.Message.Contains("previousPathS="), - "PathS diagnostic contains previous value"); - Verification.True(exception.Message.Contains("candidatePathS="), - "PathS diagnostic contains candidate value"); - Verification.True(exception.Message.Contains("difference="), - "PathS diagnostic contains difference"); - } - } - - private static EmTrajectoryMetadata CreateMetadata(TravelDirection direction, EmTerminalType terminalType, - EmLongitudinalMode longitudinalMode = EmLongitudinalMode.ExactStopAtBoundary, - EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) - { - return new EmTrajectoryMetadata("trajectory", DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch, 3L, - "reference", 4L, string.Empty, 2, direction, terminalType, longitudinalMode, - planningScope); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs deleted file mode 100644 index dee1db2..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +++ /dev/null @@ -1,1444 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.EMPlanner; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; -using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; -using TrajectoryPlanningVisualization; - -namespace EMPlannerVerificationHost; - -internal static class TrajectoryObservationChecks -{ - public static void Run() - { - TrajectoryObservationSettingsChecks.Run(); - TrajectoryObservationSegmentChecks.Run(); - VerifiesTrajectoryObservationReportWriterCreatesTxt(); - VerifiesPainterPoseGeometryReplacesLargeArrow(); - TrajectoryObservationVisualizationChecks.Run(); - VerifiesObservationSourceHasNoActuatorCalls(); - VerifiesObservationSourceUsesRequiredOperatorText(); - VerifiesOperatorDocumentationUsesExactUiEntry(); - VerifiesMovementTestVehicleInputsAndSettingsSnapshot(); - VerifiesMovementTestKeepsNativePainterLazyAndConditional(); - VerifiesPlanningConfigurationDiagnosticUsesEffectiveConfiguration(); - RejectsInvalidObservationSettings(); - VerifiesStartGoalBoundsUseOnlyConfiguredPadding(); - VerifiesValidNonEmptyObstacleSource(); - RejectsObstacleOutsideConfiguredBounds(); - VerifiesObserverTicksWhilePlanningIsDelayed(); - VerifiesFullDirectionSegmentPlansOncePerActiveSegment(); - VerifiesFullDirectionRequestCarriesValidatedScope(); - VerifiesFullDirectionPreplansAfterStopAndActivatesAfterConfirmation(); - VerifiesDeterministicSingleForwardObservationSimulation(); - VerifiesFailedFullPlanDoesNotAutoRetry(); - VerifiesStaticSnapshotExportsPlanningScope(); - VerifiesSessionLayerCleanupDecisions(); - VerifiesGearSwitchWaitStateForWorldPresentation(); - VerifiesLsAndStUsePublishedTrajectoryData(); - VerifiesPresentationTextDescribesObservationWithoutSendingCommand(); - VerifiesPlanningDiagnosticsKeepRawFailureReason(); - VerifiesPublishedPlanningDiagnosticsIncludeTrajectorySummary(); - VerifiesCurveReportIncludesLsStSpeedAccelerationAndJerk(); - VerifiesEmptyChartsReceivePersistentPlanningDiagnostic(); - VerifiesLsPresentationUsesPathSOnHorizontalAxis(); - VerifiesPainterStAxesUsePathSAndTimeUnits(); - VerifiesRollingRequestUsesOnePublishedTrajectorySnapshot(); - VerifiesActiveSegmentControllerDoesNotCrossSeedTrajectories(); - VerifiesLoopDefersSegmentAdvanceUntilPlanningIsConsumed(); - FreezesBootstrapVehicleForRollingRequests(); - VerifiesEmptyMapBootstrapProducesPublishableReference(); - } - - private static void VerifiesObservationSourceHasNoActuatorCalls() - { - string sourceDirectory = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest"); - string movementTestSource = Path.Combine(sourceDirectory, "MovementTest.TrajectoryObservationTest.cs"); - Verification.True(File.Exists(movementTestSource), "observation MovementTest source exists"); - - string[] forbiddenTokens = - { - ".SendXYThSpeed(", ".SendMotion(", ".SendTh(", ".AccumulateSpeed(", - ".ComputeWheelsGeometrically(", ".DriveStop(", ".PredefinedDriveStop(" - }; - string[] runtimeSources = Directory.GetFiles(sourceDirectory, "*.cs", SearchOption.TopDirectoryOnly); - for (int sourceIndex = 0; sourceIndex < runtimeSources.Length; sourceIndex++) - { - string source = File.ReadAllText(runtimeSources[sourceIndex]); - for (int tokenIndex = 0; tokenIndex < forbiddenTokens.Length; tokenIndex++) - { - Verification.True(source.IndexOf(forbiddenTokens[tokenIndex], StringComparison.Ordinal) < 0, - "observation runtime source excludes actuator token " + forbiddenTokens[tokenIndex]); - } - } - } - - private static void VerifiesTrajectoryObservationReportWriterCreatesTxt() - { - string reportDirectory = Path.Combine(Path.GetTempPath(), - "em-trajectory-report-" + Guid.NewGuid().ToString("N")); - try - { - var writer = new TrajectoryObservationReportWriter(17L, reportDirectory); - writer.Append("cycle=1 status=SolverTimedOut primal=140.918"); - string reportPath = writer.Save(); - - Verification.True(File.Exists(reportPath), "trajectory observation txt report exists"); - string text = File.ReadAllText(reportPath); - Verification.True(text.Contains("session=17"), "report includes session identity"); - Verification.True(text.Contains("OBSERVE_ONLY"), "report records observation-only boundary"); - Verification.True(text.Contains("SolverTimedOut"), "report includes cycle failure"); - } - finally - { - if (Directory.Exists(reportDirectory)) - Directory.Delete(reportDirectory, true); - } - } - - private static void VerifiesObservationSourceUsesRequiredOperatorText() - { - string movementTestSource = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(movementTestSource)); - - Verification.True(source.Contains("[MovementTest(name = \"EM轨迹规划观察闭环测试\")]"), - "observation MovementTest uses required Chinese display name"); - Verification.Equal("等待真实档位/方向确认;观察模式不会激活下一方向段", - TrajectoryObservationRuntimeState.GearSwitchWaitingNotice, - "observation runtime uses required gear-switch notice"); - Verification.True(source.Contains("Console.WriteLine(\"[TrajectoryObserver] \" + text);"), - "observer status is mirrored to the host terminal"); - } - - private static void VerifiesOperatorDocumentationUsesExactUiEntry() - { - string sourceDirectory = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest"); - string movementTestSource = Path.Combine(sourceDirectory, "MovementTest.TrajectoryObservationTest.cs"); - string readmePath = Path.Combine(sourceDirectory, "README.md"); - string movementTestText = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(movementTestSource)); - string readmeText = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(readmePath)); - const string uiEntry = "EM轨迹规划观察闭环测试"; - - Verification.True(movementTestText.Contains("[MovementTest(name = \"" + uiEntry + "\")]"), - "observation MovementTest has the exact UI entry"); - Verification.True(readmeText.Contains(uiEntry), - "observation README has the exact UI entry"); - } - - private static void VerifiesMovementTestVehicleInputsAndSettingsSnapshot() - { - string movementTestSource = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(movementTestSource)); - Verification.True(source.Contains("public double VehicleLengthMeters = 0.80d;"), - "observer MovementTest exposes vehicle length with Task-1 default"); - Verification.True(source.Contains("public double VehicleWidthMeters = 0.60d;"), - "observer MovementTest exposes vehicle width with Task-1 default"); - Verification.True(source.Contains("public double SafetyMarginMeters = 0.05d;"), - "observer MovementTest exposes safety margin with Task-1 default"); - Verification.True(source.Contains("public double MaximumCurvaturePerMeter = 1d / 1.20d;"), - "observer MovementTest exposes maximum curvature with Task-1 default"); - Verification.True(source.Contains("public double SolverTimeoutSeconds ="), - "observer MovementTest exposes a configurable solver timeout"); - Verification.True(source.Contains("public int MaximumOsqpIterations ="), - "observer MovementTest exposes a configurable OSQP iteration limit"); - Verification.True(source.Contains("public double TimeHorizonSeconds ="), - "observer MovementTest exposes a configurable ST time horizon"); - Verification.True(source.Contains("public double OutputTimeStepSeconds ="), - "observer MovementTest exposes a configurable ST timestamp spacing"); - Verification.True(source.Contains("不等于观察循环周期"), - "observer MovementTest documents the distinction between output timestamps and observer ticks"); - Verification.True(source.Contains("VehicleLengthMeters = VehicleLengthMeters,"), - "observer MovementTest copies vehicle length into settings"); - Verification.True(source.Contains("VehicleWidthMeters = VehicleWidthMeters,"), - "observer MovementTest copies vehicle width into settings"); - Verification.True(source.Contains("SafetyMarginMeters = SafetyMarginMeters,"), - "observer MovementTest copies safety margin into settings"); - Verification.True(source.Contains("MaximumCurvaturePerMeter = MaximumCurvaturePerMeter,"), - "observer MovementTest copies maximum curvature into settings"); - Verification.True(source.Contains("SolverTimeoutSeconds = SolverTimeoutSeconds,"), - "observer MovementTest copies solver timeout into settings"); - Verification.True(source.Contains("MaximumOsqpIterations = MaximumOsqpIterations,"), - "observer MovementTest copies OSQP iteration limit into settings"); - Verification.True(source.Contains("TimeHorizonSeconds = TimeHorizonSeconds,"), - "observer MovementTest copies ST time horizon into settings"); - Verification.True(source.Contains("OutputTimeStepSeconds = OutputTimeStepSeconds,"), - "observer MovementTest copies ST timestamp spacing into settings"); - string normalizedSource = source.Replace("\r\n", "\n"); - Verification.True(normalizedSource.Contains( - "VehicleMotionState state = ReadVehicleState();\n DateTimeOffset now = state.CapturedAtUtc;"), - "observer host uses the fresh state snapshot time for each observation tick"); - Verification.True(source.Contains("effectiveConfiguration.Frenet.MaximumProjectionDistanceMeters"), - "native observation charts use the EM Frenet projection tolerance rather than map padding"); - Verification.True(source.Contains("tick.SegmentState.Phase == TrajectoryObservationSegmentPhase.Completed"), - "observer session terminates after the final direction segment completes"); - Verification.NearlyEqual(0.10d, new TrajectoryObservationSettings().OutputTimeStepSeconds, - "observer settings use the ST timestamp-spacing default"); - - var configured = new TrajectoryObservationSettings - { - VehicleLengthMeters = 1.10d, - VehicleWidthMeters = 0.70d, - SafetyMarginMeters = 0.08d, - MaximumCurvaturePerMeter = 0.55d, - SolverTimeoutSeconds = 0.42d, - MaximumOsqpIterations = 9000, - TimeHorizonSeconds = 4d, - OutputTimeStepSeconds = 0.20d, - }; - TrajectoryObservationSettings snapshot = configured.CreateValidatedSnapshot(); - configured.VehicleLengthMeters = 9.10d; - configured.VehicleWidthMeters = 9.20d; - configured.SafetyMarginMeters = 9.30d; - configured.MaximumCurvaturePerMeter = 9.40d; - configured.SolverTimeoutSeconds = 9.50d; - configured.MaximumOsqpIterations = 9500; - configured.TimeHorizonSeconds = 5d; - configured.OutputTimeStepSeconds = 0.25d; - - Verification.NearlyEqual(1.10d, snapshot.VehicleLengthMeters, - "observer settings snapshot freezes vehicle length"); - Verification.NearlyEqual(0.70d, snapshot.VehicleWidthMeters, - "observer settings snapshot freezes vehicle width"); - Verification.NearlyEqual(0.08d, snapshot.SafetyMarginMeters, - "observer settings snapshot freezes safety margin"); - Verification.NearlyEqual(0.55d, snapshot.MaximumCurvaturePerMeter, - "observer settings snapshot freezes maximum curvature"); - Verification.NearlyEqual(0.42d, snapshot.SolverTimeoutSeconds, - "observer settings snapshot freezes solver timeout"); - Verification.Equal(9000, snapshot.MaximumOsqpIterations, - "observer settings snapshot freezes OSQP iteration limit"); - Verification.NearlyEqual(4d, snapshot.TimeHorizonSeconds, - "observer settings snapshot freezes ST time horizon"); - Verification.NearlyEqual(0.20d, snapshot.OutputTimeStepSeconds, - "observer settings snapshot freezes ST timestamp spacing"); - - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), snapshot, - Array.Empty(), 0L); - AssertVehicleSnapshot(job.Vehicle, 1.10d, 0.70d, 0.08d, 0.55d, null, - "observer configured bootstrap vehicle"); - } - - private static void VerifiesPlanningConfigurationDiagnosticUsesEffectiveConfiguration() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 5, 0, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - ReplanPeriodSeconds = 0.25d, - SolverTimeoutSeconds = 1.25d, - MaximumOsqpIterations = 54321, - TimeHorizonSeconds = 3.5d, - OutputTimeStepSeconds = 0.10d, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "observer configuration-diagnostic bootstrap succeeds"); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedTrajectoryPlanningService(CreatePublishedTrajectory(effectiveAt)), "config-diagnostic"); - - string text = controller.CreateConfigurationDiagnostic().Text; - MethodInfo effectiveConfigurationMethod = typeof(TrajectoryObservationController).GetMethod( - "CreateEffectiveConfigurationSnapshot", BindingFlags.Instance | BindingFlags.NonPublic)!; - var effectiveConfiguration = (EmPlannerConfiguration)effectiveConfigurationMethod.Invoke( - controller, Array.Empty())!; - string distanceHorizonText = "distanceHorizon=" + - effectiveConfiguration.Scheduling.DistanceHorizonMeters.ToString("F2", CultureInfo.InvariantCulture) + "m"; - foreach (string expected in new[] - { - "planning configuration:", "timeHorizon=3.50s", distanceHorizonText, "outputTimeStep=0.10s", - "outputFrequency=10.00Hz", "trajectoryKnots=36", "maximumOsqpIterations=54321", - "solverTimeout=1.25s", "replanPeriod=0.25s", "maximumJerkLimitedStopDistance=", - "maximumJerkLimitedStopDuration=", "requiredDistanceHorizon=", - }) - { - Verification.True(text.Contains(expected), "configuration diagnostic includes " + expected); - } - - string movementTestSource = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); - string runnerSource = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(movementTestSource)); - int firstCall = runnerSource.IndexOf("CreateConfigurationDiagnostic()", StringComparison.Ordinal); - Verification.True(firstCall >= 0, "runner prints configuration diagnostic"); - Verification.Equal(-1, runnerSource.IndexOf("CreateConfigurationDiagnostic()", firstCall + 1, - StringComparison.Ordinal), "runner prints configuration diagnostic once"); - } - - private static void RejectsInvalidObservationSettings() - { - AssertInvalidSetting(settings => settings.MapPaddingMeters = 0d, "map padding"); - AssertInvalidSetting(settings => settings.MapResolutionMillimeters = float.NaN, "map resolution"); - AssertInvalidSetting(settings => settings.ReplanPeriodSeconds = 0d, "replan period"); - AssertInvalidSetting(settings => settings.ObserverPeriodSeconds = double.PositiveInfinity, - "observer period"); - AssertInvalidSetting(settings => settings.VehicleLengthMeters = 0d, "vehicle length"); - AssertInvalidSetting(settings => settings.VehicleWidthMeters = double.NaN, "vehicle width"); - AssertInvalidSetting(settings => settings.SafetyMarginMeters = 0d, "safety margin"); - AssertInvalidSetting(settings => settings.MaximumCurvaturePerMeter = double.PositiveInfinity, - "maximum curvature"); - AssertInvalidSetting(settings => settings.SolverTimeoutSeconds = 0d, "solver timeout"); - AssertInvalidSetting(settings => settings.MaximumOsqpIterations = 0, "OSQP iteration limit"); - AssertInvalidSetting(settings => settings.MaximumOsqpIterations = -1, "negative OSQP iteration limit"); - AssertInvalidSetting(settings => settings.TimeHorizonSeconds = 0d, "ST time horizon"); - AssertInvalidSetting(settings => settings.OutputTimeStepSeconds = 0d, "ST timestamp spacing"); - AssertInvalidSetting(settings => - { - settings.TimeHorizonSeconds = 1d; - settings.OutputTimeStepSeconds = 1.01d; - }, "ST timestamp spacing larger than the horizon"); - } - - private static void AssertInvalidSetting(Action mutate, string name) - { - var settings = new TrajectoryObservationSettings(); - mutate(settings); - Verification.True(Throws(settings.Validate), "observer rejects invalid " + name); - } - - private static void VerifiesStartGoalBoundsUseOnlyConfiguredPadding() - { - var settings = new TrajectoryObservationSettings - { - MapPaddingMeters = 2d, - MapResolutionMillimeters = 50f, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(10d, -5d, 0d), new Pose2D(13d, -1d, 0d), settings, - Array.Empty(), 17L); - - Verification.NearlyEqual(8000d, job.MapRequest.Bounds.XMin, "observer map x min"); - Verification.NearlyEqual(15000d, job.MapRequest.Bounds.XMax, "observer map x max"); - Verification.NearlyEqual(-7000d, job.MapRequest.Bounds.YMin, "observer map y min"); - Verification.NearlyEqual(1000d, job.MapRequest.Bounds.YMax, "observer map y max"); - Verification.NearlyEqual(50d, job.MapRequest.ResolutionMm, "observer map resolution"); - } - - private static void VerifiesValidNonEmptyObstacleSource() - { - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 1d, 0d), new TrajectoryObservationSettings(), - new[] { TrajectoryObservationObstacle.Circle(500d, 500d, 100d) }, 23L); - - Verification.True(!job.MapRequest.AllowExplicitEmptyMap, - "observer non-empty obstacle map is not explicitly empty"); - Verification.Equal(1, job.MapRequest.ObstacleSources.Count, - "observer non-empty map has exactly one obstacle source"); - IMapObstacleSource source = job.MapRequest.ObstacleSources[0]; - Verification.Equal("trajectory-observer-manual", source.SourceId, - "observer manual obstacle source ID"); - Verification.Equal(23L, source.SourceVersion, - "observer manual obstacle source version"); - Verification.True(source.IsRequired, "observer manual obstacle source is required"); - ObstacleProjectionResult projection = source.ProjectToWorld(); - Verification.Equal(ObstacleSourceStatus.Applied, projection.Status, - "observer valid manual obstacle source applies"); - Verification.Equal(1, projection.Obstacles.Count, - "observer valid manual obstacle source preserves geometry"); - } - - private static void RejectsObstacleOutsideConfiguredBounds() - { - Verification.True(Throws(() => TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 1d, 0d), new TrajectoryObservationSettings(), - new[] { TrajectoryObservationObstacle.Rectangle(-2100d, -2000d, 0d, 100d) }, 1L)), - "observer obstacle outside configured bounds"); - } - - private static void VerifiesObserverTicksWhilePlanningIsDelayed() - { - DateTimeOffset startedAt = new DateTimeOffset(2026, 8, 4, 3, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.RollingHorizon, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "observer delayed-planner bootstrap succeeds"); - - EmTrajectory published = CreatePublishedTrajectory(startedAt); - var planningService = new DelayedTrajectoryPlanningService(published, 2); - try - { - var controller = new TrajectoryObservationController( - bootstrap, settings, planningService, "cadence-check"); - var loop = new TrajectoryObservationLoop(controller); - var firstState = new VehicleMotionState( - new Pose2D(0.10d, 0d, 0d), 0.10d, null, startedAt, 10L); - - TrajectoryObservationLoopTick firstTick = loop.Tick(startedAt, firstState, CancellationToken.None); - - Verification.True(firstTick.PlanningStarted, "observer first tick reports a planning-cycle start"); - Verification.True(!firstTick.PlanningCompleted, "observer first tick has no completed cycle"); - Verification.True(firstTick.ShouldLog, - "observer first cadence tick is eligible for session-guarded logging"); - Verification.Equal(10L, firstTick.Observation.VehicleState.SequenceId, - "observer first tick uses first fresh state"); - - TrajectoryObservationLoopTick? initialPublicationTick = null; - bool initiallyPublished = SpinWait.SpinUntil(() => - { - var currentState = new VehicleMotionState( - new Pose2D(0.15d, 0d, 0d), 0.15d, null, - startedAt.AddSeconds(0.10d), 11L); - initialPublicationTick = loop.Tick(startedAt.AddSeconds(0.10d), currentState, - CancellationToken.None); - return !initialPublicationTick.PlanningInFlight; - }, TimeSpan.FromSeconds(5d)); - Verification.True(initiallyPublished && initialPublicationTick != null && - ReferenceEquals(published, initialPublicationTick.Observation.PublishedTrajectory), - "observer establishes a published trajectory before delayed rolling planning"); - - DateTimeOffset replanAt = startedAt.AddSeconds(settings.ReplanPeriodSeconds); - var replanState = new VehicleMotionState( - new Pose2D(0.20d, 0d, 0d), 0.20d, null, replanAt, 12L); - TrajectoryObservationLoopTick replanTick = loop.Tick(replanAt, replanState, - CancellationToken.None); - Verification.True(replanTick.PlanningInFlight, - "observer delayed rolling planning remains in flight after replan tick"); - Verification.True(planningService.WaitUntilEntered(TimeSpan.FromSeconds(5d)), - "observer delayed planner enters planning service"); - - DateTimeOffset secondAt = replanAt.AddSeconds(settings.ObserverPeriodSeconds); - var secondState = new VehicleMotionState( - new Pose2D(0.25d, 0d, 0d), 0.25d, null, secondAt, 13L); - TrajectoryObservationLoopTick secondTick = loop.Tick(secondAt, secondState, CancellationToken.None); - - Verification.True(secondTick.PlanningInFlight, - "observer second tick does not wait for delayed planning"); - Verification.True(secondTick.ShouldLog, - "observer second cadence tick is eligible for session-guarded logging"); - Verification.True(ReferenceEquals(published, secondTick.Observation.PublishedTrajectory), - "observer keeps observing the existing publication during delayed rolling planning"); - Verification.Equal(13L, secondTick.Observation.VehicleState.SequenceId, - "observer second tick uses second fresh state"); - Verification.Equal(secondAt, secondTick.Observation.ObservedAtUtc, - "observer second tick observes at its own time"); - - planningService.Release(); - TrajectoryObservationLoopTick? completedTick = null; - bool completed = SpinWait.SpinUntil(() => - { - var currentState = new VehicleMotionState( - new Pose2D(0.30d, 0d, 0d), 0.30d, null, - replanAt.AddSeconds(0.10d), 14L); - completedTick = loop.Tick(replanAt.AddSeconds(0.10d), currentState, - CancellationToken.None); - return !completedTick.PlanningInFlight; - }, TimeSpan.FromSeconds(5d)); - - Verification.True(completed, "observer delayed planning completes deterministically"); - Verification.True(completedTick != null, "observer delayed planning produces a completion tick"); - TrajectoryObservationLoopTick finalTick = completedTick!; - Verification.True(finalTick.PlanningCompleted, "observer completion tick reports cycle completion"); - Verification.True(finalTick.LatestCycle != null && finalTick.LatestCycle.Published, - "observer delayed planning result is consumed without a continuation"); - Verification.Equal(14L, finalTick.Observation.VehicleState.SequenceId, - "observer post-plan observation does not reuse pre-plan state"); - Verification.Equal(replanAt.AddSeconds(0.10d), finalTick.Observation.ObservedAtUtc, - "observer post-plan observation does not reuse pre-plan time"); - } - finally - { - planningService.Release(); - } - } - - private static void VerifiesFullDirectionSegmentPlansOncePerActiveSegment() - { - DateTimeOffset startedAt = new DateTimeOffset(2026, 8, 7, 0, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings(); - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "one-shot bootstrap succeeds"); - - EmTrajectory published = CreatePublishedTrajectory(startedAt); - var planningService = new FixedTrajectoryPlanningService(published); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "one-shot"); - var loop = new TrajectoryObservationLoop(controller); - controller.StartCycle(startedAt, - new VehicleMotionState(new Pose2D(0.10d, 0d, 0d), 0.10d, null, startedAt, 1L), - CancellationToken.None).GetAwaiter().GetResult(); - - TrajectoryObservationLoopTick? lastTick = null; - for (int index = 0; index < 20; index++) - { - DateTimeOffset now = startedAt.AddSeconds((index + 1) * settings.ObserverPeriodSeconds); - lastTick = loop.Tick(now, - new VehicleMotionState(new Pose2D(0.10d + index * 0.01d, 0d, 0d), 0.10d, null, - now, 2L + index), CancellationToken.None); - } - - Verification.Equal(1, planningService.Requests.Count, - "full direction segment is planned only once per active segment"); - Verification.True(lastTick != null && !lastTick.PlanningStarted, - "no coordinator-cadence replan after the full segment plan"); - Verification.True(lastTick != null && lastTick.Observation != null && - ReferenceEquals(published, lastTick.Observation.PublishedTrajectory), - "observation continues to publish the frozen full-segment trajectory"); - } - - private static void VerifiesFullDirectionRequestCarriesValidatedScope() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 7, 1, 0, 0, TimeSpan.Zero); - var source = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.FullDirectionSegment, - }; - TrajectoryObservationSettings snapshot = source.CreateValidatedSnapshot(); - source.PlanningScope = EmPlanningScope.RollingHorizon; - - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), snapshot, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "scope propagation bootstrap succeeds"); - - EmTrajectory published = CreatePublishedTrajectory(effectiveAt); - var planningService = new FixedTrajectoryPlanningService(published); - var controller = new TrajectoryObservationController(bootstrap, snapshot, planningService, "scope-check"); - snapshot.PlanningScope = EmPlanningScope.RollingHorizon; - var state = new VehicleMotionState(new Pose2D(0.10d, 0d, 0d), 0.10d, null, effectiveAt, 1L); - controller.StartCycle(effectiveAt, state, CancellationToken.None).GetAwaiter().GetResult(); - - Verification.Equal(EmPlanningScope.FullDirectionSegment, planningService.Requests[0].PlanningScope, - "controller propagates the validated full-direction scope into EM requests"); - Verification.True(!controller.ShouldStartCycle(effectiveAt.AddSeconds(1d)), - "full scope blocks coordinator-cadence restart after the first plan"); - } - - private static void VerifiesFullDirectionPreplansAfterStopAndActivatesAfterConfirmation() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 7, 2, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - DirectionConfirmationSamples = 1, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult baseBootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(baseBootstrap.Succeeded, "one-shot transition bootstrap succeeds"); - TrajectoryObservationBootstrapResult bootstrap = TrajectoryObservationBootstrapResult.Success( - baseBootstrap.Job, baseBootstrap.CoarseResult, baseBootstrap.SmoothedPath, CreateDirectionalSegments()); - - EmTrajectory forwardGearTrajectory = CreateSegmentTrajectory(t0, "one-shot-forward", 0, - TravelDirection.Forward, EmTerminalType.GearSwitch, EmBoundaryType.GearSwitchApproach, 1d); - EmTrajectory reverseTrajectory = CreateSegmentTrajectory(t0.AddSeconds(1d), "one-shot-reverse", 1, - TravelDirection.Reverse, EmTerminalType.Goal, EmBoundaryType.Goal, 0d); - var planningService = new SequenceTrajectoryPlanningService(forwardGearTrajectory, reverseTrajectory, - reverseTrajectory); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "one-shot-advance"); - var stoppedState = new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0, 1L); - controller.StartCycle(t0, stoppedState, CancellationToken.None).GetAwaiter().GetResult(); - - controller.TryAdvanceSegment(t0, stoppedState); - DateTimeOffset stoppedAt = t0.AddSeconds(0.21d); - var heldStoppedState = new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, stoppedAt, 2L); - controller.TryAdvanceSegment(stoppedAt, heldStoppedState); - Verification.True(controller.ShouldStartCycle(stoppedAt), - "the next full direction segment is planned after the real stop hold, before direction evidence"); - - PlanningCycleResult pendingCycle = controller.StartCycle(stoppedAt, heldStoppedState, CancellationToken.None) - .GetAwaiter().GetResult(); - Verification.True(pendingCycle.Published, "the stopped-state pending segment plan is independently publishable"); - Verification.Equal(2, planningService.Requests.Count, - "the next segment is planned exactly once from the stopped state"); - Verification.Equal(1, planningService.Requests[1].SegmentIndex, - "the stopped-state pending request targets segment N+1"); - Verification.Equal(heldStoppedState.SequenceId, planningService.Requests[1].VehicleState.SequenceId, - "the pending segment request uses the real stopped vehicle state"); - Verification.True(planningService.Requests[1].PreviousTrajectory == null, - "a pending direction segment never reuses the old direction trajectory as an EM seed"); - Verification.Equal(0, controller.ActiveSegment.SegmentIndex, - "planning N+1 does not replace the old segment before direction confirmation"); - Verification.True(ReferenceEquals(forwardGearTrajectory, controller.PublishedTrajectory), - "the old segment trajectory remains active while N+1 waits for confirmation"); - - var reverseState = new VehicleMotionState(new Pose2D(1d, 0d, 0d), -0.03d, null, - t0.AddSeconds(0.22d), 3L); - Verification.True(controller.TryAdvanceSegment(t0.AddSeconds(0.22d), reverseState), - "confirmed N to N+1 transition succeeds"); - Verification.Equal(1, controller.ActiveSegment.SegmentIndex, - "the pending plan becomes active only after direction confirmation"); - Verification.True(!controller.ShouldStartCycle(t0.AddSeconds(0.23d)), - "confirmed activation does not trigger a duplicate full-segment plan"); - Verification.Equal(2, planningService.Requests.Count, - "the pending plan is reused at activation instead of planning N+1 twice"); - Verification.Equal(reverseState.CapturedAtUtc, controller.PublishedTrajectory.Metadata.EffectiveAtUtc, - "pending trajectory effective time is rebased to its real activation time"); - } - - private static void VerifiesDeterministicSingleForwardObservationSimulation() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 8, 0, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.FullDirectionSegment, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult baseBootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(baseBootstrap.Succeeded, "deterministic single-forward simulation bootstrap succeeds"); - TrajectoryObservationBootstrapResult bootstrap = TrajectoryObservationBootstrapResult.Success( - baseBootstrap.Job, baseBootstrap.CoarseResult, baseBootstrap.SmoothedPath, new[] - { - CreateDirectionalSegment(0, TravelDirection.Forward, 0d, 1d, false, - EmBoundaryType.None, EmBoundaryType.Goal), - }); - EmTrajectory trajectory = CreateSingleForwardSimulationTrajectory(t0); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedTrajectoryPlanningService(trajectory), "single-forward-simulation"); - var loop = new TrajectoryObservationLoop(controller); - - double x = 0d; - double heading = 0d; - var initialState = new VehicleMotionState(new Pose2D(x, 0d, heading), 0d, null, t0, 1L); - controller.StartCycle(t0, initialState, CancellationToken.None).GetAwaiter().GetResult(); - TrajectoryControlCommand command = controller.Observe(t0, initialState).Command; - TrajectoryObservationLoopTick? finalTick = null; - const double deltaSeconds = 0.10d; - for (int step = 1; step <= 10; step++) - { - x += command.SignedLongitudinalVelocity * Math.Cos(heading) * deltaSeconds; - heading += command.YawRate * deltaSeconds; - DateTimeOffset now = t0.AddSeconds(step * deltaSeconds); - var state = new VehicleMotionState(new Pose2D(x, 0d, heading), command.SignedLongitudinalVelocity, - null, now, step + 1L); - finalTick = loop.Tick(now, state, CancellationToken.None); - command = finalTick.Observation.Command; - } - - Verification.NearlyEqual(1d, x, "simulated forward vehicle reaches the real Goal pose from executor commands"); - Verification.Equal(TrajectoryObservationSegmentPhase.Completed, finalTick!.SegmentState.Phase, - "simulated forward Goal completes the observation lifecycle"); - Verification.NearlyEqual(0d, command.SignedLongitudinalVelocity, - "simulated forward Goal receives the terminal zero-speed command"); - } - - private static void VerifiesFailedFullPlanDoesNotAutoRetry() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 7, 3, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings(); - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "failed-plan bootstrap succeeds"); - - var planningService = new FailingTrajectoryPlanningService(); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "failed-one-shot"); - var loop = new TrajectoryObservationLoop(controller); - var state = new VehicleMotionState(new Pose2D(0.10d, 0d, 0d), 0.10d, null, t0, 1L); - loop.Tick(t0, state, CancellationToken.None); - - TrajectoryObservationLoopTick? failureTick = null; - bool consumed = SpinWait.SpinUntil(() => - { - DateTimeOffset now = t0.AddSeconds(0.05d); - var current = new VehicleMotionState(new Pose2D(0.11d, 0d, 0d), 0.10d, null, now, 2L); - failureTick = loop.Tick(now, current, CancellationToken.None); - return failureTick.LatestCycle != null; - }, TimeSpan.FromSeconds(5d)); - Verification.True(consumed, "failed full plan is consumed and visible"); - Verification.Equal(1, planningService.Requests.Count, - "failed full plan makes exactly one attempt"); - Verification.True(failureTick != null && failureTick.LatestCycle != null && - !failureTick.LatestCycle.Published, - "failed full plan remains visible as unpublished"); - - TrajectoryObservationLoopTick? lastTick = null; - for (int index = 0; index < 20; index++) - { - DateTimeOffset now = t0.AddSeconds(0.10d + (index + 1) * settings.ObserverPeriodSeconds); - lastTick = loop.Tick(now, - new VehicleMotionState(new Pose2D(0.10d + index * 0.01d, 0d, 0d), 0.10d, null, - now, 3L + index), CancellationToken.None); - } - Verification.Equal(1, planningService.Requests.Count, - "failed full plan does not automatically roll into coordinator-cadence retries"); - Verification.True(lastTick != null && lastTick.Observation != null && !lastTick.PlanningStarted, - "observation continues after a failed full plan without starting retries"); - } - - private static void VerifiesStaticSnapshotExportsPlanningScope() - { - var settings = new TrajectoryObservationSettings(); - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "static scope snapshot bootstrap succeeds"); - - PlanningVisualizationStaticSnapshot snapshot = new TrajectoryObservationStaticSnapshotBuilder().Build( - bootstrap, EmPlannerConfiguration.CreateDefault(), settings.CreateValidatedSnapshot(), 0L); - Verification.True(snapshot.ConfigurationGroups.SelectMany(x => x.Entries).Any(x => - x.RawName == "PlanningScope" && x.Value == "FullDirectionSegment"), - "static snapshot exports the validated full-direction scope"); - } - - private static void VerifiesSessionLayerCleanupDecisions() - { - Verification.True(!TrajectoryObservationSessionLifecycle.ShouldClearLayers( - TrajectoryObservationSessionEndReason.BootstrapFailure), - "observer bootstrap failure preserves the diagnostic world view"); - Verification.True(TrajectoryObservationSessionLifecycle.ShouldClearLayers( - TrajectoryObservationSessionEndReason.RuntimeFault), - "observer runtime fault clears all painter layers"); - Verification.True(TrajectoryObservationSessionLifecycle.ShouldClearLayers( - TrajectoryObservationSessionEndReason.Cancellation), - "observer cancellation clears all painter layers"); - } - - private static void VerifiesGearSwitchWaitStateForWorldPresentation() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 4, 0, 0, TimeSpan.Zero); - EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt, EmTerminalType.GearSwitch); - - TrajectoryObservationRuntimeState beforeFinal = TrajectoryObservationRuntimeState.Create( - effectiveAt.AddSeconds(0.99d), trajectory); - Verification.True(!beforeFinal.WaitingAtGearSwitch, - "observer does not paint gear-switch wait state before final time"); - Verification.Equal(string.Empty, beforeFinal.WorldNotice, - "observer has no gear-switch world notice before final time"); - - TrajectoryObservationRuntimeState atFinal = TrajectoryObservationRuntimeState.Create( - effectiveAt.AddSeconds(1d), trajectory); - Verification.True(atFinal.WaitingAtGearSwitch, - "observer enters gear-switch wait state at final time"); - Verification.Equal("等待真实档位/方向确认;观察模式不会激活下一方向段", atFinal.WorldNotice, - "observer exposes the exact gear-switch state to the world painter"); - Verification.Equal(0, trajectory.Metadata.SegmentIndex, - "observer gear-switch wait state remains on segment zero"); - - var tracker = new TrajectoryObservationSegmentTracker(CreateDirectionalSegments(), - new TrajectoryObservationSettings(), 0.01d); - TrajectoryObservationRuntimeState planningState = TrajectoryObservationRuntimeState.Create(tracker.State); - Verification.True(!planningState.WaitingAtGearSwitch, - "tracker-planning state does not show a gear-switch wait notice merely because planned time elapsed"); - tracker.Update(effectiveAt.AddSeconds(1d), new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, - effectiveAt.AddSeconds(1d), 1L), - CreateSegmentTrajectory(effectiveAt, "runtime-forward", 0, TravelDirection.Forward, - EmTerminalType.GearSwitch, EmBoundaryType.GearSwitchApproach, 1d)); - TrajectoryObservationRuntimeState stoppedState = TrajectoryObservationRuntimeState.Create(tracker.State); - Verification.True(stoppedState.WaitingAtGearSwitch, - "actual tracker stop state shows the gear-switch wait notice"); - - string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); - string presentationSource = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); - Verification.True(presentationSource.Contains( - "worldPainter.DrawText(Color.OrangeRed, runtimeState.WorldNotice"), - "observer world painter draws the exact runtime wait state"); - } - - private static void VerifiesLsAndStUsePublishedTrajectoryData() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 0, 0, 0, TimeSpan.Zero); - DirectionSegmentView segment = CreateStraightSegment(); - EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt); - - TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build(trajectory, segment, 0.5d); - Verification.Equal(2, charts.StSamples.Count, "observer ST sample count"); - Verification.NearlyEqual(0d, charts.StSamples[0].TimeFromStart, "observer first ST time"); - Verification.NearlyEqual(4d, charts.StSamples[0].PathS, "observer first ST path S"); - Verification.NearlyEqual(1d, charts.StSamples[1].TimeFromStart, "observer second ST time"); - Verification.NearlyEqual(5d, charts.StSamples[1].PathS, "observer second ST path S"); - Verification.Equal(2, charts.SpeedSamples.Count, "observer speed sample count"); - Verification.NearlyEqual(0.20d, charts.SpeedSamples[0].SignedLongitudinalVelocity, - "observer first signed speed"); - Verification.NearlyEqual(0.40d, charts.SpeedSamples[1].SignedLongitudinalVelocity, - "observer second signed speed"); - Verification.Equal(2, charts.LsSamples.Count, "observer LS projection count"); - Verification.Equal(0, charts.FailedProjectionCount, "observer LS projection failure count"); - Verification.NearlyEqual(10.25d, charts.LsSamples[0].PathS, "observer first LS path S"); - Verification.NearlyEqual(0.10d, charts.LsSamples[0].LateralOffset, "observer first LS offset"); - - var settings = new TrajectoryObservationSettings - { - MaximumOsqpIterations = 9000, - TimeHorizonSeconds = 4d, - OutputTimeStepSeconds = 0.20d, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper( - new CoarsePathPlanningService(), new PathSmoothingService()) - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "observer bootstrap succeeds"); - - var planningService = new FixedTrajectoryPlanningService(trajectory); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "observer-check"); - var state = new VehicleMotionState(new Pose2D(0.25d, 0.10d, 0d), 0.20d, null, effectiveAt, 1L); - PlanningCycleResult firstCycle = controller.StartCycle(effectiveAt, state, CancellationToken.None) - .GetAwaiter().GetResult(); - Verification.True(firstCycle.Published, "observer first cycle publishes"); - Verification.Equal(0, planningService.Requests[0].SegmentIndex, "observer starts at segment zero"); - Verification.Equal("observer-check-trajectory-1", planningService.Requests[0].OutputTrajectoryId, - "observer first trajectory identity"); - Verification.NearlyEqual(settings.ReplanPeriodSeconds, - planningService.Requests[0].Configuration.Scheduling.ReplanPeriodSeconds, - "observer configured replan period"); - Verification.NearlyEqual(settings.SolverTimeoutSeconds, - planningService.Requests[0].Configuration.Scheduling.SolverTimeoutSeconds, - "observer configured solver timeout"); - Verification.Equal(settings.MaximumOsqpIterations, - planningService.Requests[0].Configuration.Solver.MaximumOsqpIterations, - "observer configured OSQP iteration limit"); - Verification.NearlyEqual(settings.TimeHorizonSeconds, - planningService.Requests[0].Configuration.Scheduling.TimeHorizonSeconds, - "observer configured ST time horizon"); - Verification.NearlyEqual(settings.OutputTimeStepSeconds, - planningService.Requests[0].Configuration.Scheduling.OutputTimeStepSeconds, - "observer configured ST timestamp spacing"); - - TrajectoryObservationObservation observation = controller.Observe(effectiveAt.AddSeconds(0.5d), state); - Verification.NearlyEqual(0.5d, observation.SelectedPoint.TimeFromStart, - "observer executor interpolates from published effective time"); - } - - private static void VerifiesRollingRequestUsesOnePublishedTrajectorySnapshot() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 1, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.RollingHorizon, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "observer rolling snapshot bootstrap succeeds"); - - EmTrajectory published = CreatePublishedTrajectory(effectiveAt); - var planningService = new FixedTrajectoryPlanningService(published); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "snapshot-check"); - var state = new VehicleMotionState(new Pose2D(0.25d, 0.10d, 0d), 0.20d, null, effectiveAt, 2L); - controller.StartCycle(effectiveAt, state, CancellationToken.None).GetAwaiter().GetResult(); - controller.StartCycle(effectiveAt.AddSeconds(settings.ReplanPeriodSeconds), state, CancellationToken.None) - .GetAwaiter().GetResult(); - - EmPlanningRequest rollingRequest = planningService.Requests[1]; - Verification.True(ReferenceEquals(published, rollingRequest.PreviousTrajectory), - "observer rolling request uses the controlled published trajectory object"); - Verification.Equal(rollingRequest.PreviousTrajectory.Metadata.TrajectoryId, - rollingRequest.PreviousTrajectoryId, - "observer rolling request trajectory object and ID use one publication snapshot"); - } - - private static void VerifiesActiveSegmentControllerDoesNotCrossSeedTrajectories() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 4, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.RollingHorizon, - DirectionConfirmationSamples = 1, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult baseBootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(baseBootstrap.Succeeded, "active-segment controller bootstrap succeeds"); - - TrajectoryObservationBootstrapResult bootstrap = TrajectoryObservationBootstrapResult.Success( - baseBootstrap.Job, baseBootstrap.CoarseResult, baseBootstrap.SmoothedPath, CreateDirectionalSegments()); - EmTrajectory forwardGearTrajectory = CreateSegmentTrajectory(t0, "active-forward", 0, - TravelDirection.Forward, EmTerminalType.GearSwitch, EmBoundaryType.GearSwitchApproach, 1d); - EmTrajectory reverseTrajectory = CreateSegmentTrajectory(t0.AddSeconds(1d), "active-reverse", 1, - TravelDirection.Reverse, EmTerminalType.Goal, EmBoundaryType.Goal, 0d); - var planningService = new SequenceTrajectoryPlanningService(forwardGearTrajectory, reverseTrajectory, - reverseTrajectory); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "active-segment"); - - VehicleMotionState stopped = new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0, 1L); - controller.StartCycle(t0, stopped, CancellationToken.None).GetAwaiter().GetResult(); - Verification.Equal(0, planningService.Requests[0].SegmentIndex, - "first active-segment request uses segment zero"); - Verification.Equal(0, controller.ActiveSegment.SegmentIndex, "controller exposes initial active segment"); - - controller.TryAdvanceSegment(t0, stopped); - controller.TryAdvanceSegment(t0.AddSeconds(0.21d), new VehicleMotionState( - new Pose2D(1d, 0d, 0d), 0d, null, t0.AddSeconds(0.21d), 2L)); - bool advanced = controller.TryAdvanceSegment(t0.AddSeconds(0.22d), new VehicleMotionState( - new Pose2D(1d, 0d, 0d), -0.03d, null, t0.AddSeconds(0.22d), 3L)); - - Verification.True(advanced, "confirmed reverse direction advances the controller"); - Verification.Equal(1, controller.ActiveSegment.SegmentIndex, "controller uses tracker-confirmed segment"); - Verification.True(ReferenceEquals(forwardGearTrajectory, controller.PreviousTrajectoryForVisualization), - "old direction trajectory is retained only for visualization"); - - DateTimeOffset reverseAt = t0.AddSeconds(1d); - controller.StartCycle(reverseAt, new VehicleMotionState(new Pose2D(1d, 0d, 0d), -0.03d, null, - reverseAt, 4L), CancellationToken.None).GetAwaiter().GetResult(); - Verification.Equal(1, planningService.Requests[1].SegmentIndex, - "next active-segment request uses segment one"); - Verification.True(planningService.Requests[1].PreviousTrajectory == null, - "new direction does not reuse the old trajectory as an EM seed"); - Verification.Equal("active-segment-trajectory-2", planningService.Requests[1].OutputTrajectoryId, - "session cycle IDs remain monotonic after coordinator replacement"); - - controller.StartCycle(reverseAt.AddSeconds(settings.ReplanPeriodSeconds), new VehicleMotionState( - new Pose2D(0.9d, 0d, 0d), -0.03d, null, reverseAt.AddSeconds(settings.ReplanPeriodSeconds), 5L), - CancellationToken.None).GetAwaiter().GetResult(); - Verification.True(ReferenceEquals(reverseTrajectory, planningService.Requests[2].PreviousTrajectory), - "same direction rolling request retains its published trajectory as the seed"); - } - - private static void VerifiesLoopDefersSegmentAdvanceUntilPlanningIsConsumed() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 5, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.RollingHorizon, - DirectionConfirmationSamples = 1, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult baseBootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - TrajectoryObservationBootstrapResult bootstrap = TrajectoryObservationBootstrapResult.Success( - baseBootstrap.Job, baseBootstrap.CoarseResult, baseBootstrap.SmoothedPath, CreateDirectionalSegments()); - EmTrajectory forwardGearTrajectory = CreateSegmentTrajectory(t0, "loop-forward", 0, - TravelDirection.Forward, EmTerminalType.GearSwitch, EmBoundaryType.GearSwitchApproach, 1d); - var planningService = new DelayedTrajectoryPlanningService(forwardGearTrajectory, 2); - try - { - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "loop-segment"); - controller.StartCycle(t0, new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0, 1L), - CancellationToken.None).GetAwaiter().GetResult(); - var loop = new TrajectoryObservationLoop(controller); - - loop.Tick(t0, new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0, 2L), - CancellationToken.None); - TrajectoryObservationLoopTick planningTick = loop.Tick(t0.AddSeconds(0.21d), - new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0.AddSeconds(0.21d), 3L), - CancellationToken.None); - Verification.True(planningTick.PlanningInFlight, "new cycle is in flight before direction evidence"); - Verification.True(planningService.WaitUntilEntered(TimeSpan.FromSeconds(5d)), - "delayed cycle entered planning service"); - - TrajectoryObservationLoopTick blockedTick = loop.Tick(t0.AddSeconds(0.22d), - new VehicleMotionState(new Pose2D(1d, 0d, 0d), -0.03d, null, t0.AddSeconds(0.22d), 4L), - CancellationToken.None); - Verification.True(!blockedTick.SegmentAdvanced, "in-flight planning prevents a segment transition"); - Verification.Equal(0, blockedTick.SegmentState.ActiveSegmentIndex, - "in-flight planning retains the immutable old segment state"); - - planningService.Release(); - TrajectoryObservationLoopTick? advancedTick = null; - int completedSequence = 5; - bool advanced = SpinWait.SpinUntil(() => - { - DateTimeOffset now = t0.AddSeconds(0.23d + (completedSequence - 5) * 0.01d); - advancedTick = loop.Tick(now, new VehicleMotionState( - new Pose2D(1d, 0d, 0d), -0.03d, null, now, completedSequence++), CancellationToken.None); - return advancedTick.SegmentAdvanced; - }, TimeSpan.FromSeconds(5d)); - Verification.True(advanced, - "loop advances only after it has consumed the completed planning task"); - Verification.Equal(1, advancedTick!.SegmentState.ActiveSegmentIndex, - "loop tick exposes the advanced immutable segment state"); - } - finally - { - planningService.Release(); - } - } - - private static void VerifiesPresentationTextDescribesObservationWithoutSendingCommand() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 0, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.RollingHorizon, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedTrajectoryPlanningService(CreatePublishedTrajectory(effectiveAt)), "presentation-check"); - var state = new VehicleMotionState(new Pose2D(0.25d, 0.10d, 0d), 0.20d, null, effectiveAt, 4L); - controller.StartCycle(effectiveAt, state, CancellationToken.None).GetAwaiter().GetResult(); - TrajectoryObservationObservation observation = controller.Observe(effectiveAt.AddSeconds(0.5d), state); - TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build( - observation.PublishedTrajectory, CreateStraightSegment(), 0.5d); - - string text = TrajectoryObservationPresentationText.Create(observation, charts); - - Verification.True(text.Contains("OBSERVE_ONLY: no chassis command is sent."), - "observer presentation observe-only notice"); - Verification.True(text.Contains("selected t=0.50 s, path-S=4.50 m"), - "observer presentation selected point time and path-S"); - Verification.True(text.Contains("signed speed=0.30 m/s"), - "observer presentation signed speed"); - Verification.True(text.Contains("yaw rate=0.00 rad/s"), - "observer presentation yaw rate"); - Verification.True(text.Contains("LS projection failures=0"), - "observer presentation LS projection failures"); - } - - private static void VerifiesPlanningDiagnosticsKeepRawFailureReason() - { - var failed = new PlanningCycleResult( - 4L, - new PlanningCycleIdentity(3L, "diagnostic-reference", 7L, string.Empty, 0), - new EmPlanningResult(EmPlanningStatus.CorridorInfeasible, null, - "map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"), - false, - "map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"); - - TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create( - failed, TimeSpan.FromMilliseconds(18d), false, null); - - Verification.True(diagnostic.Text.Contains("cycle=4"), "diagnostic has cycle version"); - Verification.True(diagnostic.Text.Contains("status=CorridorInfeasible"), "diagnostic preserves raw status"); - Verification.True(diagnostic.Text.Contains("published=False"), "diagnostic preserves publish state"); - Verification.True(diagnostic.Text.Contains("elapsed=18ms"), "diagnostic preserves elapsed time"); - Verification.True(diagnostic.Text.Contains("reason=map=3;reference=diagnostic-reference"), - "diagnostic preserves planner failure reason"); - foreach (string expected in new[] - { - "longitudinalMode=", "remainingToBoundary=", "minimumStoppingDistance=", - "minimumStoppingDuration=", "maximumStoppedReachableDistance=", - }) - { - Verification.True(diagnostic.Text.Contains(expected), "failure diagnostic includes " + expected); - } - Verification.True(!diagnostic.Text.Contains("trajectory summary:"), - "failed diagnostic has no stale trajectory summary"); - - TrajectoryObservationDiagnostic pending = TrajectoryObservationDiagnostics.Create( - null, TimeSpan.Zero, true, null); - Verification.Equal("planning status=pending", pending.Text, "diagnostic reports pending before completion"); - } - - private static void VerifiesPublishedPlanningDiagnosticsIncludeTrajectorySummary() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 5, 0, 0, 0, TimeSpan.Zero); - EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt); - var succeeded = new PlanningCycleResult(9L, - new PlanningCycleIdentity(1L, "summary-reference", 2L, string.Empty, 0), - new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty), true, string.Empty); - - string text = TrajectoryObservationDiagnostics.Create( - succeeded, TimeSpan.FromMilliseconds(12d), false, trajectory).Text; - foreach (string expected in new[] - { - "trajectory summary:", "trajectoryId=observer-published", "points=2", "duration=1.000s", - "pathLength=1.000m", "maxSpeed=0.400m/s", "maxAcceleration=0.200m/s2", "maxJerk=0.000m/s3", - "longitudinalMode=RollingContinuation", "terminalSpeed=0.400m/s", "terminalAcceleration=0.000m/s2", - }) - { - Verification.True(text.Contains(expected), "published diagnostic includes " + expected); - } - } - - private static void VerifiesCurveReportIncludesLsStSpeedAccelerationAndJerk() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 5, 0, 0, 0, TimeSpan.Zero); - EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt); - DirectionSegmentView segment = CreateStraightSegment(); - TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build(trajectory, segment, 0.5d); - - string text = TrajectoryObservationDiagnostics.CreateCurveReport(charts, trajectory).Text; - - foreach (string section in new[] - { - "LS_CURVE", "ST_CURVE", "SPEED_CURVE", "ACCELERATION_CURVE", "JERK_CURVE", - "referenceS(m)", "time(s)", "signedSpeed(m/s)", "acceleration(m/s2)", "jerk(m/s3)", - }) - { - Verification.True(text.Contains(section), "curve report includes " + section); - } - } - - private static void VerifiesEmptyChartsReceivePersistentPlanningDiagnostic() - { - string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); - - Verification.True(source.Contains("DrawLs(TrajectoryObservationCharts charts, string diagnosticText)"), - "LS painter accepts planning diagnostic input"); - Verification.True(source.Contains("DrawSt(TrajectoryObservationCharts charts, string diagnosticText)"), - "ST painter accepts planning diagnostic input"); - Verification.True(source.Contains("No published trajectory available for L-S chart.\\n"), - "LS empty state includes diagnostic after chart label"); - Verification.True(source.Contains("No published trajectory available for T-S/T-V charts.\\n"), - "ST empty state includes diagnostic after chart label"); - } - - private static void VerifiesLsPresentationUsesPathSOnHorizontalAxis() - { - TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build( - CreatePublishedTrajectory(new DateTimeOffset(2026, 8, 4, 0, 0, 0, TimeSpan.Zero)), - CreateStraightSegment(), 0.5d); - - TrajectoryObservationLsPresentationModel model = TrajectoryObservationLsPresentationModel.Create(charts); - - Verification.Equal("ReferenceS (m)", model.HorizontalAxisLabel, "observer LS horizontal axis label"); - Verification.Equal("l (m)", model.VerticalAxisLabel, "observer LS vertical axis label"); - Verification.NearlyEqual(10.25d, model.Samples[0].HorizontalPathS, - "observer LS shared ReferenceS is horizontal"); - Verification.NearlyEqual(0.10d, model.Samples[0].VerticalLateralOffset, - "observer LS lateral offset is vertical"); - } - - private static void VerifiesPainterPoseGeometryReplacesLargeArrow() - { - string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); - - Verification.True(!source.Contains("endArrow: true"), - "observer painter removes the large terminal arrow"); - Verification.True(source.Contains("endArrow: false"), - "observer painter uses a thin heading ray without an arrow"); - foreach (string corner in new[] - { - "frontLeftX", "frontLeftY", "frontRightX", "frontRightY", - "rearRightX", "rearRightY", "rearLeftX", "rearLeftY", - }) - { - Verification.True(source.Contains(corner), "observer pose outline includes " + corner); - } - Verification.True(source.Contains("VehicleParameters vehicle"), - "observer pose outline is scaled from vehicle parameters"); - Verification.True(source.Contains("MillimetersPerMeter = 1000f") && - source.Contains("ToMillimeters(path[index - 1].X)") && - source.Contains("ToMillimeters(path[index - 1].Y)") && - source.Contains("ToMillimeters(path[index].X)") && - source.Contains("ToMillimeters(path[index].Y)"), - "observer world paths use one millimetres-per-metre factor for x and y"); - } - - private static void VerifiesPainterStAxesUsePathSAndTimeUnits() - { - string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); - - Verification.True(source.Contains("\"t (s)\""), "observer ST horizontal axis uses seconds"); - Verification.True(source.Contains("\"PathS (m)\""), "observer ST vertical axis uses true PathS"); - } - - private static void FreezesBootstrapVehicleForRollingRequests() - { - DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 2, 0, 0, TimeSpan.Zero); - var settings = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.RollingHorizon, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - Array.Empty(), 0L); - VehicleParameters originalVehicle = job.Vehicle; - double expectedLength = originalVehicle.LengthMeters; - double expectedWidth = originalVehicle.WidthMeters; - double expectedMargin = originalVehicle.SafetyMarginMeters; - double? expectedMaximumCurvature = originalVehicle.MaximumCurvaturePerMeter; - double? expectedMinimumRadius = originalVehicle.MinimumTurningRadiusMeters; - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "observer vehicle snapshot bootstrap succeeds"); - - originalVehicle.LengthMeters = 91d; - originalVehicle.WidthMeters = 92d; - originalVehicle.SafetyMarginMeters = 93d; - originalVehicle.MaximumCurvaturePerMeter = 94d; - originalVehicle.MinimumTurningRadiusMeters = 95d; - - EmTrajectory published = CreatePublishedTrajectory(effectiveAt); - var planningService = new FixedTrajectoryPlanningService(published); - var controller = new TrajectoryObservationController(bootstrap, settings, planningService, "vehicle-check"); - var state = new VehicleMotionState(new Pose2D(0.25d, 0.10d, 0d), 0.20d, null, effectiveAt, 3L); - controller.StartCycle(effectiveAt, state, CancellationToken.None).GetAwaiter().GetResult(); - AssertVehicleSnapshot(planningService.Requests[0].Vehicle, expectedLength, expectedWidth, expectedMargin, - expectedMaximumCurvature, expectedMinimumRadius, "mutated bootstrap job vehicle"); - Verification.True(!ReferenceEquals(originalVehicle, planningService.Requests[0].Vehicle), - "observer request does not retain mutable bootstrap vehicle object"); - - job.Vehicle = new VehicleParameters - { - LengthMeters = 101d, - WidthMeters = 102d, - SafetyMarginMeters = 103d, - MaximumCurvaturePerMeter = 104d, - MinimumTurningRadiusMeters = 105d, - }; - controller.StartCycle(effectiveAt.AddSeconds(settings.ReplanPeriodSeconds), state, CancellationToken.None) - .GetAwaiter().GetResult(); - AssertVehicleSnapshot(planningService.Requests[1].Vehicle, expectedLength, expectedWidth, expectedMargin, - expectedMaximumCurvature, expectedMinimumRadius, "replaced bootstrap job vehicle"); - } - - private static void VerifiesEmptyMapBootstrapProducesPublishableReference() - { - var settings = new TrajectoryObservationSettings(); - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0.5d, 0.5d, 0d), new Pose2D(3.5d, 0.5d, 0d), settings, - Array.Empty(), 0L); - - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper() - .Bootstrap(job, CancellationToken.None); - - Verification.True(bootstrap.Succeeded, "observer empty-map bootstrap succeeds"); - Verification.True(bootstrap.CoarseResult.MapResult.Succeeded, - "observer empty-map bootstrap builds a successful map"); - Verification.True(bootstrap.Map != null, "observer empty-map bootstrap returns a map"); - Verification.Equal(PlanningStatus.Success, bootstrap.CoarseResult.PlanningResult.Status, - "observer empty-map coarse planning succeeds"); - PathSmoothingResult smoothedPath = bootstrap.SmoothedPath ?? throw new InvalidOperationException( - "observer empty-map bootstrap has no smoothing result"); - Verification.True(IsPublishableSmoothingStatus(smoothedPath.Status), - "observer empty-map bootstrap returns a publishable smoothing result"); - for (int index = 0; index < smoothedPath.Path.Count; index++) - { - Verification.True(!double.IsNaN(smoothedPath.Path[index].BodyClearance) && - !double.IsInfinity(smoothedPath.Path[index].BodyClearance), - "observer empty-map smoothing path has finite body clearance at " + index); - } - Verification.True(bootstrap.Segments.Count > 0, - "observer empty-map bootstrap returns at least one direction segment"); - } - - private static bool IsPublishableSmoothingStatus(PathSmoothingStatus status) - { - return status == PathSmoothingStatus.Complete || - status == PathSmoothingStatus.PartialImprovement || - status == PathSmoothingStatus.NotNeeded || - status == PathSmoothingStatus.Unchanged; - } - - private static void AssertVehicleSnapshot(VehicleParameters actual, double expectedLength, double expectedWidth, - double expectedMargin, double? expectedMaximumCurvature, double? expectedMinimumRadius, string name) - { - Verification.NearlyEqual(expectedLength, actual.LengthMeters, name + " length"); - Verification.NearlyEqual(expectedWidth, actual.WidthMeters, name + " width"); - Verification.NearlyEqual(expectedMargin, actual.SafetyMarginMeters, name + " safety margin"); - Verification.Equal(expectedMaximumCurvature, actual.MaximumCurvaturePerMeter, - name + " maximum curvature"); - Verification.Equal(expectedMinimumRadius, actual.MinimumTurningRadiusMeters, - name + " minimum turning radius"); - } - - private static DirectionSegmentView CreateStraightSegment() - { - var points = new List - { - new SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, TravelDirection.Forward, - 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(2d, 0d, 0d, 0d, 2d, TravelDirection.Forward, - 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - return new DirectionSegmentView(0, TravelDirection.Forward, points, - new ReferenceBoundary(0, 0d, EmBoundaryType.None, 10d), - new ReferenceBoundary(0, 2d, EmBoundaryType.Goal, 12d), 10d); - } - - private static EmTrajectory CreatePublishedTrajectory(DateTimeOffset effectiveAt, - EmTerminalType terminalType = EmTerminalType.Goal) - { - var metadata = new EmTrajectoryMetadata("observer-published", effectiveAt, effectiveAt, 1L, - "observer-reference", 1L, string.Empty, 0, TravelDirection.Forward, terminalType, - EmLongitudinalMode.RollingContinuation, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(0.25d, 0.10d, 0d, 0.20d, 0d, 0d, 0, 0.25d, 4d, - TravelDirection.Forward, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(1.25d, -0.20d, 0d, 0.40d, 1d, 0d, 0, 1.25d, 5d, - TravelDirection.Forward, EmBoundaryType.None, 0d, 0d), - }); - } - - private static IReadOnlyList CreateDirectionalSegments() - { - return new[] - { - CreateDirectionalSegment(0, TravelDirection.Forward, 0d, 1d, false, - EmBoundaryType.None, EmBoundaryType.GearSwitchApproach), - CreateDirectionalSegment(1, TravelDirection.Reverse, 1d, 0d, true, - EmBoundaryType.GearSwitchDeparture, EmBoundaryType.Goal), - }; - } - - private static DirectionSegmentView CreateDirectionalSegment(int index, TravelDirection direction, double startX, - double endX, bool startIsGearSwitch, EmBoundaryType startBoundary, EmBoundaryType endBoundary) - { - var points = new List - { - new SmoothedPathPoint(startX, 0d, 0d, 0d, 0d, direction, 0d, 0d, 1d, - startIsGearSwitch, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(endX, 0d, 0d, 0d, 1d, direction, 0d, 0d, 1d, - false, SmoothedPathPointSource.Anchor), - }; - return new DirectionSegmentView(index, direction, points, - new ReferenceBoundary(index, 0d, startBoundary, index), - new ReferenceBoundary(index, 1d, endBoundary, index + 1d), index); - } - - private static EmTrajectory CreateSegmentTrajectory(DateTimeOffset effectiveAt, string trajectoryId, int segmentIndex, - TravelDirection direction, EmTerminalType terminalType, EmBoundaryType boundaryType, double x) - { - var metadata = new EmTrajectoryMetadata(trajectoryId, effectiveAt, effectiveAt, 1L, - "active-segment-reference", 1L, string.Empty, segmentIndex, direction, terminalType, - terminalType == EmTerminalType.GearSwitch - ? EmLongitudinalMode.ExactStopAtBoundary - : EmLongitudinalMode.RollingContinuation, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(x, 0d, 0d, 0d, 0d, 0d, segmentIndex, 1d, 1d, - direction, boundaryType, 0d, 0d), - }); - } - - private static EmTrajectory CreateSingleForwardSimulationTrajectory(DateTimeOffset effectiveAt) - { - var metadata = new EmTrajectoryMetadata("single-forward-simulation", effectiveAt, effectiveAt, 1L, - "single-forward-reference", 1L, string.Empty, 0, TravelDirection.Forward, EmTerminalType.Goal, - EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.FullDirectionSegment); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(0d, 0d, 0d, 1d, 0d, 0d, 0, 0d, 0d, - TravelDirection.Forward, EmBoundaryType.None, 0d, 0d), - new EmTrajectoryPoint(1d, 0d, 0d, 0d, 1d, 0d, 0, 1d, 1d, - TravelDirection.Forward, EmBoundaryType.Goal, 0d, 0d), - }); - } - - private sealed class FixedTrajectoryPlanningService : IEmPlanningService - { - private readonly EmTrajectory trajectory; - - public FixedTrajectoryPlanningService(EmTrajectory trajectory) - { - this.trajectory = trajectory; - } - - public List Requests { get; } = new List(); - - public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) - { - Requests.Add(request); - return new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty); - } - } - - private sealed class SequenceTrajectoryPlanningService : IEmPlanningService - { - private readonly IReadOnlyList trajectories; - private int nextTrajectoryIndex; - - public SequenceTrajectoryPlanningService(params EmTrajectory[] trajectories) - { - this.trajectories = trajectories ?? throw new ArgumentNullException(nameof(trajectories)); - } - - public List Requests { get; } = new List(); - - public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) - { - Requests.Add(request); - EmTrajectory trajectory = trajectories[Math.Min(nextTrajectoryIndex, trajectories.Count - 1)]; - nextTrajectoryIndex++; - return new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty); - } - } - - private sealed class DelayedTrajectoryPlanningService : IEmPlanningService - { - private readonly EmTrajectory trajectory; - private readonly int delayedCall; - private readonly ManualResetEventSlim entered = new ManualResetEventSlim(false); - private readonly ManualResetEventSlim release = new ManualResetEventSlim(false); - private int callCount; - - public DelayedTrajectoryPlanningService(EmTrajectory trajectory, int delayedCall) - { - this.trajectory = trajectory; - this.delayedCall = delayedCall; - } - - public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) - { - if (Interlocked.Increment(ref callCount) == delayedCall) - { - entered.Set(); - release.Wait(cancellationToken); - } - return new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty); - } - - public bool WaitUntilEntered(TimeSpan timeout) - { - return entered.Wait(timeout); - } - - public void Release() - { - release.Set(); - } - } - - private sealed class FailingTrajectoryPlanningService : IEmPlanningService - { - public List Requests { get; } = new List(); - - public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) - { - Requests.Add(request); - return new EmPlanningResult(EmPlanningStatus.CorridorInfeasible, null, - "one-shot failure fixture"); - } - } - - private static void VerifiesMovementTestKeepsNativePainterLazyAndConditional() - { - string movementTestPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(movementTestPath)); - Verification.True(!source.Contains("static readonly TrajectoryObservationPresentation"), - "MovementTest has no static eager Painter presentation"); - Verification.True(source.Contains("settings.EnableNativePainterVisualization ? new TrajectoryObservationPresentation() : null"), - "MovementTest creates Painter only under the native visualization flag"); - } - - private static bool Throws(Action action) - { - try { action(); return false; } - catch (ArgumentOutOfRangeException) { return true; } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs deleted file mode 100644 index c204933..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.EMPlanner; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; - -namespace EMPlannerVerificationHost; - -internal static class TrajectoryObservationSegmentChecks -{ - public static void Run() - { - RejectsHardcodedActiveSegmentIndex(); - AdvancesOnlyAfterContinuousStopAndStableNextDirection(); - RejectsNonTerminalOrMismatchedGearTrajectory(); - ResetsConfirmationForInvalidDirectionEvidence(); - CompletesOneSegmentWithoutIndexingPastTheEnd(); - CompletesSingleForwardGoalTrajectory(); - } - - private static void RejectsHardcodedActiveSegmentIndex() - { - string pipelinePath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPipeline.cs"); - string source = File.ReadAllText(pipelinePath); - - Verification.True(source.IndexOf("segmentIndex = 0", StringComparison.Ordinal) < 0, - "active observation segment is not hardcoded to zero"); - } - - private static void AdvancesOnlyAfterContinuousStopAndStableNextDirection() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 0, 0, 0, TimeSpan.Zero); - var tracker = CreateTracker(); - EmTrajectory trajectory = GearTerminal(t0); - - tracker.Update(t0, StateAtSwitch(0d, t0, 1L), trajectory); - Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForStop, tracker.State.Phase, - "first zero sample begins stop hold"); - tracker.Update(t0.AddSeconds(0.21d), StateAtSwitch(0d, t0.AddSeconds(0.21d), 2L), trajectory); - Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForDirection, tracker.State.Phase, - "continuous stop arms next direction"); - tracker.Update(t0.AddSeconds(0.25d), StateAtSwitch(-0.03d, t0.AddSeconds(0.25d), 3L), trajectory); - tracker.Update(t0.AddSeconds(0.30d), StateAtSwitch(-0.03d, t0.AddSeconds(0.30d), 4L), trajectory); - TrajectoryObservationSegmentUpdate advanced = tracker.Update( - t0.AddSeconds(0.35d), StateAtSwitch(-0.03d, t0.AddSeconds(0.35d), 5L), trajectory); - - Verification.True(advanced.Advanced, "three stable reverse samples advance exactly one segment"); - Verification.Equal(1, tracker.State.ActiveSegmentIndex, "tracker advances only from N to N+1"); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, tracker.State.Phase, - "advanced tracker resumes planning on next segment"); - } - - private static void RejectsNonTerminalOrMismatchedGearTrajectory() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 1, 0, 0, TimeSpan.Zero); - var tracker = CreateTracker(); - - tracker.Update(t0, StateAtSwitch(0d, t0, 1L), GearTerminal(t0, EmTerminalType.Goal)); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, tracker.State.Phase, - "non-gear terminal cannot begin confirmation"); - - tracker.Update(t0.AddSeconds(0.01d), StateAtSwitch(0d, t0.AddSeconds(0.01d), 2L), - GearTerminal(t0, EmTerminalType.GearSwitch, TravelDirection.Reverse)); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, tracker.State.Phase, - "wrong trajectory direction cannot begin confirmation"); - - tracker.Update(t0.AddSeconds(0.02d), StateAtSwitch(0d, t0.AddSeconds(0.02d), 3L), - GearTerminal(t0, EmTerminalType.GearSwitch, TravelDirection.Forward, 0.10d)); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, tracker.State.Phase, - "trajectory before its absolute terminal cannot begin confirmation"); - } - - private static void ResetsConfirmationForInvalidDirectionEvidence() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 2, 0, 0, TimeSpan.Zero); - AssertConfirmationReset(t0, StateAtSwitch(0.03d, t0.AddSeconds(0.25d), 3L), GearTerminal(t0), - "wrong signed direction resets confirmation"); - AssertConfirmationReset(t0, StateAtSwitch(0d, t0.AddSeconds(0.25d), 3L), GearTerminal(t0), - "zero speed resets confirmation"); - AssertConfirmationReset(t0, StateAt(2d, 0d, -0.03d, t0.AddSeconds(0.25d), 3L), GearTerminal(t0), - "excessive switch projection distance resets confirmation"); - AssertConfirmationReset(t0, StateAtSwitch(-0.03d, t0.AddSeconds(0.25d), 3L), - GearTerminal(t0, EmTerminalType.Goal), "non-gear trajectory resets confirmation"); - - var repeated = CreateTracker(); - ArmWaitingForDirection(repeated, t0, GearTerminal(t0)); - repeated.Update(t0.AddSeconds(0.25d), StateAtSwitch(-0.03d, t0.AddSeconds(0.25d), 2L), GearTerminal(t0)); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, repeated.State.Phase, - "repeated sequence resets confirmation"); - - var discontinuous = CreateTracker(); - ArmWaitingForDirection(discontinuous, t0, GearTerminal(t0)); - discontinuous.Update(t0.AddSeconds(0.20d), StateAtSwitch(-0.03d, t0.AddSeconds(0.20d), 3L), GearTerminal(t0)); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, discontinuous.State.Phase, - "time discontinuity resets confirmation"); - } - - private static void CompletesOneSegmentWithoutIndexingPastTheEnd() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 3, 0, 0, TimeSpan.Zero); - var tracker = new TrajectoryObservationSegmentTracker(new[] { CreateSegments()[0] }, - CreateSettings(), 0.01d); - - TrajectoryObservationSegmentUpdate completed = tracker.Update( - t0, StateAtSwitch(0d, t0, 1L), GearTerminal(t0)); - - Verification.True(completed.Completed, "single segment update reports completion"); - Verification.Equal(TrajectoryObservationSegmentPhase.Completed, tracker.State.Phase, - "single segment reaches completed instead of indexing past end"); - Verification.Equal(0, tracker.State.ActiveSegmentIndex, "completed tracker retains final segment index"); - } - - private static void CompletesSingleForwardGoalTrajectory() - { - DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 3, 30, 0, TimeSpan.Zero); - var tracker = new TrajectoryObservationSegmentTracker(new[] - { - CreateSegment(0, TravelDirection.Forward, 0d, 1d, false, 0d, EmBoundaryType.None, EmBoundaryType.Goal), - }, CreateSettings(), 0.01d); - - TrajectoryObservationSegmentUpdate completed = tracker.Update(t0, StateAtSwitch(0d, t0, 1L), - GoalTerminal(t0)); - - Verification.True(completed.Completed, "single forward Goal trajectory reports completion"); - Verification.Equal(TrajectoryObservationSegmentPhase.Completed, tracker.State.Phase, - "single forward Goal reaches completed without requiring a gear-switch terminal"); - } - - private static void AssertConfirmationReset(DateTimeOffset t0, VehicleMotionState invalidState, - EmTrajectory invalidTrajectory, string name) - { - var tracker = CreateTracker(); - ArmWaitingForDirection(tracker, t0, GearTerminal(t0)); - tracker.Update(invalidState.CapturedAtUtc, invalidState, invalidTrajectory); - Verification.Equal(TrajectoryObservationSegmentPhase.Planning, tracker.State.Phase, name); - } - - private static void ArmWaitingForDirection(TrajectoryObservationSegmentTracker tracker, DateTimeOffset t0, - EmTrajectory trajectory) - { - tracker.Update(t0, StateAtSwitch(0d, t0, 1L), trajectory); - tracker.Update(t0.AddSeconds(0.21d), StateAtSwitch(0d, t0.AddSeconds(0.21d), 2L), trajectory); - Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForDirection, tracker.State.Phase, - "fixture arms direction confirmation"); - } - - private static TrajectoryObservationSegmentTracker CreateTracker() - { - return new TrajectoryObservationSegmentTracker(CreateSegments(), CreateSettings(), 0.01d); - } - - private static TrajectoryObservationSettings CreateSettings() - { - return new TrajectoryObservationSettings - { - DirectionConfirmationSpeedMetersPerSecond = 0.02d, - DirectionConfirmationSamples = 3, - GearSwitchProjectionToleranceMeters = 0.50d, - GearSwitchStopHoldSeconds = 0.20d, - }.CreateValidatedSnapshot(); - } - - private static IReadOnlyList CreateSegments() - { - return new[] - { - CreateSegment(0, TravelDirection.Forward, 0d, 1d, false, 0d, - EmBoundaryType.None, EmBoundaryType.GearSwitchApproach), - CreateSegment(1, TravelDirection.Reverse, 1d, 0d, true, 1d, - EmBoundaryType.GearSwitchDeparture, EmBoundaryType.Goal), - }; - } - - private static DirectionSegmentView CreateSegment(int index, TravelDirection direction, double startX, - double endX, bool startIsGearSwitch, double sourceStartS, EmBoundaryType startBoundary, - EmBoundaryType endBoundary) - { - var points = new List - { - new SmoothedPathPoint(startX, 0d, 0d, 0d, 0d, direction, 0d, 0d, 1d, - startIsGearSwitch, SmoothedPathPointSource.Anchor), - new SmoothedPathPoint(endX, 0d, 0d, 0d, 1d, direction, 0d, 0d, 1d, - false, SmoothedPathPointSource.Anchor), - }; - return new DirectionSegmentView(index, direction, points, - new ReferenceBoundary(index, 0d, startBoundary, sourceStartS), - new ReferenceBoundary(index, 1d, endBoundary, sourceStartS + 1d), sourceStartS); - } - - private static VehicleMotionState StateAtSwitch(double speed, DateTimeOffset time, long sequenceId) - { - return StateAt(1d, 0d, speed, time, sequenceId); - } - - private static VehicleMotionState StateAt(double x, double y, double speed, DateTimeOffset time, long sequenceId) - { - return new VehicleMotionState(new Pose2D(x, y, 0d), speed, null, time, sequenceId); - } - - private static EmTrajectory GearTerminal(DateTimeOffset effectiveAtUtc, - EmTerminalType terminalType = EmTerminalType.GearSwitch, TravelDirection direction = TravelDirection.Forward, - double finalTimeFromStart = 0d) - { - var metadata = new EmTrajectoryMetadata("gear-terminal-" + effectiveAtUtc.Ticks, effectiveAtUtc, - effectiveAtUtc, 1L, "segment-check", 1L, string.Empty, 0, direction, terminalType, - EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.RollingHorizon); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(1d, 0d, 0d, 0d, finalTimeFromStart, 0d, 0, 1d, 1d, - direction, EmBoundaryType.GearSwitchApproach, 0d, 0d), - }); - } - - private static EmTrajectory GoalTerminal(DateTimeOffset effectiveAtUtc) - { - var metadata = new EmTrajectoryMetadata("goal-terminal-" + effectiveAtUtc.Ticks, effectiveAtUtc, - effectiveAtUtc, 1L, "segment-check", 1L, string.Empty, 0, TravelDirection.Forward, - EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.FullDirectionSegment); - return new EmTrajectory(metadata, new[] - { - new EmTrajectoryPoint(1d, 0d, 0d, 0d, 0d, 0d, 0, 1d, 1d, - TravelDirection.Forward, EmBoundaryType.Goal, 0d, 0d), - }); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs deleted file mode 100644 index 82d900c..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.IO; -using System.Text; -using MultiWheelC.TrajectoryPlanning.EMPlanner; -using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; - -namespace EMPlannerVerificationHost; - -internal static class TrajectoryObservationSettingsChecks -{ - public static void Run() - { - VerifiesDefaults(); - VerifiesValidatedSnapshotFreezesVisualizationSettings(); - RejectsInvalidVisualizationSettings(); - VerifiesMovementTestMapsEveryFrozenSetting(); - VerifiesMovementTestFullDirectionScopeMapping(); - } - - private static void VerifiesDefaults() - { - var defaults = new TrajectoryObservationSettings(); - - Verification.NearlyEqual(5d, defaults.SolverTimeoutSeconds, "observer solver timeout default"); - Verification.Equal(100000, defaults.MaximumOsqpIterations, "observer OSQP default"); - Verification.NearlyEqual(2d, defaults.TimeHorizonSeconds, "observer ST horizon default"); - Verification.Equal(EmPlanningScope.FullDirectionSegment, defaults.PlanningScope, - "observer full-direction-segment scope default"); - Verification.True(defaults.EnableWebVisualization, "web defaults on"); - Verification.True(defaults.AutoOpenWebVisualization, "web auto-open defaults on"); - Verification.True(!defaults.EnableNativePainterVisualization, "Painter defaults off"); - Verification.Equal(0, defaults.WebVisualizationPort, "dynamic port default"); - Verification.NearlyEqual(10d, defaults.WebRefreshRateHz, "web refresh default"); - Verification.Equal(60, defaults.VisualizationHistoryCycleLimit, "history default"); - Verification.NearlyEqual(0.02d, defaults.DirectionConfirmationSpeedMetersPerSecond, - "direction speed threshold"); - Verification.Equal(3, defaults.DirectionConfirmationSamples, "direction sample default"); - Verification.NearlyEqual(0.50d, defaults.GearSwitchProjectionToleranceMeters, - "gear projection default"); - Verification.NearlyEqual(0.20d, defaults.GearSwitchStopHoldSeconds, "gear stop hold default"); - } - - private static void VerifiesValidatedSnapshotFreezesVisualizationSettings() - { - var source = new TrajectoryObservationSettings - { - PlanningScope = EmPlanningScope.FullDirectionSegment, - EnableWebVisualization = true, - AutoOpenWebVisualization = false, - WebVisualizationPort = 2048, - WebRefreshRateHz = 15d, - VisualizationHistoryCycleLimit = 17, - EnableNativePainterVisualization = true, - DirectionConfirmationSpeedMetersPerSecond = 0.03d, - DirectionConfirmationSamples = 4, - GearSwitchProjectionToleranceMeters = 0.6d, - GearSwitchStopHoldSeconds = 0.3d, - }; - TrajectoryObservationSettings snapshot = source.CreateValidatedSnapshot(); - - source.EnableWebVisualization = false; - source.PlanningScope = EmPlanningScope.RollingHorizon; - source.AutoOpenWebVisualization = true; - source.WebVisualizationPort = 0; - source.WebRefreshRateHz = 12d; - source.VisualizationHistoryCycleLimit = 18; - source.EnableNativePainterVisualization = false; - source.DirectionConfirmationSpeedMetersPerSecond = 0.04d; - source.DirectionConfirmationSamples = 5; - source.GearSwitchProjectionToleranceMeters = 0.7d; - source.GearSwitchStopHoldSeconds = 0.4d; - - Verification.Equal(EmPlanningScope.FullDirectionSegment, snapshot.PlanningScope, - "snapshot freezes planning scope"); - Verification.True(snapshot.EnableWebVisualization, "snapshot freezes web switch"); - Verification.True(!snapshot.AutoOpenWebVisualization, "snapshot freezes web auto-open switch"); - Verification.Equal(2048, snapshot.WebVisualizationPort, "snapshot freezes web port"); - Verification.NearlyEqual(15d, snapshot.WebRefreshRateHz, "snapshot freezes web refresh"); - Verification.Equal(17, snapshot.VisualizationHistoryCycleLimit, "snapshot freezes history limit"); - Verification.True(snapshot.EnableNativePainterVisualization, "snapshot freezes Painter switch"); - Verification.NearlyEqual(0.03d, snapshot.DirectionConfirmationSpeedMetersPerSecond, - "snapshot freezes direction speed"); - Verification.Equal(4, snapshot.DirectionConfirmationSamples, "snapshot freezes direction samples"); - Verification.NearlyEqual(0.6d, snapshot.GearSwitchProjectionToleranceMeters, - "snapshot freezes gear projection tolerance"); - Verification.NearlyEqual(0.3d, snapshot.GearSwitchStopHoldSeconds, "snapshot freezes gear stop hold"); - } - - private static void RejectsInvalidVisualizationSettings() - { - AssertInvalid(settings => settings.WebVisualizationPort = -1, "negative web port"); - AssertInvalid(settings => settings.WebVisualizationPort = 1, "low reserved web port"); - AssertInvalid(settings => settings.WebVisualizationPort = 1023, "last reserved web port"); - AssertInvalid(settings => settings.WebVisualizationPort = 65536, "overflow web port"); - AssertInvalid(settings => settings.PlanningScope = (EmPlanningScope)999, "undefined planning scope"); - AssertInvalid(settings => settings.WebRefreshRateHz = 0d, "zero web refresh"); - AssertInvalid(settings => settings.VisualizationHistoryCycleLimit = 0, "zero history limit"); - AssertInvalid(settings => settings.DirectionConfirmationSpeedMetersPerSecond = 0d, - "zero direction speed threshold"); - AssertInvalid(settings => settings.DirectionConfirmationSamples = 0, "zero direction samples"); - AssertInvalid(settings => settings.GearSwitchProjectionToleranceMeters = 0d, - "zero gear projection tolerance"); - AssertInvalid(settings => settings.GearSwitchStopHoldSeconds = 0d, "zero gear stop hold"); - } - - private static void VerifiesMovementTestMapsEveryFrozenSetting() - { - string path = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ParkrobTrajplanner", - "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(path)); - foreach (string field in new[] - { - "EnableWebVisualization", "AutoOpenWebVisualization", "WebVisualizationPort", "WebRefreshRateHz", - "VisualizationHistoryCycleLimit", "EnableNativePainterVisualization", - "DirectionConfirmationSpeedMetersPerSecond", "DirectionConfirmationSamples", - "GearSwitchProjectionToleranceMeters", "GearSwitchStopHoldSeconds", - }) - { - Verification.True(source.Contains("public ") && source.Contains(" " + field + " ="), - "MovementTest exposes " + field); - Verification.True(source.Contains(field + " = " + field + ","), - "MovementTest maps " + field + " into settings"); - } - } - - private static void VerifiesMovementTestFullDirectionScopeMapping() - { - string path = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ParkrobTrajplanner", - "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(path)); - - Verification.True(source.Contains("public bool UseFullDirectionSegmentPlanning = true;"), - "MovementTest exposes the full-direction-segment switch on by default"); - Verification.True(source.Contains( - "PlanningScope = UseFullDirectionSegmentPlanning ? EmPlanningScope.FullDirectionSegment : EmPlanningScope.RollingHorizon,"), - "MovementTest maps the full-direction-segment switch into settings"); - Verification.True(source.Contains("滚动兼容"), - "MovementTest labels TimeHorizonSeconds as rolling-compatible"); - } - - private static void AssertInvalid(Action mutate, string name) - { - var settings = new TrajectoryObservationSettings(); - mutate(settings); - bool threw = false; - try { settings.Validate(); } - catch (ArgumentOutOfRangeException) { threw = true; } - Verification.True(threw, "observer rejects invalid " + name); - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs deleted file mode 100644 index 8d3be8f..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs +++ /dev/null @@ -1,648 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.EMPlanner; -using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; -using TrajectoryPlanningVisualization; - -namespace EMPlannerVerificationHost; - -internal static class TrajectoryObservationVisualizationChecks -{ - public static void Run() - { - VerifiesStaticSnapshotExportsFrozenConfigurationAndGeometry(); - VerifiesKinematicChartsKeepKnotAndIntervalSemantics(); - VerifiesHandoffUsesSharedReferenceSAndSurvivesProjectionFailure(); - VerifiesDynamicSnapshotContainsObservationEvidenceWithoutCyclePoints(); - VerifiesFullModeSnapshotPublishesSingleCurrentTrajectoryAndBoundaryMarkers(); - VerifiesPainterBoundarySemanticsMatchWeb(); - VerifiesWebPublisherFusesFaultWithoutStoppingObserverTicks(); - VerifiesWebPublisherGatesAtConfiguredCadenceAndSkipsDisabledOutput(); - } - - private static void VerifiesStaticSnapshotExportsFrozenConfigurationAndGeometry() - { - var settings = new TrajectoryObservationSettings(); - TrajectoryObservationBootstrapResult bootstrap = Bootstrap(settings); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedPlanningService(CreateTrajectory("static", DateTimeOffset.UnixEpoch, 2, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward)), "static-configuration"); - EmPlannerConfiguration effective = ReadEffectiveConfiguration(controller); - settings.TimeHorizonSeconds = 99d; - - PlanningVisualizationStaticSnapshot snapshot = new TrajectoryObservationStaticSnapshotBuilder() - .Build(bootstrap, effective, settings.CreateValidatedSnapshot(), 17L); - - Verification.Equal(10, snapshot.ConfigurationGroups.Count, "static configuration group count"); - Verification.Equal("调度", snapshot.ConfigurationGroups[0].ChineseTitle, "static configuration group 0"); - Verification.Equal("OSQP", snapshot.ConfigurationGroups[1].ChineseTitle, "static configuration group 1"); - Verification.Equal("车辆与安全", snapshot.ConfigurationGroups[2].ChineseTitle, "static configuration group 2"); - Verification.Equal("纵向限制", snapshot.ConfigurationGroups[3].ChineseTitle, "static configuration group 3"); - Verification.Equal("ST 权重", snapshot.ConfigurationGroups[4].ChineseTitle, "static configuration group 4"); - Verification.Equal("横向限制", snapshot.ConfigurationGroups[5].ChineseTitle, "static configuration group 5"); - Verification.Equal("LS 权重", snapshot.ConfigurationGroups[6].ChineseTitle, "static configuration group 6"); - Verification.Equal("走廊与投影", snapshot.ConfigurationGroups[7].ChineseTitle, "static configuration group 7"); - Verification.Equal("地图", snapshot.ConfigurationGroups[8].ChineseTitle, "static configuration group 8"); - Verification.Equal("可视化与换向确认", snapshot.ConfigurationGroups[9].ChineseTitle, "static configuration group 9"); - - IReadOnlyDictionary values = Flatten(snapshot); - AssertValue(values, "TimeHorizonSeconds", "2", "effective configuration is frozen before editable settings change"); - AssertValue(values, "MaximumOsqpIterations", "100000", "effective OSQP iteration limit"); - AssertEffectiveValues(values, effective, settings, bootstrap); - foreach (string rawName in new[] - { - "AbsoluteTolerance", "RelativeTolerance", "StrictResidualTolerance", "WarmStart", "Polish", "NativeVerbose", - "LengthMeters", "WidthMeters", "SafetyMarginMeters", "MaximumCurvaturePerMeter", - "MaximumForwardSpeedMetersPerSecond", "MaximumReverseSpeedMetersPerSecond", - "MaximumAccelerationMetersPerSecondSquared", "MaximumDecelerationMetersPerSecondSquared", - "MaximumJerkMetersPerSecondCubed", "MaximumLateralAccelerationMetersPerSecondSquared", - "MaximumCurvatureRatePerMeterPerSecond", "StopSpeedToleranceMetersPerSecond", "ZeroSpeedHoldSeconds", - "ReferenceSpeed", "Acceleration", "Jerk", "PreviousTrajectory", "TerminalAcceleration", - "ReferenceOffset", "HeadingDeviation", "SecondDerivative", "ThirdDerivative", "Curvature", - "CurvatureVariation", "RollingTerminal", "LongitudinalSampleSpacingMeters", - "LateralSampleSpacingMeters", "MaximumLateralOffsetMeters", "AdditionalClearanceReserveMeters", - "MaximumCollisionCheckStepMeters", "MaximumProjectionDistanceMeters", "MinimumFrenetDenominator", - "BoundaryAnchorToleranceMeters", "SpatialToleranceMeters", "KinematicTolerance", "ObstacleSnapshotVersion", - "ResolutionMm", "Bounds.XMin", "Bounds.XMax", "Bounds.YMin", "Bounds.YMax", "Rows", "Cols", - "EnableWebVisualization", "AutoOpenWebVisualization", "WebVisualizationPort", "WebRefreshRateHz", - "VisualizationHistoryCycleLimit", "EnableNativePainterVisualization", - "DirectionConfirmationSpeedMetersPerSecond", "DirectionConfirmationSamples", - "GearSwitchProjectionToleranceMeters", "GearSwitchStopHoldSeconds", - }) - Verification.True(values.ContainsKey(rawName), "static configuration exports " + rawName); - - Verification.Equal(bootstrap.Segments.Count, snapshot.DirectionSegments.Count, "all direction segments exported"); - Verification.True(snapshot.StaticPolylines.Any(x => x.Id == "coarse-path"), "coarse path exported"); - Verification.True(snapshot.StaticPolylines.Any(x => x.Id == "local-g2-path"), "full Local G2 path exported"); - VisualizationMarker smoothStart = snapshot.StaticMarkers.Single(x => x.Kind == "smooth-start"); - Verification.Equal("平滑路径起点", smoothStart.LabelChinese, - "static snapshot labels the smoothed-path start"); - Verification.NearlyEqual(bootstrap.SmoothedPath.Path[0].X, smoothStart.Position.X, - "smoothed-path start marker X matches the first smoothed point"); - Verification.NearlyEqual(bootstrap.SmoothedPath.Path[0].Y, smoothStart.Position.Y, - "smoothed-path start marker Y matches the first smoothed point"); - byte[] occupancy = Convert.FromBase64String(snapshot.OccupancyGrid.OccupancyBitsBase64); - Verification.Equal((bootstrap.Map.Rows * bootstrap.Map.Cols + 7) / 8, occupancy.Length, - "occupancy bitset compact length"); - bool verifiedOccupiedCell = false; - for (int row = 0; row < bootstrap.Map.Rows; row++) - for (int col = 0; col < bootstrap.Map.Cols; col++) - if (bootstrap.Map.IsOccupied(row, col)) - { - int bit = row * bootstrap.Map.Cols + col; - Verification.True((occupancy[bit >> 3] & (1 << (bit & 7))) != 0, - "occupancy uses row-major LSB-first bitset"); - verifiedOccupiedCell = true; - break; - } - Verification.True(verifiedOccupiedCell, "static fixture has an occupied cell"); - - EmPlannerConfiguration defaults = EmPlannerConfiguration.CreateDefault(); - TrajectoryObservationBootstrapResult gearBootstrap = TrajectoryObservationBootstrapResult.Success( - bootstrap.Job, bootstrap.CoarseResult, bootstrap.SmoothedPath, - new[] { CreateGearSwitchSegment(0, 0d) }); - PlanningVisualizationStaticSnapshot gearStatic = new TrajectoryObservationStaticSnapshotBuilder() - .Build(gearBootstrap, defaults, settings.CreateValidatedSnapshot(), 0L); - Verification.True(gearStatic.StaticMarkers.Any(x => x.Kind == "gear-switch-end"), - "static snapshot marks gear-switch s_end"); - Verification.True(snapshot.StaticMarkers.Any(x => x.Kind == "final-goal"), - "static snapshot marks final goal s_end"); - } - - private static void VerifiesKinematicChartsKeepKnotAndIntervalSemantics() - { - DirectionSegmentView segment = CreateStraightSegment(0, 50d); - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - EmTrajectory trajectory = CreateTrajectory("rolling", DateTimeOffset.UnixEpoch, 21, - EmTerminalType.Goal, EmLongitudinalMode.RollingContinuation, TravelDirection.Forward); - - var vehicle = new VehicleParameters - { - LengthMeters = 0.8d, - WidthMeters = 0.6d, - SafetyMarginMeters = 0.05d, - MaximumCurvaturePerMeter = 0.4d, - MinimumTurningRadiusMeters = 2.5d, - }; - IReadOnlyList charts = new TrajectoryObservationKinematicChartBuilder() - .Build(trajectory, segment, vehicle, configuration); - - Verification.Equal(8, charts.Count, "all eight kinematic charts exported"); - Verification.Equal(20, FindChart(charts, "jerk-t").Series[0].Points.Count, "jerk has N-1 intervals"); - Verification.True(FindChart(charts, "jerk-t").NoteChinese.Contains("末点后无时间区间"), - "jerk explains no terminal successor interval"); - Verification.Equal(21, FindChart(charts, "velocity-t").Series[0].Points.Count, "velocity remains knot based"); - Verification.Equal("ReferenceS (m)", FindChart(charts, "ls").XAxisLabel, "LS uses shared reference station"); - Verification.Equal("PathS (m)", FindChart(charts, "st").YAxisLabel, "ST uses plan-local PathS"); - foreach (string id in new[] { "curvature-s", "curvature-t", "velocity-t", "acceleration-t", "jerk-t" }) - Verification.True(FindChart(charts, id).Series.Any(x => x.LineStyle == VisualizationLineStyle.Limit), - id + " has an effective limit line"); - Verification.True(!FindChart(charts, "yaw-rate-t").Series.Any(x => x.LineStyle == VisualizationLineStyle.Limit), - "yaw rate has no invented limit"); - configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1.25d; - charts = new TrajectoryObservationKinematicChartBuilder().Build(trajectory, segment, vehicle, configuration); - Verification.Equal("ω (rad/s)", FindChart(charts, "yaw-rate-t").YAxisLabel, - "yaw-rate axis uses omega unit"); - Verification.NearlyEqual(0.4d, FindChart(charts, "curvature-s") - .Series.Single(x => x.Id == "curvature-s-positive-limit").Points[0].Y, - "curvature hard limit uses vehicle curvature"); - Verification.True(FindChart(charts, "curvature-s").Series[0].Points.Any(p => Math.Abs(p.Y) > 1e-6), - "curvature-distance uses vehicle curvature data"); - PropertyInfo? annotationsProperty = typeof(VisualizationChart).GetProperty("Annotations"); - Verification.True(annotationsProperty != null, "chart exposes annotation collection"); - if (annotationsProperty != null) - { - object annotationList = annotationsProperty!.GetValue(FindChart(charts, "st"))!; - Verification.True(annotationList is System.Collections.IEnumerable, - "ST annotations are enumerable"); - var annotations = new List(); - foreach (object annotation in (System.Collections.IEnumerable)annotationList!) - { - annotations.Add(annotation); - } - Verification.True(annotations.Count > 0, "ST chart marks s_end"); - Verification.True(annotations.Any(annotation => - (string)annotation.GetType().GetProperty("Kind")!.GetValue(annotation)! == "s-end"), - "ST s_end annotation kind is explicit"); - object lsAnnotationList = annotationsProperty!.GetValue(FindChart(charts, "ls"))!; - var lsAnnotations = new List(); - foreach (object annotation in (System.Collections.IEnumerable)lsAnnotationList!) - { - lsAnnotations.Add(annotation); - } - Verification.True(lsAnnotations.Count > 0, "LS chart marks s_end"); - Verification.True(lsAnnotations.Any(annotation => - (string)annotation.GetType().GetProperty("Kind")!.GetValue(annotation)! == "s-end"), - "LS s_end annotation kind is explicit"); - } - Verification.True(typeof(TrajectoryObservationDynamicSnapshotBuilder).GetMethods().Any(method => - method.Name == "Build" && - method.GetParameters().Any(parameter => parameter.ParameterType == typeof(VehicleParameters))), - "dynamic builder accepts vehicle parameters"); - EmTrajectory unprojectable = CreateTrajectory("unprojectable", DateTimeOffset.UnixEpoch, 21, - EmTerminalType.Goal, EmLongitudinalMode.RollingContinuation, TravelDirection.Forward, 100d); - Verification.True(FindChart(new TrajectoryObservationKinematicChartBuilder().Build(unprojectable, segment, vehicle, configuration), "ls") - .NoteChinese.Contains("投影失败=21"), "LS chart reports every failed full-segment projection"); - } - - private static void VerifiesHandoffUsesSharedReferenceSAndSurvivesProjectionFailure() - { - DirectionSegmentView segment = CreateStraightSegment(0, 100d); - DateTimeOffset t0 = DateTimeOffset.UnixEpoch; - EmTrajectory previous = CreateTrajectory("previous", t0, 11, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward, 2d, 0d); - EmTrajectory current = CreateTrajectory("current", t0.AddSeconds(1d), 2, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward, 4d, 0d); - TrajectoryObservationHandoffMetrics metrics = new TrajectoryObservationHandoffAnalyzer().Analyze(current, previous, segment); - - Verification.True(metrics.Available, "handoff samples absolute-time aligned trajectories"); - Verification.NearlyEqual(1d, metrics.DeltaPositionMeters!.Value, "handoff world delta"); - Verification.NearlyEqual(1d, metrics.DeltaReferenceSMeters!.Value, "handoff compares shared ReferenceS"); - Verification.True(metrics.DeltaReferenceSMeters.Value != current.Points[0].PathS - previous.Points[0].PathS, - "handoff does not compare local PathS origins"); - - EmTrajectory offSegment = CreateTrajectory("off-segment", t0.AddSeconds(1d), 2, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward, 300d, 0d); - TrajectoryObservationHandoffMetrics unavailable = new TrajectoryObservationHandoffAnalyzer().Analyze(offSegment, previous, segment); - Verification.True(unavailable.Available, "handoff keeps non-projection deltas available"); - Verification.True(!unavailable.DeltaReferenceSMeters.HasValue, "projection failure makes only ReferenceS unavailable"); - Verification.True(unavailable.DeltaPositionMeters.HasValue && unavailable.DeltaVelocityMetersPerSecond.HasValue && - unavailable.DeltaAccelerationMetersPerSecondSquared.HasValue, "projection failure preserves world and kinematic deltas"); - } - - private static void VerifiesDynamicSnapshotContainsObservationEvidenceWithoutCyclePoints() - { - var settings = new TrajectoryObservationSettings(); - TrajectoryObservationBootstrapResult bootstrap = Bootstrap(settings); - DateTimeOffset now = DateTimeOffset.UnixEpoch.AddMinutes(1); - EmTrajectory current = CreateTrajectory("dynamic", now, 21, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward); - var controller = new TrajectoryObservationController(bootstrap, settings, new FixedPlanningService(current), "dynamic"); - var loop = new TrajectoryObservationLoop(controller); - var state = new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0.1d, null, now, 1L); - controller.StartCycle(now, state, CancellationToken.None).GetAwaiter().GetResult(); - TrajectoryObservationLoopTick tick = loop.Tick(now.AddSeconds(settings.ObserverPeriodSeconds), state, CancellationToken.None); - EmPlannerConfiguration configuration = ReadEffectiveConfiguration(controller); - EmTrajectory previous = CreateTrajectory("dynamic-previous", now.AddSeconds(-1d), 21, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward); - - PlanningVisualizationDynamicSnapshot snapshot = new TrajectoryObservationDynamicSnapshotBuilder() - .Build(5L, tick, controller.ActiveSegment, previous, bootstrap.Vehicle, configuration); - - Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "current"), "dynamic snapshot contains current trajectory"); - Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "previous"), "dynamic snapshot contains previous trajectory"); - Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "active-segment"), "dynamic snapshot contains active segment highlight"); - Verification.Equal(1, snapshot.DynamicPolylines.Count(x => x.Id == "current"), - "same current trajectory is published once"); - Verification.True(!snapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"), - "identical current trajectory is not duplicated as current horizon"); - foreach (string rawName in new[] { "ActiveSegmentIndex", "ActiveDirection", "PlanningStatus", "LongitudinalMode", - "TerminalType", "PlanningElapsedMilliseconds", "TrajectoryAgeSeconds", "LsProjectionFailures", - "RollingTerminalConstraint", "TerminalVelocity", "TerminalAcceleration", "DeltaPositionMeters", - "DeltaReferenceSMeters", "DeltaVelocityMetersPerSecond", "DeltaAccelerationMetersPerSecondSquared", - "GearConfirmation" }) - Verification.True(snapshot.StatusValues.Any(x => x.RawName == rawName), "dynamic status includes " + rawName); - Verification.True(snapshot.StatusValues.Single(x => x.RawName == "RollingTerminalConstraint").Value.Contains("滚动末端停车硬约束:未启用"), - "rolling status describes disabled exact-stop constraint"); - Verification.NearlyEqual(current.Points[current.Points.Count - 1].SignedLongitudinalVelocity, - snapshot.CycleSummary.TerminalVelocity!.Value, "cycle summary uses actual terminal velocity"); - Verification.NearlyEqual(ReadAcceleration(current.Points[current.Points.Count - 1]), - snapshot.CycleSummary.TerminalAcceleration!.Value, "cycle summary uses actual terminal acceleration"); - Verification.True(!typeof(VisualizationCycleSummary).GetProperties().Any(x => - x.PropertyType == typeof(EmTrajectory) || x.PropertyType == typeof(EmTrajectoryPoint) || - (x.PropertyType.IsGenericType && x.PropertyType.GetGenericArguments().Any(t => t == typeof(EmTrajectoryPoint)))), - "cycle summary does not carry complete point arrays"); - - EmTrajectory exactStop = CreateTrajectory("exact-stop", now, 21, EmTerminalType.GearSwitch, - EmLongitudinalMode.ExactStopAtBoundary, TravelDirection.Forward); - var exactController = new TrajectoryObservationController(bootstrap, settings, - new FixedPlanningService(exactStop), "exact-stop"); - exactController.StartCycle(now, state, CancellationToken.None).GetAwaiter().GetResult(); - TrajectoryObservationLoopTick exactTick = new TrajectoryObservationLoop(exactController) - .Tick(now.AddSeconds(settings.ObserverPeriodSeconds), state, CancellationToken.None); - PlanningVisualizationDynamicSnapshot exactSnapshot = new TrajectoryObservationDynamicSnapshotBuilder().Build(6L, - exactTick, exactController.ActiveSegment, null, bootstrap.Vehicle, - ReadEffectiveConfiguration(exactController)); - Verification.True(exactSnapshot.StatusValues.Single(x => x.RawName == "RollingTerminalConstraint").Value.Contains("精确停车锚点"), - "exact-stop status describes the true stop anchor"); - Verification.NearlyEqual(exactStop.Points[exactStop.Points.Count - 1].SignedLongitudinalVelocity, - exactSnapshot.CycleSummary.TerminalVelocity!.Value, "exact-stop terminal velocity is real final knot"); - Verification.NearlyEqual(ReadAcceleration(exactStop.Points[exactStop.Points.Count - 1]), - exactSnapshot.CycleSummary.TerminalAcceleration!.Value, "exact-stop terminal acceleration is real final knot"); - - EmTrajectory offSegment = CreateTrajectory("dynamic-off-segment", now, 21, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward, 100d); - var offSegmentController = new TrajectoryObservationController(bootstrap, settings, - new FixedPlanningService(offSegment), "dynamic-off-segment"); - offSegmentController.StartCycle(now, state, CancellationToken.None).GetAwaiter().GetResult(); - TrajectoryObservationLoopTick offSegmentTick = new TrajectoryObservationLoop(offSegmentController) - .Tick(now.AddSeconds(settings.ObserverPeriodSeconds), state, CancellationToken.None); - PlanningVisualizationDynamicSnapshot recovered = new TrajectoryObservationDynamicSnapshotBuilder().Build(7L, - offSegmentTick, offSegmentController.ActiveSegment, null, bootstrap.Vehicle, - ReadEffectiveConfiguration(offSegmentController)); - Verification.Equal("21", recovered.StatusValues.Single(x => x.RawName == "LsProjectionFailures").Value, - "projection failure is reported while dynamic snapshot construction remains available"); - } - - private static void VerifiesFullModeSnapshotPublishesSingleCurrentTrajectoryAndBoundaryMarkers() - { - var settings = new TrajectoryObservationSettings(); - TrajectoryObservationBootstrapResult goalBootstrap = Bootstrap(settings); - TrajectoryObservationBootstrapResult gearBootstrap = TrajectoryObservationBootstrapResult.Success( - goalBootstrap.Job, goalBootstrap.CoarseResult, goalBootstrap.SmoothedPath, - new[] { CreateGearSwitchSegment(0, 0d) }); - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - - PlanningVisualizationDynamicSnapshot gearSnapshot = BuildDynamicSnapshot(gearBootstrap, settings, configuration, - CreateTrajectory("full-gear", DateTimeOffset.UnixEpoch.AddSeconds(1d), 21, EmTerminalType.GearSwitch, - EmLongitudinalMode.ExactStopAtBoundary, TravelDirection.Forward, 0d, 0d, - EmPlanningScope.FullDirectionSegment)); - Verification.Equal(1, gearSnapshot.DynamicPolylines.Count(x => x.Kind == "current"), - "full mode publishes one current trajectory"); - Verification.True(!gearSnapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"), - "full mode does not duplicate current horizon"); - Verification.True(gearSnapshot.DynamicMarkers.Any(x => x.Kind == "vehicle"), - "dynamic snapshot marks the vehicle"); - Verification.True(gearSnapshot.DynamicMarkers.Any(x => x.Kind == "plan-start"), - "dynamic snapshot marks the plan start"); - VisualizationMarker gearEnd = gearSnapshot.DynamicMarkers.Single(x => x.Kind == "gear-switch-end"); - Verification.True(gearEnd.LabelChinese.Contains("换向点") && gearEnd.LabelChinese.Contains("s_end"), - "gear switch boundary uses the explicit s_end label"); - - PlanningVisualizationDynamicSnapshot goalSnapshot = BuildDynamicSnapshot(goalBootstrap, settings, configuration, - CreateTrajectory("full-goal", DateTimeOffset.UnixEpoch.AddSeconds(2d), 21, EmTerminalType.Goal, - EmLongitudinalMode.ExactStopAtBoundary, TravelDirection.Forward, 0d, 0d, - EmPlanningScope.FullDirectionSegment)); - Verification.Equal(1, goalSnapshot.DynamicPolylines.Count(x => x.Kind == "current"), - "goal full mode publishes one current trajectory"); - Verification.True(!goalSnapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"), - "goal full mode does not duplicate current horizon"); - VisualizationMarker finalGoal = goalSnapshot.DynamicMarkers.Single(x => x.Kind == "final-goal"); - Verification.True(finalGoal.LabelChinese.Contains("终点") && finalGoal.LabelChinese.Contains("s_end"), - "final goal boundary uses the explicit s_end label"); - } - - private static void VerifiesWebPublisherFusesFaultWithoutStoppingObserverTicks() - { - var settings = new TrajectoryObservationSettings { EnableWebVisualization = true }; - var sink = new RecordingVisualizationSink { ThrowOnPublish = true }; - var logs = new List(); - var publisher = new TrajectoryObservationVisualizationPublisher(settings, sink, logs.Add); - DateTimeOffset now = DateTimeOffset.UnixEpoch; - - publisher.Start(CreateStaticSnapshot(settings)); - Verification.True(!publisher.TryPublish(now, () => CreateDynamicSnapshot(now)), - "publisher reports a failed web publish"); - Verification.True(!publisher.TryPublish(now.AddSeconds(1d), () => throw new InvalidOperationException("must not build after fuse")), - "publisher permanently disables web output after the first fault"); - Verification.Equal(1, sink.PublishCalls, "publisher invokes the throwing sink once"); - Verification.Equal(1, sink.StopCalls, "publisher stops the sink once after its fault"); - Verification.Equal(1, logs.Count, "publisher logs the web fault once"); - Verification.True(publisher.FaultReason.Contains("网页"), "publisher records an immutable Chinese web fault reason"); - - TrajectoryObservationBootstrapResult bootstrap = Bootstrap(settings); - DateTimeOffset tickAt = now.AddSeconds(1d); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedPlanningService(CreateTrajectory("publisher-loop", tickAt, 2, EmTerminalType.Goal, - EmLongitudinalMode.RollingContinuation, TravelDirection.Forward)), "publisher-loop"); - var loop = new TrajectoryObservationLoop(controller); - var state = new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0d, null, tickAt, 1L); - TrajectoryObservationLoopTick firstTick = loop.Tick(tickAt, state, CancellationToken.None); - TrajectoryObservationLoopTick secondTick = loop.Tick(tickAt.AddSeconds(settings.ObserverPeriodSeconds), state, - CancellationToken.None); - Verification.True(firstTick.PlanningStarted || secondTick.PlanningStarted || secondTick.PlanningCompleted, - "observer loop continues producing ticks after a web publisher fault"); - } - - private static void VerifiesWebPublisherGatesAtConfiguredCadenceAndSkipsDisabledOutput() - { - var settings = new TrajectoryObservationSettings - { - EnableWebVisualization = true, - WebRefreshRateHz = 10d, - }; - var sink = new RecordingVisualizationSink(); - var publisher = new TrajectoryObservationVisualizationPublisher(settings, sink, _ => { }); - DateTimeOffset start = DateTimeOffset.UnixEpoch; - publisher.Start(CreateStaticSnapshot(settings)); - for (int index = 0; index <= 20; index++) - publisher.TryPublish(start.AddMilliseconds(index * 50), () => CreateDynamicSnapshot(start)); - Verification.True(sink.PublishCalls <= 11, "20 Hz observer ticks publish no more than 11 snapshots at 10 Hz over one second"); - Verification.Equal(11, sink.PublishCalls, "publisher includes the initial 10 Hz snapshot"); - publisher.Stop(); - publisher.Stop(); - Verification.Equal(1, sink.StopCalls, "publisher stop is idempotent"); - - var disabledSettings = new TrajectoryObservationSettings { EnableWebVisualization = false }; - var disabledSink = new RecordingVisualizationSink(); - var disabledPublisher = new TrajectoryObservationVisualizationPublisher(disabledSettings, disabledSink, _ => { }); - int factoryCalls = 0; - disabledPublisher.Start(CreateStaticSnapshot(disabledSettings)); - disabledPublisher.TryPublish(start, () => - { - factoryCalls++; - return CreateDynamicSnapshot(start); - }); - Verification.Equal(0, disabledSink.StartCalls, "disabled web visualization does not start a service"); - Verification.Equal(0, factoryCalls, "disabled web visualization does not build dynamic snapshots"); - } - - private static void VerifiesPainterBoundarySemanticsMatchWeb() - { - string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); - - Verification.True(source.Contains("DrawBoundaryMarkers"), - "observer painter draws shared boundary markers"); - foreach (string expected in new[] - { - "计划起点", "车辆", "换向点 ", " / s_end", "终点 / s_end", - }) - { - Verification.True(source.Contains(expected), - "observer painter boundary semantics include " + expected); - } - } - - private static PlanningVisualizationStaticSnapshot CreateStaticSnapshot(TrajectoryObservationSettings settings) - { - TrajectoryObservationBootstrapResult bootstrap = Bootstrap(settings); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedPlanningService(CreateTrajectory("publisher-static", DateTimeOffset.UnixEpoch, 2, - EmTerminalType.Goal, EmLongitudinalMode.RollingContinuation, TravelDirection.Forward)), "publisher-static"); - return new TrajectoryObservationStaticSnapshotBuilder().Build(bootstrap, - ReadEffectiveConfiguration(controller), settings.CreateValidatedSnapshot(), 0L); - } - - private static PlanningVisualizationDynamicSnapshot CreateDynamicSnapshot(DateTimeOffset now) - { - return new PlanningVisualizationDynamicSnapshot(1L, now, "OBSERVE_ONLY", 0, "Forward", - new VisualizationPose(0d, 0d, 0d), Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty(), - new VisualizationCycleSummary(1L, now, "Success", true, 0d, 0, "Forward", "RollingContinuation", - "Goal", 0d, 0d, string.Empty)); - } - - private sealed class RecordingVisualizationSink : ITrajectoryObservationVisualizationSink - { - public bool ThrowOnPublish { get; set; } - public int StartCalls { get; private set; } - public int PublishCalls { get; private set; } - public int StopCalls { get; private set; } - - public PlanningVisualizationSessionInfo Start(PlanningVisualizationOptions options, - PlanningVisualizationStaticSnapshot snapshot) - { - StartCalls++; - return new PlanningVisualizationSessionInfo(new Uri("http://127.0.0.1:12345/?token=test"), "test"); - } - - public void Publish(PlanningVisualizationDynamicSnapshot snapshot) - { - PublishCalls++; - if (ThrowOnPublish) throw new InvalidOperationException("synthetic publish failure"); - } - - public void Stop() - { - StopCalls++; - } - } - - private static TrajectoryObservationBootstrapResult Bootstrap(TrajectoryObservationSettings settings) - { - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, - new[] { TrajectoryObservationObstacle.Circle(1500d, 1500d, 100d) }, 17L); - TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper().Bootstrap(job, CancellationToken.None); - Verification.True(bootstrap.Succeeded, "visualization bootstrap succeeds"); - return bootstrap; - } - - private static EmPlannerConfiguration ReadEffectiveConfiguration(TrajectoryObservationController controller) - { - MethodInfo method = typeof(TrajectoryObservationController).GetMethod("CreateEffectiveConfigurationSnapshot", - BindingFlags.Instance | BindingFlags.NonPublic)!; - return (EmPlannerConfiguration)method.Invoke(controller, Array.Empty())!; - } - - private static IReadOnlyDictionary Flatten(PlanningVisualizationStaticSnapshot snapshot) - { - return snapshot.ConfigurationGroups.SelectMany(x => x.Entries) - .GroupBy(x => x.RawName).ToDictionary(x => x.Key, x => x.Last()); - } - - private static double ReadAcceleration(EmTrajectoryPoint point) - { - PropertyInfo property = typeof(EmTrajectoryPoint).GetProperty("LongitudinalAcceleration", - BindingFlags.Instance | BindingFlags.NonPublic)!; - return (double)property.GetValue(point)!; - } - - private static void AssertValue(IReadOnlyDictionary values, string rawName, string expected, string name) - { - Verification.Equal(expected, values[rawName].Value, name); - } - - private static void AssertEffectiveValues(IReadOnlyDictionary values, - EmPlannerConfiguration configuration, TrajectoryObservationSettings settings, - TrajectoryObservationBootstrapResult bootstrap) - { - string F(double value) => value.ToString("R", CultureInfo.InvariantCulture); - string I(int value) => value.ToString(CultureInfo.InvariantCulture); - string B(bool value) => value ? "true" : "false"; - foreach ((string RawName, string Value) expected in new[] - { - ("ReplanPeriodSeconds", F(configuration.Scheduling.ReplanPeriodSeconds)), - ("TimeHorizonSeconds", F(configuration.Scheduling.TimeHorizonSeconds)), - ("DistanceHorizonMeters", F(configuration.Scheduling.DistanceHorizonMeters)), - ("OutputTimeStepSeconds", F(configuration.Scheduling.OutputTimeStepSeconds)), - ("SolverTimeoutSeconds", F(configuration.Scheduling.SolverTimeoutSeconds)), - ("HandoffLookaheadSeconds", F(configuration.Scheduling.HandoffLookaheadSeconds)), - ("MaximumVehicleStateAgeSeconds", F(configuration.Scheduling.MaximumVehicleStateAgeSeconds)), - ("MaximumOuterIterations", I(configuration.Solver.MaximumOuterIterations)), - ("MaximumOsqpIterations", I(configuration.Solver.MaximumOsqpIterations)), - ("AbsoluteTolerance", F(configuration.Solver.AbsoluteTolerance)), - ("RelativeTolerance", F(configuration.Solver.RelativeTolerance)), - ("StrictResidualTolerance", F(configuration.Solver.StrictResidualTolerance)), - ("WarmStart", B(configuration.Solver.WarmStart)), ("Polish", B(configuration.Solver.Polish)), - ("NativeVerbose", B(configuration.Solver.NativeVerbose)), - ("MaximumForwardSpeedMetersPerSecond", F(configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond)), - ("MaximumReverseSpeedMetersPerSecond", F(configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond)), - ("MaximumAccelerationMetersPerSecondSquared", F(configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared)), - ("MaximumDecelerationMetersPerSecondSquared", F(configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared)), - ("MaximumJerkMetersPerSecondCubed", F(configuration.Longitudinal.MaximumJerkMetersPerSecondCubed)), - ("MaximumLateralAccelerationMetersPerSecondSquared", F(configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared)), - ("MaximumCurvatureRatePerMeterPerSecond", F(configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond)), - ("StopSpeedToleranceMetersPerSecond", F(configuration.Longitudinal.StopSpeedToleranceMetersPerSecond)), - ("ZeroSpeedHoldSeconds", F(configuration.Longitudinal.ZeroSpeedHoldSeconds)), - ("ReferenceSpeed", F(configuration.Longitudinal.Weights.ReferenceSpeed)), - ("Acceleration", F(configuration.Longitudinal.Weights.Acceleration)), - ("Jerk", F(configuration.Longitudinal.Weights.Jerk)), - ("TerminalAcceleration", F(configuration.Longitudinal.Weights.TerminalAcceleration)), - ("ReferenceOffset", F(configuration.Lateral.Weights.ReferenceOffset)), - ("HeadingDeviation", F(configuration.Lateral.Weights.HeadingDeviation)), - ("SecondDerivative", F(configuration.Lateral.Weights.SecondDerivative)), - ("ThirdDerivative", F(configuration.Lateral.Weights.ThirdDerivative)), - ("Curvature", F(configuration.Lateral.Weights.Curvature)), - ("CurvatureVariation", F(configuration.Lateral.Weights.CurvatureVariation)), - ("RollingTerminal", F(configuration.Lateral.Weights.RollingTerminal)), - ("MaximumLateralStepPerIterationMeters", F(configuration.Lateral.MaximumLateralStepPerIterationMeters)), - ("MaximumLateralSlope", F(configuration.Lateral.MaximumLateralSlope)), - ("MaximumLateralSecondDerivativePerMeter", F(configuration.Lateral.MaximumLateralSecondDerivativePerMeter)), - ("MaximumLateralThirdDerivativePerSquareMeter", F(configuration.Lateral.MaximumLateralThirdDerivativePerSquareMeter)), - ("LongitudinalSampleSpacingMeters", F(configuration.Corridor.LongitudinalSampleSpacingMeters)), - ("LateralSampleSpacingMeters", F(configuration.Corridor.LateralSampleSpacingMeters)), - ("MaximumLateralOffsetMeters", F(configuration.Corridor.MaximumLateralOffsetMeters)), - ("AdditionalClearanceReserveMeters", F(configuration.Corridor.AdditionalClearanceReserveMeters)), - ("MaximumCollisionCheckStepMeters", F(configuration.Corridor.MaximumCollisionCheckStepMeters)), - ("MaximumProjectionDistanceMeters", F(configuration.Frenet.MaximumProjectionDistanceMeters)), - ("MinimumFrenetDenominator", F(configuration.Frenet.MinimumFrenetDenominator)), - ("BoundaryAnchorToleranceMeters", F(configuration.Frenet.BoundaryAnchorToleranceMeters)), - ("SpatialToleranceMeters", F(configuration.Validation.SpatialToleranceMeters)), - ("KinematicTolerance", F(configuration.Validation.KinematicTolerance)), - ("LengthMeters", F(bootstrap.Vehicle.LengthMeters)), ("WidthMeters", F(bootstrap.Vehicle.WidthMeters)), - ("SafetyMarginMeters", F(bootstrap.Vehicle.SafetyMarginMeters)), - ("MaximumCurvaturePerMeter", F(bootstrap.Vehicle.MaximumCurvaturePerMeter!.Value)), - ("ObstacleSnapshotVersion", "17"), ("ResolutionMm", F(bootstrap.Map.ResolutionMm)), - ("Bounds.XMin", F(bootstrap.Map.Bounds.XMin)), ("Bounds.XMax", F(bootstrap.Map.Bounds.XMax)), - ("Bounds.YMin", F(bootstrap.Map.Bounds.YMin)), ("Bounds.YMax", F(bootstrap.Map.Bounds.YMax)), - ("Rows", I(bootstrap.Map.Rows)), ("Cols", I(bootstrap.Map.Cols)), - ("EnableWebVisualization", B(settings.EnableWebVisualization)), ("AutoOpenWebVisualization", B(settings.AutoOpenWebVisualization)), - ("WebVisualizationPort", I(settings.WebVisualizationPort)), ("WebRefreshRateHz", F(settings.WebRefreshRateHz)), - ("VisualizationHistoryCycleLimit", I(settings.VisualizationHistoryCycleLimit)), - ("EnableNativePainterVisualization", B(settings.EnableNativePainterVisualization)), - ("DirectionConfirmationSpeedMetersPerSecond", F(settings.DirectionConfirmationSpeedMetersPerSecond)), - ("DirectionConfirmationSamples", I(settings.DirectionConfirmationSamples)), - ("GearSwitchProjectionToleranceMeters", F(settings.GearSwitchProjectionToleranceMeters)), - ("GearSwitchStopHoldSeconds", F(settings.GearSwitchStopHoldSeconds)), - }) - AssertValue(values, expected.RawName, expected.Value, "effective configuration value " + expected.RawName); - } - - private static VisualizationChart FindChart(IReadOnlyList charts, string id) - { - return charts.Single(x => x.Id == id); - } - - private static PlanningVisualizationDynamicSnapshot BuildDynamicSnapshot( - TrajectoryObservationBootstrapResult bootstrap, TrajectoryObservationSettings settings, - EmPlannerConfiguration configuration, EmTrajectory trajectory) - { - DateTimeOffset now = DateTimeOffset.UnixEpoch.AddMinutes(1); - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedPlanningService(trajectory), "snapshot-" + trajectory.Metadata.TrajectoryId); - var loop = new TrajectoryObservationLoop(controller); - var state = new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0.1d, null, now, 1L); - controller.StartCycle(now, state, CancellationToken.None).GetAwaiter().GetResult(); - TrajectoryObservationLoopTick tick = loop.Tick(now.AddSeconds(settings.ObserverPeriodSeconds), - state, CancellationToken.None); - return new TrajectoryObservationDynamicSnapshotBuilder().Build( - 1L, tick, controller.ActiveSegment, null, bootstrap.Vehicle, configuration); - } - - private static DirectionSegmentView CreateStraightSegment(int index, double sourceStart) - { - var points = new List - { - new MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, - TravelDirection.Forward, 0d, 0d, 1d, false, MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPointSource.Anchor), - new MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint(10d, 0d, 0d, 0d, 10d, - TravelDirection.Forward, 0d, 0d, 1d, false, MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPointSource.Anchor), - }; - return new DirectionSegmentView(index, TravelDirection.Forward, points, - new ReferenceBoundary(index, 0d, EmBoundaryType.None, sourceStart), - new ReferenceBoundary(index, 10d, EmBoundaryType.Goal, sourceStart + 10d), sourceStart); - } - - private static DirectionSegmentView CreateGearSwitchSegment(int index, double sourceStart) - { - var points = new List - { - new MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, - TravelDirection.Forward, 0d, 0d, 1d, false, MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPointSource.Anchor), - new MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint(10d, 0d, 0d, 0d, 10d, - TravelDirection.Forward, 0d, 0d, 1d, false, MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPointSource.Anchor), - }; - return new DirectionSegmentView(index, TravelDirection.Forward, points, - new ReferenceBoundary(index, 0d, EmBoundaryType.None, sourceStart), - new ReferenceBoundary(index, 10d, EmBoundaryType.GearSwitchApproach, sourceStart + 10d), sourceStart); - } - - private static EmTrajectory CreateTrajectory(string id, DateTimeOffset effectiveAt, int count, EmTerminalType terminal, - EmLongitudinalMode mode, TravelDirection direction, double startX = 0d, double startPathS = 1000d, - EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) - { - var points = new List(); - for (int index = 0; index < count; index++) - { - double t = index * 0.1d; - points.Add(new EmTrajectoryPoint(startX + t, 0d, 0d, 0.1d + t, t, 0.01d, 0, - startX + t, startPathS + t, direction, index == count - 1 ? EmBoundaryType.Goal : EmBoundaryType.None, - 0.2d + t, 0.3d + t)); - } - return new EmTrajectory(new EmTrajectoryMetadata(id, effectiveAt, effectiveAt, 1L, "visualization-reference", 1L, - string.Empty, 0, direction, terminal, mode, planningScope), points); - } - - private sealed class FixedPlanningService : IEmPlanningService - { - private readonly EmTrajectory trajectory; - - public FixedPlanningService(EmTrajectory trajectory) { this.trajectory = trajectory; } - - public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken) - { - return new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty); - } - } -} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/Verification.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/Verification.cs deleted file mode 100644 index 4d0b1e0..0000000 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/Verification.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace EMPlannerVerificationHost; - -internal static class Verification -{ - public static void Equal(T expected, T actual, string name) - { - if (!Equals(expected, actual)) - throw new InvalidOperationException(name + " expected " + expected + " but was " + actual + "."); - } - - public static void NearlyEqual(double expected, double actual, string name) - { - if (Math.Abs(expected - actual) > 1e-12d) - throw new InvalidOperationException(name + " expected " + expected + " but was " + actual + "."); - } - - public static void True(bool condition, string name) - { - if (!condition) - throw new InvalidOperationException(name + " was false."); - } -} diff --git a/ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj b/ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj deleted file mode 100644 index 94e673d..0000000 --- a/ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - Exe - net10.0-windows - true - disable - enable - - - - - - diff --git a/ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs b/ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs deleted file mode 100644 index e9d3f9d..0000000 --- a/ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs +++ /dev/null @@ -1,590 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Globalization; -using System.IO; -using System.Text.RegularExpressions; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.PathSmoothing; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; - -internal static class Program -{ - private static readonly byte[] PngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 }; - - private static int Main(string[] arguments) - { - try - { - if (arguments.Length == 3 && arguments[0] == "--verify-local-g2-diagnostic") - { - VerifyLocalG2Diagnostic(arguments[1], arguments[2]); - Console.WriteLine("Local G2 diagnostic visualization verification completed."); - return 0; - } - if (arguments.Length == 4 && arguments[0] == "--export-local-g2-diagnostic") - { - ExportLocalG2Diagnostic(arguments[1], arguments[2], arguments[3]); - return 0; - } - if (arguments.Length == 3 && arguments[0] == "--export-fixtures") - { - ExportFixtureReports(arguments[1], arguments[2]); - return 0; - } - if (arguments.Length == 2 && arguments[0] == "--export-end-to-end") - { - ExportEndToEndReports(arguments[1]); - return 0; - } - Require(arguments.Length == 1 && File.Exists(arguments[0]), "A fixture path is required."); - Verify(arguments[0]); - Console.WriteLine("PNG verification host completed."); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine(exception.ToString()); - return 1; - } - } - - private static void ExportFixtureReports(string fixturePath, string outputDirectory) - { - Require(File.Exists(fixturePath), "Fixture path was not found."); - PrintReports(new PathSmoothingComparisonDemo().ExportFixtureReports(fixturePath, outputDirectory)); - } - - private static void ExportEndToEndReports(string outputDirectory) - { - PrintReports(new PathSmoothingComparisonDemo().ExportEndToEndReports(outputDirectory)); - } - - private static void ExportLocalG2Diagnostic(string fixturePath, string evidencePath, string outputDirectory) - { - Require(File.Exists(fixturePath), "Fixture path was not found."); - Require(File.Exists(evidencePath), "Diagnostic evidence path was not found."); - SmoothingReportExportResult report = new LocalG2DiagnosticVisualizationDemo().Export( - fixturePath, - evidencePath, - outputDirectory); - Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); - Console.WriteLine("Diagnostic report=" + outputDirectory); - } - - private static void VerifyLocalG2Diagnostic(string fixturePath, string evidencePath) - { - Require(File.Exists(fixturePath), "Fixture path was not found."); - Require(File.Exists(evidencePath), "Diagnostic evidence path was not found."); - string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-diagnostic-" + Guid.NewGuid().ToString("N")); - string badEvidencePath = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-bad-" + Guid.NewGuid().ToString("N") + ".json"); - string rejectedOutputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-rejected-" + Guid.NewGuid().ToString("N")); - string alteredFixturePath = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-altered-fixture-" + Guid.NewGuid().ToString("N") + ".json"); - string rejectedFixtureOutputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-fixture-rejected-" + Guid.NewGuid().ToString("N")); - try - { - SmoothingReportExportResult report = new LocalG2DiagnosticVisualizationDemo().Export( - fixturePath, - evidencePath, - outputDirectory); - Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); - Require(report.SvgPaths.Count == 5 && report.PngPaths.Count == 5 && File.Exists(report.CsvPath), - "Diagnostic export must publish five SVGs, five PNGs and one CSV."); - Require(Path.GetFileName(report.SvgPaths[4]) == "05-local-g2-diagnostic-candidate.svg", - "Diagnostic SVG stem is incorrect."); - Require(Path.GetFileName(report.PngPaths[4]) == "05-local-g2-diagnostic-candidate.png", - "Diagnostic PNG stem is incorrect."); - string diagnosticSvg = File.ReadAllText(report.SvgPaths[4]); - Require(diagnosticSvg.Contains("data-series=\"raw\"") && diagnosticSvg.Contains("data-series=\"local-g2-diagnostic\""), - "Diagnostic SVG must contain raw and diagnostic samples."); - Require(diagnosticSvg.Contains("净空拒绝") && diagnosticSvg.Contains("未发布") && - diagnosticSvg.Contains("严格输出=原始路径"), - "Diagnostic SVG must disclose clearance rejection, publication state, and strict output boundary."); - VerifyDiagnosticPointOverlay(diagnosticSvg, report.PngPaths[6]); - Require(!diagnosticSvg.Contains("violation-cross"), - "Clearance rejection must not be drawn as an obstacle collision."); - Require(File.ReadAllText(report.CsvPath).Contains("LocalG2Quintic,Unchanged"), - "CSV must retain the normal strict Local G2 row."); - for (int index = 0; index < report.PngPaths.Count; index++) - VerifyPng(File.ReadAllBytes(report.PngPaths[index])); - Require(!ContainsTemporaryFiles(outputDirectory), "Diagnostic export must not leave temporary files."); - - File.Copy(fixturePath, alteredFixturePath); - File.AppendAllText(alteredFixturePath, " "); - bool alteredFixtureRejected = false; - try - { - new LocalG2DiagnosticVisualizationDemo().Export(alteredFixturePath, evidencePath, rejectedFixtureOutputDirectory); - } - catch (Exception) - { - alteredFixtureRejected = true; - } - Require(alteredFixtureRejected, "Fixture bytes that differ from diagnostic evidence must be rejected."); - Require(!Directory.Exists(rejectedFixtureOutputDirectory), - "Fixture hash mismatch must not create an output directory."); - - File.WriteAllText(badEvidencePath, File.ReadAllText(evidencePath).Replace( - "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563", - "0000000000000000000000000000000000000000000000000000000000000000")); - bool badEvidenceRejected = false; - try - { - new LocalG2DiagnosticVisualizationDemo().Export(fixturePath, badEvidencePath, rejectedOutputDirectory); - } - catch (Exception) - { - badEvidenceRejected = true; - } - Require(badEvidenceRejected, "Tampered diagnostic evidence must be rejected."); - Require(!Directory.Exists(rejectedOutputDirectory), - "Rejected diagnostic evidence must not create an output directory."); - } - finally - { - if (File.Exists(alteredFixturePath)) File.Delete(alteredFixturePath); - if (Directory.Exists(rejectedFixtureOutputDirectory)) Directory.Delete(rejectedFixtureOutputDirectory, true); - if (File.Exists(badEvidencePath)) File.Delete(badEvidencePath); - if (Directory.Exists(rejectedOutputDirectory)) Directory.Delete(rejectedOutputDirectory, true); - if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); - } - } - - private static void PrintReports(IReadOnlyList scenarios) - { - for (int scenarioIndex = 0; scenarioIndex < scenarios.Count; scenarioIndex++) - { - PathSmoothingComparisonScenarioResult scenario = scenarios[scenarioIndex]; - if (scenario.Comparison == null) - { - Console.WriteLine(scenario.ScenarioId + " coarse=" + scenario.CoarsePathStatus + " " + scenario.Diagnostic); - continue; - } - PrintEntry(scenario.ScenarioId, scenario.Comparison.RawPathBaseline); - for (int entryIndex = 0; entryIndex < scenario.Comparison.Entries.Count; entryIndex++) - PrintEntry(scenario.ScenarioId, scenario.Comparison.Entries[entryIndex]); - Console.WriteLine(scenario.ScenarioId + " report=" + scenario.Report.Status + " " + scenario.Report.Reason); - } - } - - private static void PrintEntry(string scenarioId, PathSmoothingComparisonEntry entry) - { - Console.WriteLine(string.Format( - CultureInfo.InvariantCulture, - "{0} {1} status={2} length={3:F4} peakKappa={4:F4} clearance={5:F4}", - scenarioId, - entry.IsRawPathBaseline ? "RawPath" : entry.Method.ToString(), - entry.Status, - entry.Metrics.PathLengthMeters, - entry.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, - entry.Metrics.MinimumBodyClearanceMeters)); - } - - private static void Verify(string fixturePath) - { - PathSmoothingComparisonRequest request = CreateComparisonRequest(); - PathSmoothingComparisonResult comparison = new PathSmoothingComparisonService().Compare(request); - Require(!comparison.IsCancelled, "Fixture comparison was cancelled."); - Require(request.SmoothingRequest.CoarsePath.Count > 1, "Fixture coarse path is too short."); - - CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; - CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; - var model = new SmoothingFigureModelBuilder().Build( - comparison, - request.SmoothingRequest.Map, - new Pose2D(first.X, first.Y, first.Heading), - new Pose2D(last.X, last.Y, last.Heading), - "straight", - "Straight"); - - VerifyObstacleRectanglesAreMerged(fixturePath, comparison, new Pose2D(first.X, first.Y, first.Heading), new Pose2D(last.X, last.Y, last.Heading)); - VerifyFourFigureDefinitionContract(model); - VerifyNormalLocalG2Series(model); - VerifyPointOnlyRendererContract(model); - - var resolver = new SmoothingFontResolver(); - Require(resolver.TryResolve("SimSun", "Times New Roman", out SmoothingFontResolution fonts, out string fontReason), - "Required report fonts are unavailable: " + fontReason); - byte[] png; - using (fonts) - { - Require(fonts.ChineseFamilyName == "SimSun", "Chinese font must resolve to exact SimSun family."); - Require(fonts.LatinFamilyName == "Times New Roman", "Latin font must resolve to exact Times New Roman family."); - Require(resolver.MeasureMixedText(fonts, "粗路径 κ(s) X (m) −π", 9f).Width > 0f, - "Mixed Chinese/Latin sample must have nonempty measured bounds."); - png = new SmoothingPngRenderer().Render(model, fonts); - } - VerifyPng(png); - - string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-png-" + Guid.NewGuid().ToString("N")); - try - { - var exporter = new SmoothingReportExporter(); - SmoothingReportExportResult report = exporter.Export(new SmoothingReportExportRequest - { - Model = model, - OutputDirectory = outputDirectory, - FileStem = "straight-report", - }); - Require(report.Status == SmoothingReportExportStatus.Success, "Report export failed: " + report.Reason); - Require(report.SvgPaths.Count == 4 && report.PngPaths.Count == 4 && File.Exists(report.CsvPath), - "Successful export must publish four SVGs, four PNGs and one CSV."); - VerifyPublishedFourFigureFiles(report, outputDirectory); - Require(!ContainsTemporaryFiles(outputDirectory), "Successful export must not leave temporary files."); - - string unavailableDirectory = Path.Combine(outputDirectory, "missing-font"); - SmoothingReportExportResult unavailable = exporter.Export(new SmoothingReportExportRequest - { - Model = model, - OutputDirectory = unavailableDirectory, - FileStem = "must-not-write", - ChineseFontFamilyName = "Missing report font", - LatinFontFamilyName = "Times New Roman", - }); - Require(unavailable.Status == SmoothingReportExportStatus.FontUnavailable, - "Missing exact font must report FontUnavailable."); - Require(!Directory.Exists(unavailableDirectory), "Missing-font export must remain atomic and create no output directory."); - } - finally - { - if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); - } - } - - private static PathSmoothingComparisonRequest CreateComparisonRequest() - { - PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(new PlanningMapRequest - { - Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f), - ResolutionMm = 50f, - AllowExplicitEmptyMap = true, - }); - Require(mapResult.Succeeded && mapResult.Map != null, "PNG verification map must be created."); - var vehicle = new VehicleParameters - { - LengthMeters = 0.20d, - WidthMeters = 0.20d, - SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 100d, - }; - var coarsePath = new List - { - CreatePoint(0.5d, 0.5d, 0d), - CreatePoint(1.0d, 0.5d, 0.5d), - CreatePoint(1.0d, 1.0d, 1.0d), - CreatePoint(1.5d, 1.0d, 1.5d), - }; - var segments = new List - { - new PathSegment(0, TravelDirection.Forward, 0, coarsePath.Count - 1, false, false), - }; - return new PathSmoothingComparisonRequest(new PathSmoothingRequest( - coarsePath, - segments, - mapResult.Map, - vehicle, - new PathSmoothingConfiguration())); - } - - private static void VerifyFourFigureDefinitionContract(SmoothingFigureModel model) - { - SmoothingFigureSet figureSet = new SmoothingFigureSetBuilder().Build(model); - var stems = new List(); - for (int index = 0; index < figureSet.Figures.Count; index++) stems.Add(figureSet.Figures[index].FileStem); - string expected = string.Join(",", new[] - { - "01-coarse-path-overview", - "02-all-paths-comparison", - "03-local-g2-overview", - "04-curvature-comparison", - }); - Require(string.Join(",", stems) == expected, "Four-figure report stems must be stable and ordered."); - Require(!figureSet.Figures[1].ShowsMapContext, "All-path comparison must contain only trajectories, axes and legend."); - Require(figureSet.Figures[2].ShowsMapContext && figureSet.Figures[2].Series[0].Opacity < 1d, - "Local G2 figure must retain a faded raw-path map reference."); - Require(figureSet.Figures[0].WorldScalePointsPerMeter > 0d && - Math.Abs((figureSet.Figures[0].WorldXMaxMeters - figureSet.Figures[0].WorldXMinMeters) / - (figureSet.Figures[0].WorldYMaxMeters - figureSet.Figures[0].WorldYMinMeters) - - figureSet.Figures[0].PlotWidthPoints / figureSet.Figures[0].PlotHeightPoints) < 0.000001d, - "Overhead figures must preserve equal X/Y scale."); - Require(figureSet.Figures[0].LegendYPoints - - (figureSet.Figures[0].PlotYPoints + figureSet.Figures[0].PlotHeightPoints + 31d) >= 8d, - "Legend must leave vertical clearance below the X-axis unit label."); - } - - private static void VerifyNormalLocalG2Series(SmoothingFigureModel model) - { - Require(model.Series.Count == 2, "Normal comparison model must contain only raw-path and Local G2 series."); - SmoothingFigureSeries localG2 = null; - int count = 0; - for (int index = 0; index < model.Series.Count; index++) - { - if (model.Series[index].Key != "local-g2") continue; - localG2 = model.Series[index]; - count++; - } - Require(count == 1, "Normal comparison model must include exactly one Local G2 series."); - Require(Enum.IsDefined(typeof(PathSmoothingStatus), localG2.Status), "Local G2 must retain a defined smoothing status."); - if (localG2.IsCurveVisible) - Require(localG2.Points.Count >= 2, "Visible Local G2 output must include at least two samples."); - } - - private static void VerifyObstacleRectanglesAreMerged(string fixturePath, PathSmoothingComparisonResult comparison, Pose2D start, Pose2D goal) - { - IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); - IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); - int rectangleIndex = -1; - for (int index = 0; index < fixtures.Count; index++) - { - if (fixtures[index].Id == "rectangle-detour") { rectangleIndex = index; break; } - } - Require(rectangleIndex >= 0, "Fixture suite must include rectangle-detour for obstacle rendering verification."); - SmoothingFigureModel model = new SmoothingFigureModelBuilder().Build( - comparison, requests[rectangleIndex].SmoothingRequest.Map, start, goal, "obstacle", "obstacle"); - Require(model.Obstacles.Count > 0, "Rectangle-detour fixture must produce report obstacles."); - for (int firstIndex = 0; firstIndex < model.Obstacles.Count; firstIndex++) - { - SmoothingFigureObstacle first = model.Obstacles[firstIndex]; - for (int secondIndex = firstIndex + 1; secondIndex < model.Obstacles.Count; secondIndex++) - { - SmoothingFigureObstacle second = model.Obstacles[secondIndex]; - bool matchingColumn = Math.Abs(first.X - second.X) < 0.0000001d && Math.Abs(first.Width - second.Width) < 0.0000001d; - bool verticallyAdjacent = Math.Abs((first.Y + first.Height) - second.Y) < 0.0000001d || - Math.Abs((second.Y + second.Height) - first.Y) < 0.0000001d; - Require(!(matchingColumn && verticallyAdjacent), "Adjacent occupied rows must merge into a single obstacle rectangle."); - } - } - } - - private static void VerifyPointOnlyRendererContract(SmoothingFigureModel model) - { - SmoothingFigureDefinition figure = new SmoothingFigureSetBuilder().Build(model).Figures[1]; - string svg = new SmoothingSvgRenderer().Render(figure); - Require(svg.Contains("class=\"trajectory-point\""), "Trajectory samples must render as discrete SVG point markers."); - Require(!svg.Contains("stroke-dasharray") && !svg.Contains("-path\""), "Trajectory SVG output must not use dashed or joined path strokes."); - Require(svg.Contains("X (m)") && svg.Contains("Y (m)"), "Overhead SVG must label metre coordinate axes."); - } - - private static void VerifyDiagnosticPointOverlay(string svg, string pngPath) - { - const string rawHeader = ""; - const string diagnosticHeader = ""; - Require(svg.Contains(rawHeader), "Diagnostic raw series must render as opaque gray outer dots."); - Require(svg.Contains(diagnosticHeader), "Diagnostic candidate must render as opaque red inner dots."); - - IReadOnlyList rawPoints = ReadSvgTrajectoryPoints(svg, "raw"); - IReadOnlyList diagnosticPoints = ReadSvgTrajectoryPoints(svg, "local-g2-diagnostic"); - Require(rawPoints.Count == 88, "Diagnostic SVG must retain the original 88 raw sample centers."); - Require(diagnosticPoints.Count == 88, "Diagnostic SVG must retain the original 88 candidate sample centers."); - for (int index = 0; index < rawPoints.Count; index++) - Require(rawPoints[index].Radius == "2.1", "Diagnostic raw points must use a 2.1 point radius."); - for (int index = 0; index < diagnosticPoints.Count; index++) - Require(diagnosticPoints[index].Radius == "1.35", "Diagnostic candidate points must use a 1.35 point radius."); - - var diagnosticIndicesByCenter = new Dictionary(StringComparer.Ordinal); - for (int index = 0; index < diagnosticPoints.Count; index++) - diagnosticIndicesByCenter[CenterKey(diagnosticPoints[index])] = index; - int sharedCount = 0; - bool hasNonEndpointSharedCenter = false; - for (int index = 0; index < rawPoints.Count; index++) - { - if (!diagnosticIndicesByCenter.TryGetValue(CenterKey(rawPoints[index]), out int diagnosticIndex)) continue; - sharedCount++; - if (index > 0 && index < rawPoints.Count - 1 && diagnosticIndex > 0 && diagnosticIndex < diagnosticPoints.Count - 1) - hasNonEndpointSharedCenter = true; - } - Require(sharedCount == 81 && hasNonEndpointSharedCenter, - "Diagnostic overlay must retain exact shared non-endpoint raw and candidate centers without coordinate offsets."); - VerifyDiagnosticOverlayPixels(pngPath, rawPoints, diagnosticPoints, diagnosticIndicesByCenter); - } - - private static IReadOnlyList ReadSvgTrajectoryPoints(string svg, string seriesKey) - { - string marker = "data-series=\"" + seriesKey + "\""; - int seriesMarker = svg.IndexOf(marker, StringComparison.Ordinal); - Require(seriesMarker >= 0, "Diagnostic SVG series is missing: " + seriesKey); - int seriesStart = svg.LastIndexOf("", seriesMarker, StringComparison.Ordinal); - Require(seriesStart >= 0 && seriesEnd > seriesStart, "Diagnostic SVG series markup is invalid: " + seriesKey); - string series = svg.Substring(seriesStart, seriesEnd - seriesStart); - MatchCollection matches = Regex.Matches(series, - "[^\"]+)\" cy=\"(?[^\"]+)\" r=\"(?[^\"]+)\"/>", - RegexOptions.CultureInvariant); - var points = new List(matches.Count); - for (int index = 0; index < matches.Count; index++) - points.Add(new SvgTrajectoryPoint(matches[index].Groups["x"].Value, matches[index].Groups["y"].Value, matches[index].Groups["r"].Value)); - return points; - } - - private static void VerifyDiagnosticOverlayPixels( - string pngPath, - IReadOnlyList rawPoints, - IReadOnlyList diagnosticPoints, - IReadOnlyDictionary diagnosticIndicesByCenter) - { - const double pixelsPerPoint = 600d / 72d; - const double annulusRadiusPoints = 1.70d; - using (var bitmap = new Bitmap(pngPath)) - { - for (int rawIndex = 1; rawIndex < rawPoints.Count - 1; rawIndex++) - { - SvgTrajectoryPoint raw = rawPoints[rawIndex]; - if (!diagnosticIndicesByCenter.TryGetValue(CenterKey(raw), out int diagnosticIndex) || - diagnosticIndex == 0 || diagnosticIndex == diagnosticPoints.Count - 1) continue; - int centerX = (int)Math.Round(raw.XPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); - int centerY = (int)Math.Round(raw.YPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); - if (!IsDiagnosticRed(bitmap.GetPixel(centerX, centerY))) continue; - - for (int angleIndex = 0; angleIndex < 24; angleIndex++) - { - double angle = 2d * Math.PI * angleIndex / 24d; - int annulusX = (int)Math.Round(centerX + Math.Cos(angle) * annulusRadiusPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); - int annulusY = (int)Math.Round(centerY + Math.Sin(angle) * annulusRadiusPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); - if (annulusX < 0 || annulusX >= bitmap.Width || annulusY < 0 || annulusY >= bitmap.Height) continue; - if (IsRawGray(bitmap.GetPixel(annulusX, annulusY))) return; - } - } - } - throw new InvalidOperationException("A shared non-endpoint sample must have a red center and gray annulus."); - } - - private static bool IsDiagnosticRed(Color color) - { - return color.R >= 140 && color.G <= 95 && color.B <= 100 && color.R >= color.G + 60; - } - - private static bool IsRawGray(Color color) - { - return color.R >= 45 && color.R <= 110 && Math.Abs(color.R - color.G) <= 8 && Math.Abs(color.G - color.B) <= 8; - } - - private static string CenterKey(SvgTrajectoryPoint point) { return point.X + "|" + point.Y; } - - private readonly struct SvgTrajectoryPoint - { - public SvgTrajectoryPoint(string x, string y, string radius) - { - X = x; - Y = y; - Radius = radius; - XPoints = double.Parse(x, CultureInfo.InvariantCulture); - YPoints = double.Parse(y, CultureInfo.InvariantCulture); - } - - public string X { get; } - public string Y { get; } - public string Radius { get; } - public double XPoints { get; } - public double YPoints { get; } - } - - private static void VerifyPublishedFourFigureFiles(SmoothingReportExportResult report, string outputDirectory) - { - var expectedStems = new[] - { - "01-coarse-path-overview", - "02-all-paths-comparison", - "03-local-g2-overview", - "04-curvature-comparison", - }; - VerifyPublishedPaths(report.SvgPaths, expectedStems, ".svg", outputDirectory); - VerifyPublishedPaths(report.PngPaths, expectedStems, ".png", outputDirectory); - Require(!File.Exists(Path.Combine(outputDirectory, "comparison.svg")) && !File.Exists(Path.Combine(outputDirectory, "comparison.png")), - "Four-figure export must not leave legacy composite comparison images."); - } - - private static void VerifyPublishedPaths(IReadOnlyList paths, string[] expectedStems, string extension, string outputDirectory) - { - var actual = new List(); - for (int index = 0; index < paths.Count; index++) actual.Add(paths[index]); - Require(actual.Count == expectedStems.Length, "Four-figure export must publish four " + extension + " files."); - for (int index = 0; index < expectedStems.Length; index++) - { - string expected = Path.Combine(outputDirectory, expectedStems[index] + extension); - Require(actual[index] == expected && File.Exists(actual[index]), "Published " + extension + " path must match the stable figure stem."); - } - } - - private static CoarsePathPoint CreatePoint(double x, double y, double arcLength) - { - return new CoarsePathPoint( - x, y, 0d, 0d, arcLength, TravelDirection.Forward, 0d, 1d, false, CoarsePathPointSource.Start); - } - - private static bool ContainsTemporaryFiles(string directory) - { - foreach (string ignored in Directory.EnumerateFiles(directory, "*.tmp")) return true; - return false; - } - - private static void VerifyPng(byte[] png) - { - Require(png != null && png.Length > PngSignature.Length, "PNG output is empty."); - byte[] data = png ?? throw new InvalidOperationException("PNG output is empty."); - for (int index = 0; index < PngSignature.Length; index++) - Require(data[index] == PngSignature[index], "PNG signature is invalid."); - - bool sawHeader = false; - bool sawPhysicalResolution = false; - int offset = PngSignature.Length; - while (offset < data.Length) - { - Require(offset + 12 <= data.Length, "PNG chunk header is truncated."); - int length = checked((int)ReadUInt32BigEndian(data, offset)); - int dataStart = offset + 8; - int crcStart = checked(dataStart + length); - Require(crcStart + 4 <= data.Length, "PNG chunk data is truncated."); - uint expectedCrc = ReadUInt32BigEndian(data, crcStart); - uint actualCrc = ComputeCrc32(data, offset + 4, length + 4); - Require(expectedCrc == actualCrc, "PNG chunk CRC is invalid."); - - string type = System.Text.Encoding.ASCII.GetString(data, offset + 4, 4); - if (type == "IHDR") - { - Require(length == 13, "IHDR length must be 13."); - Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.WidthPixels, "PNG width must be 4296."); - Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.HeightPixels, "PNG height must be 3120."); - sawHeader = true; - } - else if (type == "pHYs") - { - Require(length == 9, "pHYs length must be 9."); - Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.PixelsPerMeter, - "PNG horizontal density must be 23622 pixels/meter."); - Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.PixelsPerMeter, - "PNG vertical density must be 23622 pixels/meter."); - Require(data[dataStart + 8] == 1, "PNG pHYs unit must be meter."); - sawPhysicalResolution = true; - } - offset = crcStart + 4; - } - Require(sawHeader && sawPhysicalResolution, "PNG must contain IHDR and pHYs chunks."); - } - - private static uint ReadUInt32BigEndian(byte[] data, int offset) - { - return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | - ((uint)data[offset + 2] << 8) | data[offset + 3]; - } - - private static uint ComputeCrc32(byte[] data, int offset, int length) - { - uint crc = 0xffffffffu; - for (int index = 0; index < length; index++) - { - crc ^= data[offset + index]; - for (int bit = 0; bit < 8; bit++) - crc = (crc & 1u) == 0u ? crc >> 1 : (crc >> 1) ^ 0xedb88320u; - } - return crc ^ 0xffffffffu; - } - - private static void Require(bool condition, string message) - { - if (!condition) throw new InvalidOperationException(message); - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs deleted file mode 100644 index 6155040..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using TrajectoryPlanningVisualization; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class ContractChecks -{ - public static void Run() - { - CopiesCollectionsAndRejectsNonFiniteNumbers(); - StoresOccupancyAsDefensiveCompactBits(); - ExposesImmutableSnapshotContracts(); - ExposesChartAnnotationContract(); - DocumentsPublicSafetyAndLifecycleBoundary(); - } - - private static void CopiesCollectionsAndRejectsNonFiniteNumbers() - { - var points = new List { new VisualizationPoint(1d, 2d) }; - var series = new VisualizationSeries("current", "当前轨迹", VisualizationLineStyle.Solid, points); - points[0] = new VisualizationPoint(9d, 9d); - - Verification.NearlyEqual(1d, series.Points[0].X, "series copies points"); - Verification.Throws( - () => new VisualizationPoint(double.NaN, 0d), "point rejects NaN"); - Verification.Throws( - () => new VisualizationPose(0d, 0d, double.PositiveInfinity), "pose rejects infinity"); - Verification.Throws( - () => new VisualizationBounds(0d, 1d, 0d, double.NaN), "bounds rejects NaN"); - Verification.Throws( - () => new VisualizationSeries("invalid", "无效", VisualizationLineStyle.Solid, - new VisualizationPoint[] { null! }), "series rejects null point"); - } - - private static void StoresOccupancyAsDefensiveCompactBits() - { - byte[] bits = { 0x01 }; - var grid = new VisualizationOccupancyGrid(new VisualizationBounds(0d, 1d, 0d, 1d), - 0.5d, rows: 2, columns: 2, bits); - bits[0] = 0x00; - - Verification.Equal("AQ==", grid.OccupancyBitsBase64, "occupancy grid copies compact bits"); - Verification.Throws( - () => new VisualizationOccupancyGrid(new VisualizationBounds(0d, 1d, 0d, 1d), - 0.5d, rows: 2, columns: 2, new byte[2]), "occupancy grid validates bit length"); - } - - private static void ExposesImmutableSnapshotContracts() - { - var staticSnapshot = new PlanningVisualizationStaticSnapshot( - "会话", - new VisualizationBounds(0d, 1d, 0d, 1d), - null, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty()); - var dynamicSnapshot = new PlanningVisualizationDynamicSnapshot( - 1L, - DateTimeOffset.UtcNow, - "运行中", - 0, - "forward", - new VisualizationPose(0d, 0d, 0d), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new VisualizationCycleSummary(1L, DateTimeOffset.UtcNow, "成功", true, 1d, 0, - "forward", "rolling", "none", null, null, "")); - - Verification.True(staticSnapshot.StaticPolylines.GetType().Name.Contains("ReadOnlyCollection"), - "static snapshot uses read-only collection"); - Verification.True(dynamicSnapshot.Charts.GetType().Name.Contains("ReadOnlyCollection"), - "dynamic snapshot uses read-only collection"); - AssertNoSetters(typeof(PlanningVisualizationStaticSnapshot)); - AssertNoSetters(typeof(PlanningVisualizationDynamicSnapshot)); - } - - private static void AssertNoSetters(Type type) - { - foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) - { - Verification.True(property.SetMethod == null, type.Name + " exposes no property setters"); - } - } - - private static void ExposesChartAnnotationContract() - { - Type chartType = typeof(VisualizationChart); - PropertyInfo annotationsProperty = chartType.GetProperty("Annotations"); - Verification.True(annotationsProperty != null, "chart exposes immutable annotations"); - Type annotationType = chartType.Assembly.GetType("TrajectoryPlanningVisualization.VisualizationChartAnnotation"); - Verification.True(annotationType != null, "chart annotation contract type exists"); - - var chart = new VisualizationChart("annotation-legacy", "标题", "x (m)", "y (m)", - new[] - { - new VisualizationSeries("s", "轨迹", VisualizationLineStyle.Solid, - new[] { new VisualizationPoint(0d, 0d) }) - }); - if (annotationsProperty == null) - { - return; - } - - object value = annotationsProperty.GetValue(chart); - Verification.True(value is System.Collections.IEnumerable, "chart annotations are enumerable"); - int count = 0; - foreach (object annotation in (System.Collections.IEnumerable)value) - { - count++; - } - Verification.Equal(0, count, "legacy chart constructor keeps annotations empty"); - AssertNoSetters(chartType); - } - - private static void DocumentsPublicSafetyAndLifecycleBoundary() - { - string readmePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, - "..", "..", "..", "..", "..", "TrajectoryPlanningVisualization", "README.md")); - string readme = File.ReadAllText(readmePath); - - Verification.True(readme.Contains("OBSERVE_ONLY"), "README documents observe-only boundary"); - Verification.True(readme.Contains("127.0.0.1"), "README documents loopback boundary"); - Verification.True(readme.Contains("capacity 1"), "README documents latest-frame capacity"); - Verification.True(readme.Contains("60"), "README documents default history limit"); - Verification.True(readme.Contains("10 Hz"), "README documents default refresh rate"); - Verification.True(readme.Contains("j[i]"), "README documents jerk interval semantics"); - Verification.True(readme.Contains("末点后无时间区间"), "README documents jerk terminal explanation"); - Verification.True(readme.Contains("PlanningVisualizationSessionInfo info = visualization.Start(staticSnapshot);"), - "README documents Start example"); - Verification.True(readme.Contains("visualization.Publish(dynamicSnapshot);"), "README documents Publish example"); - Verification.True(readme.Contains("visualization.Stop();"), "README documents Stop example"); - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs deleted file mode 100644 index 56ff0b7..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using TrajectoryPlanningVisualization; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class Program -{ - private static int Main(string[] args) - { - if (args.Length == 2 && args[0] == "--smoke-seconds" && - int.TryParse(args[1], out int seconds) && seconds > 0) - { - return RunSmoke(seconds); - } - - try - { - ContractChecks.Run(); - RuntimeChecks.Run(); - ServerChecks.Run(); - WebAssetChecks.Run(); - Console.WriteLine("PASS trajectory-planning-visualization"); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine(exception); - return 1; - } - } - - private static int RunSmoke(int seconds) - { - try - { - using var session = new PlanningVisualizationSession( - new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 10, HistoryCycleLimit = 60 }); - PlanningVisualizationSessionInfo info = session.Start(SampleSnapshotFactory.CreateStaticSnapshot()); - Console.WriteLine(info.Uri.AbsoluteUri); - Stopwatch stopwatch = Stopwatch.StartNew(); - long sequence = 1L; - while (stopwatch.Elapsed < TimeSpan.FromSeconds(seconds)) - { - session.Publish(SampleSnapshotFactory.CreateDynamicSnapshot(sequence++)); - TimeSpan remaining = TimeSpan.FromSeconds(seconds) - stopwatch.Elapsed; - if (remaining <= TimeSpan.Zero) - { - break; - } - - Thread.Sleep(remaining < TimeSpan.FromMilliseconds(100) ? remaining : TimeSpan.FromMilliseconds(100)); - } - - session.Stop(); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine(exception); - return 1; - } - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs deleted file mode 100644 index 038014b..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Linq; -using TrajectoryPlanningVisualization; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class RuntimeChecks -{ - public static void Run() - { - KeepsOnlyTheLatestFrame(); - KeepsBoundedDeduplicatedCycleHistory(); - SerializesStableCamelCaseJson(); - RejectsInvalidOptions(); - } - - private static void KeepsOnlyTheLatestFrame() - { - var store = new LatestVisualizationFrameStore(); - store.Publish(Snap(1)); - store.Publish(Snap(2)); - - Verification.True(store.TryReadAfter(0, out VisualizationFrame frame), "latest frame exists"); - Verification.Equal(2L, frame.Snapshot.Sequence, "latest frame replaces old frame"); - Verification.True(!store.TryReadAfter(frame.Version, out _), "same frame is not replayed"); - } - - private static void KeepsBoundedDeduplicatedCycleHistory() - { - var history = new BoundedCycleHistory(2); - history.Add(Cycle(1)); - history.Add(Cycle(2)); - history.Add(Cycle(3)); - history.Add(Cycle(3)); - - Verification.Equal("2|3", string.Join("|", history.Snapshot().Select(x => x.CycleVersion)), - "history is bounded and deduplicated"); - } - - private static void SerializesStableCamelCaseJson() - { - string json = VisualizationJson.Serialize(Snap(2)); - Verification.True(json.Contains("\"sessionStateChinese\"") && json.Contains("\"observedAtUtc\""), - "JSON uses stable camel case names"); - } - - private static void RejectsInvalidOptions() - { - Verification.Throws( - () => new PlanningVisualizationOptions { Port = -1 }.CreateValidatedSnapshot(), - "options reject a negative port"); - Verification.Throws( - () => new PlanningVisualizationOptions { RefreshRateHz = 0 }.CreateValidatedSnapshot(), - "options reject a zero refresh rate"); - Verification.Throws( - () => new PlanningVisualizationOptions { HistoryCycleLimit = 0 }.CreateValidatedSnapshot(), - "options reject a zero history limit"); - Verification.Throws( - () => new PlanningVisualizationOptions { Port = 65536 }.CreateValidatedSnapshot(), - "options reject a port above the TCP range"); - - var snapshot = new PlanningVisualizationOptions().CreateValidatedSnapshot(); - Verification.Equal(0, snapshot.Port, "default port is automatic"); - Verification.Equal(10, snapshot.RefreshRateHz, "default refresh rate is 10 Hz"); - Verification.Equal(60, snapshot.HistoryCycleLimit, "default history is bounded to 60 cycles"); - Verification.Equal(2, snapshot.MaximumClients, "maximum clients is fixed at two"); - } - - private static PlanningVisualizationDynamicSnapshot Snap(long sequence) - { - return new PlanningVisualizationDynamicSnapshot( - sequence, - DateTimeOffset.Parse("2026-08-06T00:00:00+00:00").AddSeconds(sequence), - "运行中", - 0, - "forward", - new VisualizationPose(1d, 2d, 0d), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Cycle(sequence)); - } - - private static VisualizationCycleSummary Cycle(long cycleVersion) - { - return new VisualizationCycleSummary( - cycleVersion, - DateTimeOffset.Parse("2026-08-06T00:00:00+00:00").AddSeconds(cycleVersion), - "成功", - true, - 2.5d, - 0, - "forward", - "rolling", - "none", - null, - null, - ""); - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs deleted file mode 100644 index f422b59..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using TrajectoryPlanningVisualization; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class SampleSnapshotFactory -{ - private static readonly DateTimeOffset Origin = DateTimeOffset.Parse("2026-08-06T00:00:00+00:00"); - - public static PlanningVisualizationStaticSnapshot CreateStaticSnapshot() - { - return new PlanningVisualizationStaticSnapshot( - "合成轨迹规划观察", - new VisualizationBounds(0d, 12d, 0d, 8d), - new VisualizationOccupancyGrid(new VisualizationBounds(0d, 12d, 0d, 8d), 2d, 4, 6, - new byte[] { 0b00100001, 0b00000100, 0b00000010 }), - new[] - { - new VisualizationPolyline("coarse-path", "粗路径", "coarse", VisualizationLineStyle.Dashed, - Points((0d, 1d), (3d, 1.2d), (6d, 1.85d), (8d, 4.2d), (11d, 6d))), - new VisualizationPolyline("local-g2-path", "完整 Local G2 路径", "local-g2", VisualizationLineStyle.Solid, - Points((0d, 1d), (3d, 1d), (6d, 2d), (8d, 4d), (11d, 6d))) - }, - new[] - { - new VisualizationMarker("smooth-start", "smooth-start", "平滑路径起点", new VisualizationPoint(0d, 1d)), - new VisualizationMarker("gear-1", "gear-switch", "换向 1", new VisualizationPoint(6d, 2d)), - new VisualizationMarker("gear-1-end", "gear-switch-end", "换向点 1 / s_end", new VisualizationPoint(6d, 2d)), - new VisualizationMarker("final-goal", "final-goal", "终点 / s_end", new VisualizationPoint(11d, 6d)) - }, - new[] - { - new VisualizationDirectionSegment(0, "forward", false, true, Points((0d, 1d), (3d, 1d), (6d, 2d))), - new VisualizationDirectionSegment(1, "reverse", true, false, Points((6d, 2d), (8d, 4d), (11d, 6d))) - }, - new[] - { - new VisualizationConfigurationGroup("调度", new[] - { - new VisualizationValue("网页刷新率", "RefreshRateHz", "10", "Hz", "normal"), - new VisualizationValue("历史容量", "HistoryCycleLimit", "60", "cycles", "normal") - }), - new VisualizationConfigurationGroup("观察安全边界", new[] - { - new VisualizationValue("观察模式", "OBSERVE_ONLY", "true", "", "notice") - }) - }); - } - - public static PlanningVisualizationDynamicSnapshot CreateDynamicSnapshot(long sequence) - { - double offset = (sequence - 1L) * 0.05d; - return new PlanningVisualizationDynamicSnapshot( - sequence, - Origin.AddMilliseconds(sequence * 100L), - "合成数据运行中", - 0, - "forward", - new VisualizationPose(2d + offset, 1d, 0d), - new[] - { - new VisualizationPolyline("active-segment", "活动方向段", "active-segment", VisualizationLineStyle.Solid, - Points((0d, 1d), (3d, 1d), (6d, 2d))), - new VisualizationPolyline("previous", "上一轮轨迹", "previous", VisualizationLineStyle.Dashed, Points((1d, 1d), (2.5d, 1d), (4d, 1.3d))), - new VisualizationPolyline("current", "当前轨迹", "current", VisualizationLineStyle.Solid, Points((0d, 1d), (3.8d + offset, 1.2d), (5.8d + offset, 1.9d))) - }, - new[] - { - new VisualizationMarker("vehicle", "vehicle", "车辆", new VisualizationPoint(2d + offset, 1d)), - new VisualizationMarker("plan-start", "plan-start", "计划起点", new VisualizationPoint(0d, 1d)), - new VisualizationMarker("gear-switch-end", "gear-switch-end", "换向点 1 / s_end", new VisualizationPoint(6d, 2d)) - }, - CreateCharts(offset), - new[] - { - new VisualizationValue("规划耗时", "PlanningElapsedMilliseconds", "4.2", "ms", "normal"), - new VisualizationValue("轨迹年龄", "TrajectoryAge", "0.1", "s", "normal") - }, - new VisualizationCycleSummary(sequence, Origin.AddMilliseconds(sequence * 100L), "成功", true, 4.2d, 0, - "forward", "rolling", "horizon", 1.2d, 0d, "")); - } - - private static IReadOnlyList CreateCharts(double offset) - { - return new[] - { - Chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", Points((0d, .05d), (2d, .08d), (4d, .02d)), - new[] { new VisualizationChartAnnotation("ls-s-end", "s-end", "s_end", 4d) }), - Chart("st", "时空轨迹", "t (s)", "PathS (m)", Points((0d, 0d), (1d, 1.2d), (2d, 3.0d)), - new[] { new VisualizationChartAnnotation("st-s-end", "s-end", "s_end", 2d) }), - Chart("curvature-s", "曲率—距离", "s (m)", "κ (m⁻¹)", Points((0d, .01d), (2d, .04d), (4d, .02d))), - Chart("curvature-t", "曲率—时间", "t (s)", "κ (m⁻¹)", Points((0d, .01d), (1d, .04d), (2d, .02d))), - Chart("velocity-t", "速度", "t (s)", "v (m/s)", Points((0d, 0d), (1d, 1d), (2d, 1.2d + offset))), - Chart("acceleration-t", "加速度", "t (s)", "a (m/s²)", Points((0d, .8d), (1d, .2d), (2d, 0d))), - Chart("jerk-t", "加加速度", "t (s)", "j (m/s³)", Points((0d, -.6d), (1d, -.2d))), - Chart("yaw-rate-t", "横摆角速度", "t (s)", "ω (rad/s)", Points((0d, 0d), (1d, .08d), (2d, .04d))) - }; - } - - private static VisualizationChart Chart(string id, string title, string xAxis, string yAxis, - IReadOnlyList points, - IReadOnlyList annotations = null) - { - return new VisualizationChart(id, title, xAxis, yAxis, - new[] { new VisualizationSeries(id + "-current", "当前轨迹", VisualizationLineStyle.Solid, points) }, - "", annotations ?? Array.Empty()); - } - - private static IReadOnlyList Points(params (double x, double y)[] values) - { - var points = new List(values.Length); - foreach ((double x, double y) in values) - { - points.Add(new VisualizationPoint(x, y)); - } - - return points; - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs deleted file mode 100644 index 01037ce..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using TrajectoryPlanningVisualization; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class ServerChecks -{ - public static void Run() - { - RejectsUnauthorizedAndMalformedRequests(); - LimitsSseClientsAndKeepsPublishNonBlocking(); - CollectsHistoryWithoutClientsAndStopsCleanly(); - } - - private static void RejectsUnauthorizedAndMalformedRequests() - { - int port; - using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 20 })) - { - session.Publish(DynamicSnapshot(0)); - PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); - Verification.Equal(info.Token, session.Start(StaticSnapshot()).Token, "repeated start is idempotent"); - port = info.Uri.Port; - Verification.Equal("127.0.0.1", info.Uri.Host, "server binds loopback"); - Verification.Equal(64, info.Token.Length, "session token is 256 bits of hex"); - Verification.Equal(403, SendStatus(port, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"), - "missing token is forbidden"); - Verification.Equal(404, SendStatus(port, Get("/missing?token=" + info.Token)), - "unknown authorized route is not found"); - Verification.Equal(405, SendStatus(port, "POST /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"), - "non-GET request is rejected"); - Verification.Equal(400, SendStatus(port, "GET /api/bootstrap HTTP/1.0\r\nHost: localhost\r\n\r\n"), - "non-HTTP/1.1 request is malformed"); - Verification.Equal(400, SendStatus(port, "GET /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\nX-Large: " + new string('a', 16 * 1024) + "\r\n\r\n"), - "oversized request header is rejected"); - - HttpResponse bootstrap = SendResponse(port, Get("/api/bootstrap?token=" + info.Token)); - Verification.Equal(200, bootstrap.Status, "authorized bootstrap succeeds"); - Verification.Equal(bootstrap.ContentLength, Encoding.UTF8.GetByteCount(bootstrap.Body), - "normal response has byte-accurate content length"); - Verification.True(bootstrap.Body.Contains("sessionNameChinese"), "bootstrap contains static snapshot"); - session.Stop(); - session.Stop(); - session.Publish(DynamicSnapshot(99)); - } - - Verification.True(CanBindReleasedPort(port), "stop releases loopback port"); - } - - private static void LimitsSseClientsAndKeepsPublishNonBlocking() - { - using var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 100 }); - PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); - using TcpClient first = OpenSseClient(info); - using TcpClient second = OpenSseClient(info); - Verification.Equal(503, SendStatus(info.Uri.Port, Get("/api/events?token=" + info.Token)), - "third SSE client is rejected"); - - Task publisher = Task.Run(() => - { - for (int i = 1; i <= 10000; i++) - { - session.Publish(DynamicSnapshot(i)); - } - }); - Verification.True(publisher.Wait(TimeSpan.FromSeconds(1)), "slow SSE client does not block Publish"); - session.Stop(); - } - - private static void CollectsHistoryWithoutClientsAndStopsCleanly() - { - int port; - using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 20 })) - { - PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); - port = info.Uri.Port; - for (int i = 1; i <= 3; i++) - { - session.Publish(DynamicSnapshot(i)); - Thread.Sleep(120); - } - - using TcpClient client = OpenSseClient(info); - string frame = ReadUntil(client.GetStream(), "\n\n", TimeSpan.FromSeconds(2)); - Verification.True(frame.Contains("cycleVersion\":1") && frame.Contains("cycleVersion\":2") && - frame.Contains("cycleVersion\":3"), "dispatcher collects bounded history without browsers"); - - Stopwatch stopWatch = Stopwatch.StartNew(); - session.Stop(); - Verification.True(stopWatch.Elapsed <= TimeSpan.FromSeconds(1), "normal stop is bounded"); - string terminal = ReadUntil(client.GetStream(), "\n\n", TimeSpan.FromSeconds(1)); - Verification.True(terminal.Contains("event: end") && terminal.Contains("会话已结束"), - "normal stop offers a terminal SSE event"); - } - - Verification.True(CanBindReleasedPort(port), "port can be rebound after normal stop"); - } - - private static TcpClient OpenSseClient(PlanningVisualizationSessionInfo info) - { - var client = new TcpClient(); - client.Connect(IPAddress.Loopback, info.Uri.Port); - byte[] request = Encoding.ASCII.GetBytes(Get("/api/events?token=" + info.Token)); - NetworkStream stream = client.GetStream(); - stream.Write(request, 0, request.Length); - string headers = ReadUntil(stream, "\r\n\r\n", TimeSpan.FromSeconds(2)); - Verification.True(headers.StartsWith("HTTP/1.1 200", StringComparison.Ordinal), "authorized SSE succeeds"); - return client; - } - - private static int SendStatus(int port, string request) - { - return SendResponse(port, request).Status; - } - - private static HttpResponse SendResponse(int port, string request) - { - using var client = new TcpClient(); - client.Connect(IPAddress.Loopback, port); - NetworkStream stream = client.GetStream(); - byte[] bytes = Encoding.ASCII.GetBytes(request); - stream.Write(bytes, 0, bytes.Length); - string headers = ReadUntil(stream, "\r\n\r\n", TimeSpan.FromSeconds(2)); - string[] lines = headers.Split(new[] { "\r\n" }, StringSplitOptions.None); - string[] statusParts = lines[0].Split(' '); - int status = int.Parse(statusParts[1]); - int contentLength = 0; - foreach (string line in lines) - { - if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) - { - contentLength = int.Parse(line.Substring("Content-Length:".Length).Trim()); - } - } - - byte[] body = ReadExactly(stream, contentLength, TimeSpan.FromSeconds(2)); - return new HttpResponse(status, contentLength, Encoding.UTF8.GetString(body)); - } - - private static string Get(string target) - { - return "GET " + target + " HTTP/1.1\r\nHost: localhost\r\n\r\n"; - } - - private static string ReadUntil(NetworkStream stream, string delimiter, TimeSpan timeout) - { - var bytes = new List(); - byte[] delimiterBytes = Encoding.ASCII.GetBytes(delimiter); - stream.ReadTimeout = (int)timeout.TotalMilliseconds; - while (true) - { - int next = stream.ReadByte(); - if (next < 0) - { - throw new InvalidOperationException("connection ended before expected response delimiter"); - } - - bytes.Add((byte)next); - if (bytes.Count >= delimiterBytes.Length) - { - bool found = true; - for (int index = 0; index < delimiterBytes.Length; index++) - { - if (bytes[bytes.Count - delimiterBytes.Length + index] != delimiterBytes[index]) - { - found = false; - break; - } - } - - if (found) - { - return Encoding.UTF8.GetString(bytes.ToArray()); - } - } - } - } - - private static byte[] ReadExactly(NetworkStream stream, int count, TimeSpan timeout) - { - byte[] result = new byte[count]; - stream.ReadTimeout = (int)timeout.TotalMilliseconds; - int offset = 0; - while (offset < count) - { - int read = stream.Read(result, offset, count - offset); - if (read == 0) - { - throw new InvalidOperationException("connection ended before response body"); - } - - offset += read; - } - - return result; - } - - private static bool CanBindReleasedPort(int port) - { - try - { - using var listener = new TcpListener(IPAddress.Loopback, port); - listener.Start(); - return true; - } - catch (SocketException) - { - return false; - } - } - - private static PlanningVisualizationStaticSnapshot StaticSnapshot() - { - return new PlanningVisualizationStaticSnapshot( - "验证会话", - new VisualizationBounds(0d, 1d, 0d, 1d), - null, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty()); - } - - private static PlanningVisualizationDynamicSnapshot DynamicSnapshot(long sequence) - { - return new PlanningVisualizationDynamicSnapshot( - sequence, - DateTimeOffset.UtcNow, - "运行中", - 0, - "forward", - new VisualizationPose(0d, 0d, 0d), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new VisualizationCycleSummary(sequence, DateTimeOffset.UtcNow, "成功", true, 1d, 0, "forward", - "rolling", "none", null, null, "")); - } - - private sealed class HttpResponse - { - public HttpResponse(int status, int contentLength, string body) - { - Status = status; - ContentLength = contentLength; - Body = body; - } - - public int Status { get; } - public int ContentLength { get; } - public string Body { get; } - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj deleted file mode 100644 index fc78432..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - Exe - net10.0-windows - 10 - - - - - diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs deleted file mode 100644 index 5c0e1ad..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class Verification -{ - public static void Equal(T expected, T actual, string message) - { - if (!Equals(expected, actual)) - { - throw new InvalidOperationException(message + ": expected " + expected + ", actual " + actual); - } - } - - public static void NearlyEqual(double expected, double actual, string message) - { - if (Math.Abs(expected - actual) > 0.0000001d) - { - throw new InvalidOperationException(message + ": expected " + expected + ", actual " + actual); - } - } - - public static void True(bool value, string message) - { - if (!value) - { - throw new InvalidOperationException(message); - } - } - - public static void Throws(Action action, string message) - where TException : Exception - { - try - { - action(); - } - catch (TException) - { - return; - } - - throw new InvalidOperationException(message); - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs deleted file mode 100644 index 48fbd48..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Linq; -using TrajectoryPlanningVisualization; - -namespace TrajectoryPlanningVisualizationVerificationHost; - -internal static class WebAssetChecks -{ - public static void Run() - { - BuildsDeterministicSmokeSnapshots(); - - string html = EmbeddedWebAssets.ReadText("index.html"); - string css = EmbeddedWebAssets.ReadText("app.css"); - string js = EmbeddedWebAssets.ReadText("app.js"); - - Verification.True(html.Contains("路径总览"), "overview Chinese title"); - Verification.True(html.Contains("LS / ST"), "LS/ST tab"); - Verification.True(html.Contains("曲率与运动学"), "kinematics tab"); - Verification.True(html.Contains("周期历史"), "history tab"); - Verification.True(html.Contains("生效配置"), "configuration panel"); - Verification.True(css.Contains("--current-trajectory: #1769aa"), "scientific current color"); - Verification.True(css.Contains("stroke-width: 1.1"), "thin scientific line"); - Verification.True(css.Contains("main > section[hidden] { display: none !important; }"), - "hidden sections cannot be overridden by grid rules"); - Verification.True(css.Contains(".axis-tick-y"), "SVG y ticks have a semantic class"); - Verification.True(css.Contains(".axis-label-y"), "SVG y labels have a semantic class"); - Verification.True(css.Contains(".world-empty-state"), "empty overview has a semantic state"); - Verification.True(css.Contains(".world-coarse"), "coarse path semantic class"); - Verification.True(css.Contains(".world-local-g2"), "Local G2 semantic class"); - Verification.True(css.Contains(".world-segment-active"), "active direction semantic class"); - Verification.True(css.Contains(".world-current"), "current trajectory semantic class"); - Verification.True(css.Contains(".world-coordinate-grid"), - "overview exposes a metric grid semantic class"); - Verification.True(css.Contains(".world-axis-zero"), - "overview exposes a zero-axis semantic class"); - Verification.True(css.Contains(".world-axis-tick"), - "overview exposes coordinate tick semantics"); - Verification.True(css.Contains(".world-axis-label"), - "overview exposes coordinate axis-label semantics"); - Verification.True(js.Contains("function niceWorldStep("), - "overview selects readable metric tick steps"); - Verification.True(js.Contains("function worldMetrics("), - "overview derives stable world-to-screen metrics"); - Verification.True(js.Contains("function renderWorldCoordinates("), - "overview renders its own metric coordinate system"); - Verification.True(js.Contains("X (m)") && js.Contains("Y (m)"), - "overview labels both metric axes"); - Verification.True(css.Contains("--world-smoothed-path"), - "overview defines a dedicated smoothed-path color"); - Verification.True(css.Contains("--world-em-trajectory"), - "overview defines a dedicated EM-path color"); - Verification.True(css.Contains(".world-marker-shape"), - "overview marker shapes use a shared screen-space class"); - Verification.True(css.Contains(".marker-smooth-start"), - "overview styles the smoothed-path start marker"); - Verification.True(js.Contains("function markerTransform("), - "overview anchors fixed-size markers in world coordinates"); - Verification.True(js.Contains("function appendWorldMarker("), - "overview centralizes fixed-size marker rendering"); - Verification.True(!js.Contains("const corners = [[.38, .18]"), - "vehicle marker no longer uses a large world-size rectangle"); - Verification.True(!css.Contains(".marker-vehicle .vehicle-outline { fill: none; stroke: #20252b; stroke-width: .7; }"), - "vehicle outline no longer uses a world-scale heavy stroke"); - Verification.True(!css.Contains(".marker-vehicle .vehicle-heading { stroke: #20252b; stroke-width: .8; }"), - "vehicle marker no longer uses world-scale heavy strokes"); - Verification.True(css.Contains(".marker-vehicle"), "vehicle marker semantic class"); - Verification.True(css.Contains(".marker-plan-start"), "plan start marker semantic class"); - Verification.True(css.Contains(".marker-plan-start { fill: none;"), - "plan start marker remains hollow when it overlaps the smoothed-path start"); - Verification.True(css.Contains(".marker-gear-switch"), "gear-switch marker semantic class"); - Verification.True(css.Contains(".marker-final-goal"), "final goal marker semantic class"); - Verification.True(js.Contains("末点后无时间区间"), "jerk terminal explanation"); - Verification.True(js.Contains("__TRAJECTORY_VISUALIZATION_TEST__"), "test hook flag is explicit"); - Verification.True(js.Contains("Object.freeze"), "test hooks are frozen"); - Verification.True(js.Contains("未收到 Local G2 路径"), "empty overview names the missing source"); - Verification.True(html.Contains("occupancy-grid"), "single occupancy canvas exists"); - Verification.True(html.Contains("world-overlay"), "SVG trajectory overlay exists"); - Verification.True(html.Contains("id=\"world-legend\""), - "overview contains a screen-space legend host"); - Verification.True(html.Contains("aria-label=\"路径图例\""), - "overview legend has a Chinese accessible name"); - Verification.True(css.Contains(".world-legend"), - "overview legend has a screen-space visual container"); - Verification.True(css.Contains(".world-legend-swatch"), - "overview legend exposes semantic line and marker samples"); - Verification.True(js.Contains("function renderWorldLegend("), - "overview builds legend entries from current snapshot data"); - Verification.True(js.Contains("legendChinese"), - "overview reuses exported Chinese polyline legends"); - Verification.True( - js.Contains("smoothStartMarker.labelChinese || \"平滑路径起点\"") && - js.Contains("planStartMarker.labelChinese || \"EM 规划起点\"") && - js.Contains("vehicleMarker.labelChinese || \"当前车辆\"") && - js.Contains("gearMarker.labelChinese || \"换向点 / s_end\"") && - js.Contains("goalMarker.labelChinese || \"平滑路径终点\""), - "overview marker legends prefer the first matching marker label with canonical fallbacks"); - Verification.True(js.Contains("const hasDynamicActiveSegment = dynamicLines.some("), - "overview detects whether the frame supplies a dynamic active segment"); - Verification.True(js.Contains( - ".filter(segment => !hasDynamicActiveSegment || segment.segmentIndex !== frame.activeSegmentIndex)"), - "overview omits the matching static segment when its dynamic active segment exists"); - Verification.True(js.Contains("if (active)") && - !js.Contains("if (active || directionSegments.length)"), - "overview legend activates the current direction segment from dynamic data only"); - int renderWorldStart = js.IndexOf("function renderWorld()", StringComparison.Ordinal); - int renderDynamicLines = js.IndexOf("const dynamicLines = safeArray(frame.dynamicPolylines);", - renderWorldStart, StringComparison.Ordinal); - int renderStaticSegments = js.IndexOf("safeArray(snapshot.directionSegments)", - renderDynamicLines, StringComparison.Ordinal); - Verification.True(renderWorldStart >= 0 && renderDynamicLines > renderWorldStart && - renderStaticSegments > renderDynamicLines, - "overview derives dynamic lines before rendering static direction segments"); - Verification.True(js.Contains("atob"), "compact occupancy bitset is decoded in browser"); - Verification.True(!html.Contains("http://") && !html.Contains("https://"), "page has no CDN URL"); - } - - private static void BuildsDeterministicSmokeSnapshots() - { - PlanningVisualizationStaticSnapshot staticSnapshot = SampleSnapshotFactory.CreateStaticSnapshot(); - PlanningVisualizationDynamicSnapshot dynamicSnapshot = SampleSnapshotFactory.CreateDynamicSnapshot(1L); - - Verification.True(staticSnapshot.StaticPolylines.Any(line => line.Kind == "coarse"), - "smoke snapshot exposes the coarse comparison path"); - Verification.True(staticSnapshot.StaticPolylines.Any(line => line.Kind == "local-g2"), - "smoke snapshot exposes the smoothed comparison path"); - Verification.True(staticSnapshot.StaticMarkers.Any(marker => marker.Kind == "smooth-start"), - "smoke snapshot exposes the smoothed-path start marker"); - Verification.Equal(1, dynamicSnapshot.DynamicPolylines.Count(line => line.Kind == "active-segment"), - "smoke snapshot has exactly one dynamic active segment"); - VisualizationMarker smoothStart = staticSnapshot.StaticMarkers.Single(marker => marker.Kind == "smooth-start"); - VisualizationMarker planStart = dynamicSnapshot.DynamicMarkers.Single(marker => marker.Kind == "plan-start"); - VisualizationPolyline current = dynamicSnapshot.DynamicPolylines.Single(line => line.Kind == "current"); - Verification.NearlyEqual(smoothStart.Position.X, planStart.Position.X, - "smoke snapshot overlaps smoothed-path and plan-start x coordinates"); - Verification.NearlyEqual(smoothStart.Position.Y, planStart.Position.Y, - "smoke snapshot overlaps smoothed-path and plan-start y coordinates"); - Verification.NearlyEqual(current.Points[0].X, planStart.Position.X, - "smoke snapshot aligns plan-start x with the current EM trajectory start"); - Verification.NearlyEqual(current.Points[0].Y, planStart.Position.Y, - "smoke snapshot aligns plan-start y with the current EM trajectory start"); - Verification.Equal(2, staticSnapshot.DirectionSegments.Count, "smoke snapshot has two direction segments"); - Verification.True(staticSnapshot.StaticMarkers.Any(marker => marker.Kind == "gear-switch"), "smoke snapshot has a gear marker"); - Verification.True(staticSnapshot.ConfigurationGroups.Count > 0, "smoke snapshot has effective configuration"); - foreach (string chartId in new[] { "ls", "st", "curvature-s", "curvature-t", "velocity-t", "acceleration-t", "jerk-t", "yaw-rate-t" }) - { - Verification.True(dynamicSnapshot.Charts.Any(chart => chart.Id == chartId), "smoke snapshot includes chart " + chartId); - } - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs deleted file mode 100644 index dd486cc..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs +++ /dev/null @@ -1,468 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { test } from "node:test"; -import { JSDOM } from "jsdom"; - -const webDir = new URL("../../TrajectoryPlanningVisualization/Web/", import.meta.url); -const readWebFile = (name) => readFileSync(new URL(name, webDir), "utf8"); - -function makeStaticSnapshot({ empty = false } = {}) { - return { - sessionNameChinese: "合成轨迹规划观察", - worldBounds: { xMin: 0, xMax: 12, yMin: 0, yMax: 8 }, - occupancyGrid: null, - staticPolylines: empty - ? [] - : [ - { - id: "global", - legendChinese: "全局参考路径", - kind: "global", - lineStyle: 0, - points: [ - { x: 0, y: 1 }, - { x: 3, y: 1 }, - { x: 6, y: 2 }, - { x: 8, y: 4 }, - { x: 11, y: 6 } - ] - }, - { - id: "local-g2", - legendChinese: "完整 Local G2 路径", - kind: "local-g2", - lineStyle: 0, - points: [ - { x: 0.2, y: 1 }, - { x: 3.2, y: 1.1 }, - { x: 6.1, y: 2.1 }, - { x: 8.1, y: 4.1 }, - { x: 10.9, y: 5.9 } - ] - } - ], - staticMarkers: empty - ? [] - : [ - { id: "gear-1", kind: "gear-switch", labelChinese: "换向 1", position: { x: 6, y: 2 } }, - { id: "gear-1-end", kind: "gear-switch-end", labelChinese: "换向点 1 / s_end", position: { x: 6, y: 2 } }, - { id: "final-goal", kind: "final-goal", labelChinese: "终点 / s_end", position: { x: 11, y: 6 } } - ], - directionSegments: empty - ? [] - : [ - { - segmentIndex: 0, - direction: "forward", - startsAtGearSwitch: false, - endsAtGearSwitch: true, - points: [ - { x: 0, y: 1 }, - { x: 3, y: 1 }, - { x: 6, y: 2 } - ] - }, - { - segmentIndex: 1, - direction: "reverse", - startsAtGearSwitch: true, - endsAtGearSwitch: false, - points: [ - { x: 6, y: 2 }, - { x: 8, y: 4 }, - { x: 11, y: 6 } - ] - } - ], - configurationGroups: [] - }; -} - -function chart(id, title, xAxis, yAxis, points, annotations = []) { - return { - id, - chineseTitle: title, - xAxisLabel: xAxis, - yAxisLabel: yAxis, - noteChinese: "", - series: [ - { - id: id + "-current", - legend: "当前轨迹", - lineStyle: 0, - points: points.map(([x, y]) => ({ x, y })) - } - ], - annotations - }; -} - -function makeDynamicSnapshot({ empty = false } = {}) { - if (empty) { - return { - sequence: 1, - observedAtUtc: "2026-08-06T00:00:00Z", - sessionStateChinese: "运行中", - activeSegmentIndex: 0, - activeDirection: "forward", - vehiclePose: null, - dynamicPolylines: [], - dynamicMarkers: [], - charts: [], - statusValues: [], - cycleSummary: { - cycleVersion: 1, - occurredAtUtc: "2026-08-06T00:00:00Z", - status: "成功", - published: true, - planningElapsedMilliseconds: 4.2, - segmentIndex: 0, - direction: "forward", - longitudinalMode: "full", - terminalType: "goal", - terminalVelocity: 0, - terminalAcceleration: 0, - failureReason: "" - } - }; - } - - return { - sequence: 1, - observedAtUtc: "2026-08-06T00:00:00Z", - sessionStateChinese: "合成数据运行中", - activeSegmentIndex: 0, - activeDirection: "forward", - vehiclePose: { x: 2, y: 1, headingRadians: 0 }, - dynamicPolylines: [ - { - id: "previous", - legendChinese: "上一轮轨迹", - kind: "previous", - lineStyle: 1, - points: [ - { x: 1, y: 1 }, - { x: 2.5, y: 1 }, - { x: 4, y: 1.3 } - ] - }, - { - id: "current", - legendChinese: "当前轨迹", - kind: "current", - lineStyle: 0, - points: [ - { x: 2, y: 1 }, - { x: 3.8, y: 1.2 }, - { x: 5.8, y: 1.9 } - ] - } - ], - dynamicMarkers: [ - { id: "vehicle", kind: "vehicle", labelChinese: "车辆", position: { x: 2, y: 1 } }, - { id: "plan-start", kind: "plan-start", labelChinese: "计划起点", position: { x: 2, y: 1 } }, - { id: "gear-switch-end", kind: "gear-switch-end", labelChinese: "换向点 1 / s_end", position: { x: 6, y: 2 } } - ], - charts: [ - chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", [[0, 0.05], [2, 0.08], [4, 0.02]], [ - { id: "ls-s-end", kind: "s-end", labelChinese: "s_end", x: 4 } - ]), - chart("st", "时空轨迹", "t (s)", "PathS (m)", [[0, 0], [1, 1.2], [2, 3]], [ - { id: "st-s-end", kind: "s-end", labelChinese: "s_end", x: 2 } - ]) - ], - statusValues: [], - cycleSummary: { - cycleVersion: 1, - occurredAtUtc: "2026-08-06T00:00:00Z", - status: "成功", - published: true, - planningElapsedMilliseconds: 4.2, - segmentIndex: 0, - direction: "forward", - longitudinalMode: "full", - terminalType: "goal", - terminalVelocity: 0, - terminalAcceleration: 0, - failureReason: "" - } - }; -} - -function fakeContext() { - return { - setTransform() {}, - clearRect() {}, - fillRect() {} - }; -} - -function loadApp({ staticSnapshot = makeStaticSnapshot(), dynamicSnapshot = makeDynamicSnapshot() } = {}) { - const dom = new JSDOM(readWebFile("index.html"), { - runScripts: "outside-only", - pretendToBeVisual: true, - url: "http://127.0.0.1/observation?token=test" - }); - const { window } = dom; - window.devicePixelRatio = 1; - window.__TRAJECTORY_VISUALIZATION_TEST__ = true; - window.__fetchCalled = false; - window.__eventSourceCreated = false; - window.fetch = async () => { - window.__fetchCalled = true; - throw new Error("test mode must not fetch"); - }; - window.EventSource = class { - constructor() { - window.__eventSourceCreated = true; - } - - close() {} - }; - const style = window.document.createElement("style"); - style.textContent = readWebFile("app.css"); - window.document.head.append(style); - window.HTMLCanvasElement.prototype.getContext = () => fakeContext(); - window.eval(readWebFile("app.js")); - - if (window.__trajectoryVisualizationTestHooks) { - const hooks = window.__trajectoryVisualizationTestHooks; - hooks.setBootstrap(staticSnapshot); - hooks.receiveFrame({ snapshot: dynamicSnapshot, history: [] }); - } - - return dom; -} - -function deepFreeze(value) { - if (value && typeof value === "object" && !Object.isFrozen(value)) { - Object.freeze(value); - Object.values(value).forEach(deepFreeze); - } - return value; -} - -function dispatchPointer(target, type, clientX, clientY, options = {}) { - const window = target.ownerDocument.defaultView; - const EventCtor = window.PointerEvent || window.MouseEvent; - target.dispatchEvent(new EventCtor(type, { - bubbles: true, - cancelable: true, - pointerId: 1, - button: 0, - clientX, - clientY, - ...options - })); -} - -function dispatchWheel(target, clientX, clientY, deltaY) { - const window = target.ownerDocument.defaultView; - const EventCtor = window.WheelEvent || window.MouseEvent; - target.dispatchEvent(new EventCtor("wheel", { - bubbles: true, - cancelable: true, - clientX, - clientY, - deltaY - })); -} - -function chartSnapshot(host) { - return { - ticks: [...host.querySelectorAll(".axis-tick-x")].map(tick => tick.textContent).join("|"), - path: host.querySelector("path.chart-data")?.getAttribute("d") || "" - }; -} - -test("tab switching hides inactive sections despite CSS grid rules", () => { - const dom = loadApp(); - const { document, getComputedStyle } = dom.window; - const overview = document.querySelector("#overview"); - const lsSt = document.querySelector("#ls-st"); - const kinematics = document.querySelector("#kinematics"); - - assert.equal(overview.hidden, false); - assert.notEqual(getComputedStyle(overview).display, "none"); - - document.querySelector('button[data-tab="ls-st"]').click(); - - assert.equal(overview.hidden, true); - assert.equal(getComputedStyle(overview).display, "none"); - assert.equal(lsSt.hidden, false); - assert.notEqual(getComputedStyle(lsSt).display, "none"); - assert.equal(kinematics.hidden, true); - assert.equal(getComputedStyle(kinematics).display, "none"); -}); - -test("ST chart renders y ticks, units, and s_end annotation", () => { - const dom = loadApp(); - const { document } = dom.window; - document.querySelector('button[data-tab="ls-st"]').click(); - - const yTicks = document.querySelectorAll("#st .axis-tick-y"); - const xTicks = document.querySelectorAll("#st .axis-tick-x"); - const yLabel = document.querySelector("#st .axis-label-y"); - const xLabel = document.querySelector("#st .axis-label-x"); - - assert.ok(yTicks.length >= 4, "ST renders y tick text"); - assert.ok(xTicks.length >= 4, "ST renders x tick text"); - assert.ok([...yTicks].every(tick => tick.textContent.length > 0), "y ticks have readable values"); - assert.equal(yLabel.textContent, "PathS (m)"); - assert.equal(xLabel.textContent, "t (s)"); - assert.ok(document.querySelector("#st .chart-annotation-label")?.textContent.includes("s_end")); -}); - -test("ST PathS polyline rises and semantic world layers are ordered", () => { - const dom = loadApp(); - const { document } = dom.window; - document.querySelector('button[data-tab="ls-st"]').click(); - const stPath = document.querySelector("#st path.chart-data"); - assert.ok(stPath, "ST chart has a polyline"); - const segments = stPath.getAttribute("d").split(/(?=[ML])/).filter(Boolean); - const yValues = segments.map(segment => Number(segment.slice(1).trim().split(/[\s,]+/)[1])); - assert.ok(yValues.length >= 3, "ST polyline has at least three rendered points"); - assert.ok(yValues[1] < yValues[0] && yValues[2] < yValues[1], - "ST PathS increases upward in rendered pixel coordinates"); - - document.querySelector('button[data-tab="overview"]').click(); - const overlay = document.querySelector("#world-overlay"); - const paths = [...overlay.querySelectorAll("path")]; - const layerClasses = ["world-coarse", "world-local-g2", "world-segment-active", "world-previous", "world-current"]; - const indices = layerClasses.map(className => paths.findIndex(path => path.classList.contains(className))); - layerClasses.forEach((className, index) => assert.ok(indices[index] !== -1, className + " exists")); - for (let index = 1; index < indices.length; index += 1) { - assert.ok(indices[index - 1] < indices[index], "semantic world layer order is preserved"); - } -}); - -test("vehicle, plan start, gear switch, and final goal use semantic markers", () => { - const dom = loadApp(); - const { document } = dom.window; - const overlay = document.querySelector("#world-overlay"); - - for (const className of ["marker-vehicle", "marker-plan-start", "marker-gear-switch", "marker-final-goal"]) { - assert.ok(overlay.querySelector("." + className), className + " is rendered"); - } - assert.equal(overlay.querySelectorAll(".marker-vehicle circle").length, 0, - "vehicle marker is not a fixed pixel circle"); -}); - -test("empty overview names the missing Local G2 source", () => { - const dom = loadApp({ - staticSnapshot: makeStaticSnapshot({ empty: true }), - dynamicSnapshot: makeDynamicSnapshot({ empty: true }) - }); - const emptyState = dom.window.document.querySelector("#world-overlay .world-empty-state"); - - assert.ok(emptyState, "empty overview renders a visible state"); - assert.match(emptyState.textContent, /未收到.*Local G2 路径/); -}); - -test("test mode exposes frozen hooks without fetch or SSE", () => { - const dom = loadApp(); - const { window } = dom; - - assert.equal(window.__fetchCalled, false); - assert.equal(window.__eventSourceCreated, false); - assert.ok(window.__trajectoryVisualizationTestHooks, "test hooks are exposed"); - assert.equal(Object.isFrozen(window.__trajectoryVisualizationTestHooks), true); - for (const name of ["setBootstrap", "receiveFrame", "renderActiveTab", "installTabs"]) { - assert.equal(typeof window.__trajectoryVisualizationTestHooks[name], "function", name + " hook exists"); - } - assert.equal("state" in window.__trajectoryVisualizationTestHooks, false); -}); - -test("wheel and box zoom change ST ticks and path without mutating frozen snapshot", () => { - const dynamicSnapshot = deepFreeze(makeDynamicSnapshot()); - const serializedBefore = JSON.stringify(dynamicSnapshot); - const dom = loadApp({ dynamicSnapshot }); - const { document } = dom.window; - document.querySelector('button[data-tab="ls-st"]').click(); - const st = document.querySelector("#st"); - const svg = st.querySelector("svg"); - const before = chartSnapshot(st); - - dispatchPointer(svg, "pointerdown", 400, 100); - dispatchPointer(svg, "pointermove", 360, 80); - dispatchPointer(svg, "pointerup", 360, 80); - - const afterBox = chartSnapshot(st); - assert.notEqual(afterBox.ticks, before.ticks); - assert.notEqual(afterBox.path, before.path); - - dispatchWheel(st.querySelector("svg"), 300, 100, -120); - - const afterWheel = chartSnapshot(st); - assert.notEqual(afterWheel.ticks, afterBox.ticks); - assert.notEqual(afterWheel.path, afterBox.path); - assert.equal(JSON.stringify(dynamicSnapshot), serializedBefore); -}); - -test("box zoom ignores drags shorter than 4 CSS pixels", () => { - const dom = loadApp(); - const { document } = dom.window; - document.querySelector('button[data-tab="ls-st"]').click(); - const st = document.querySelector("#st"); - const svg = st.querySelector("svg"); - const before = chartSnapshot(st); - - dispatchPointer(svg, "pointerdown", 200, 100); - dispatchPointer(svg, "pointermove", 202, 102); - dispatchPointer(svg, "pointerup", 202, 102); - - assert.deepEqual(chartSnapshot(st), before); -}); - -test("reset restores the original chart data domain", () => { - const dom = loadApp(); - const { document } = dom.window; - document.querySelector('button[data-tab="ls-st"]').click(); - const st = document.querySelector("#st"); - const before = chartSnapshot(st); - - dispatchWheel(st.querySelector("svg"), 300, 100, -120); - - const zoomed = chartSnapshot(st); - assert.notEqual(zoomed.ticks, before.ticks); - - st.querySelector('button[data-action="reset"]').click(); - assert.deepEqual(chartSnapshot(st), before); -}); - -test("fullscreen requests only the selected chart host", () => { - const dom = loadApp(); - const { document, Element } = dom.window; - const requested = []; - Element.prototype.requestFullscreen = function requestFullscreen() { - requested.push(this.id); - }; - document.querySelector('button[data-tab="ls-st"]').click(); - const st = document.querySelector("#st"); - const ls = document.querySelector("#ls"); - - st.querySelector('button[data-action="fullscreen"]').click(); - - assert.deepEqual(requested, ["st"]); - assert.equal(st.classList.contains("is-fullscreen"), true); - assert.equal(ls.classList.contains("is-fullscreen"), false); -}); - -test("new SSE frame keeps an active viewport and leaves its snapshot unchanged", () => { - const first = deepFreeze(makeDynamicSnapshot()); - const dom = loadApp({ dynamicSnapshot: first }); - const { document } = dom.window; - document.querySelector('button[data-tab="ls-st"]').click(); - const st = document.querySelector("#st"); - dispatchWheel(st.querySelector("svg"), 300, 100, -120); - const zoomed = chartSnapshot(st); - - const second = JSON.parse(JSON.stringify(first)); - second.sequence = 2; - deepFreeze(second); - const serializedSecond = JSON.stringify(second); - dom.window.__trajectoryVisualizationTestHooks.receiveFrame({ snapshot: second, history: [] }); - - assert.deepEqual(chartSnapshot(st), zoomed); - assert.equal(JSON.stringify(second), serializedSecond); -}); diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json deleted file mode 100644 index 852499e..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json +++ /dev/null @@ -1,558 +0,0 @@ -{ - "name": "trajectory-planning-visualization-web-dom-tests", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "trajectory-planning-visualization-web-dom-tests", - "devDependencies": { - "jsdom": "29.1.1" - }, - "engines": { - "node": ">=24" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.10", - "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", - "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tldts": { - "version": "7.4.10", - "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.10.tgz", - "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.10" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.10", - "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.10.tgz", - "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json deleted file mode 100644 index 3f21a39..0000000 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "trajectory-planning-visualization-web-dom-tests", - "private": true, - "type": "module", - "scripts": { - "test": "node --test dashboard.dom.test.mjs" - }, - "devDependencies": { - "jsdom": "29.1.1" - }, - "engines": { - "node": ">=24" - } -} diff --git a/ClumsyPilot/tests/generate_path_smoothing_fixtures.ps1 b/ClumsyPilot/tests/generate_path_smoothing_fixtures.ps1 deleted file mode 100644 index 9f8223a..0000000 --- a/ClumsyPilot/tests/generate_path_smoothing_fixtures.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -param( - [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), - [string]$OutputPath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), - [switch]$Overwrite -) - -$ErrorActionPreference = 'Stop' -$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -$type = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.SmoothingFixtureGenerator', $true) -$method = $type.GetMethod('Generate', [Type[]]@([string], [bool])) -if ($null -eq $method) { throw 'SmoothingFixtureGenerator.Generate(string, bool) is missing.' } -$method.Invoke($null, @([IO.Path]::GetFullPath($OutputPath), [bool]$Overwrite)) -Write-Output "Path smoothing fixtures generated: $OutputPath" diff --git a/ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 b/ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 deleted file mode 100644 index e1e4465..0000000 --- a/ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -param( - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), - [string]$EvidencePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json'), - [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-single-turn') -) - -$ErrorActionPreference = 'Stop' - -$clumsyPilotRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path -$allowedRoot = [IO.Path]::GetFullPath((Join-Path $clumsyPilotRoot 'obj\path_smoothing_reports')) -$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) -$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar -if ($resolvedOutputDirectory -ne $allowedRoot -and -not $resolvedOutputDirectory.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { - throw "Report output must stay below $allowedRoot" -} - -$projectPath = Join-Path $clumsyPilotRoot 'ClumsyPilot.csproj' -$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' -$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path -$resolvedEvidencePath = (Resolve-Path -LiteralPath $EvidencePath).Path - -& dotnet build $projectPath --no-restore -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -& dotnet run --project $hostProject --no-restore -- --export-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath $resolvedOutputDirectory -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -Write-Output "Local G2 diagnostic visualization written below $resolvedOutputDirectory" diff --git a/ClumsyPilot/tests/run_path_smoothing_comparison.ps1 b/ClumsyPilot/tests/run_path_smoothing_comparison.ps1 deleted file mode 100644 index 2308ccd..0000000 --- a/ClumsyPilot/tests/run_path_smoothing_comparison.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -param( - [switch]$FixtureOnly, - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), - [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports') -) - -$ErrorActionPreference = 'Stop' - -$clumsyPilotRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path -$allowedRoot = [IO.Path]::GetFullPath((Join-Path $clumsyPilotRoot 'obj\path_smoothing_reports')) -$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) -$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar -if ($resolvedOutputDirectory -ne $allowedRoot -and -not $resolvedOutputDirectory.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { - throw "Report output must stay below $allowedRoot" -} - -$projectPath = Join-Path $clumsyPilotRoot 'ClumsyPilot.csproj' -$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' -$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path - -& dotnet build $projectPath --no-restore -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -& dotnet run --project $hostProject --no-restore -- --export-fixtures $resolvedFixturePath $resolvedOutputDirectory -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -if (-not $FixtureOnly) { - & dotnet run --project $hostProject --no-restore -- --export-end-to-end $resolvedOutputDirectory - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -} - -Write-Output "Path smoothing reports written below $resolvedOutputDirectory" diff --git a/ClumsyPilot/tests/verify_coarse_path_collision.ps1 b/ClumsyPilot/tests/verify_coarse_path_collision.ps1 deleted file mode 100644 index b7cd515..0000000 --- a/ClumsyPilot/tests/verify_coarse_path_collision.ps1 +++ /dev/null @@ -1,130 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function New-TestMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) - $rectangleType = $assembly.GetType($mapping + 'AxisAlignedRectangleObstacle', $true) - $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) - $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) - $obstacle = [Activator]::CreateInstance($rectangleType, @([single]1500, [single]1550, [single]1200, [single]1800)) - $obstacles = [Array]::CreateInstance($obstacleType, 1) - $obstacles.SetValue($obstacle, 0) - $source = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) - $sources = [Array]::CreateInstance($sourceType, 1) - $sources.SetValue($source, 0) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.ObstacleSources = $sources - $request.AllowExplicitEmptyMap = $false - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Test map must be created.' - Assert-True $result.Map.PlanningReady 'Test map must be ready.' - return $result.Map -} - -function New-DiagonalCellMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) - $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) - $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) - $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) - $obstacle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0)) - $obstacles = [Array]::CreateInstance($obstacleType, 1) - $obstacles.SetValue($obstacle, 0) - $source = [Activator]::CreateInstance($manualType, @('diagonal-cell', [long]1, $true, $obstacles)) - $sources = [Array]::CreateInstance($sourceType, 1) - $sources.SetValue($source, 0) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.ObstacleSources = $sources - $request.AllowExplicitEmptyMap = $false - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Diagonal cell map must be created.' - Assert-True $result.Map.PlanningReady 'Diagonal cell map must be ready.' - Assert-True $result.Map.IsOccupied(30, 30) 'Diagonal cell obstacle must occupy its 50mm cell.' - return $result.Map -} - -$map = New-TestMap -$vehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters -$vehicle.LengthMeters = 0.20 -$vehicle.WidthMeters = 0.20 -$vehicle.SafetyMarginMeters = 0.0 -$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker - -$diagonalCellMap = New-DiagonalCellMap -$diagonalCellVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters -$diagonalCellVehicle.LengthMeters = 0.10 -$diagonalCellVehicle.WidthMeters = 0.10 -$diagonalCellVehicle.SafetyMarginMeters = 0.0 -$diagonalCellPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.299, 1.299, 0.0) -$diagonalCellClearance = 0.0 -$diagonalCellSafe = $checker.IsPoseCollisionFree($diagonalCellPose, $diagonalCellMap, $diagonalCellVehicle, 0.20, [ref]$diagonalCellClearance) -Assert-False $diagonalCellSafe 'Expanded diagonal footprint must not bypass an occupied 50mm cell.' - -$edgePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.35, 1.50, 0.0) -$edgeClearance = 0.0 -$edgeSafe = $checker.IsPoseCollisionFree($edgePose, $map, $vehicle, 0.0, [ref]$edgeClearance) -Assert-False $edgeSafe 'Touching an occupied cell must be a collision.' - -$farPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.50, 0.50, 0.0) -$farClearance = 0.0 -$farSafe = $checker.IsPoseCollisionFree($farPose, $map, $vehicle, 0.0, [ref]$farClearance) -Assert-True $farSafe 'Strict distance field clearance must allow the distant pose.' -Assert-True ($farClearance -gt 0.0) 'Distant pose must report positive body clearance.' - -$from = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.20, 1.50, 0.0) -$to = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.80, 1.50, 0.0) -$sweepClearance = 0.0 -$sweepSafe = $checker.IsSweptMotionCollisionFree($from, $to, $map, $vehicle, 0.50, [ref]$sweepClearance) -Assert-False $sweepSafe 'A swept vehicle must not pass through an occupied cell.' - -$gridCenterPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.475, 1.475, 0.0) -$gridCenterClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($gridCenterPose, $map, $vehicle, 0.0, [ref]$gridCenterClearance) 'A pose at an occupied grid center must collide.' - -$subGridPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.481, 1.463, 0.37) -$subGridClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($subGridPose, $map, $vehicle, 0.0, [ref]$subGridClearance) 'An arbitrary sub-grid heading must collide with the thin obstacle.' - -$diagonalPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.32, 1.50, ([Math]::PI / 4.0)) -$diagonalClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($diagonalPose, $map, $vehicle, 0.0, [ref]$diagonalClearance) 'A 45 degree footprint overlap must collide.' - -$outsidePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.05, 0.05, 0.0) -$outsideClearance = 0.0 -Assert-False $checker.IsPoseCollisionFree($outsidePose, $map, $vehicle, 0.0, [ref]$outsideClearance) 'Any footprint corner outside the map must collide.' - -$curveVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters -$curveVehicle.MaximumCurvaturePerMeter = 0.80 -$curveVehicle.MinimumTurningRadiusMeters = 2.0 -$maximumCurvature = 0.0 -$hasMaximumCurvature = [MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.VehicleKinematics]::TryGetMaximumCurvaturePerMeter($curveVehicle, [ref]$maximumCurvature) -Assert-True $hasMaximumCurvature 'Vehicle curvature constraints must be accepted.' -Assert-Near 0.50 $maximumCurvature 'Both curvature constraints must use the conservative minimum.' - -Write-Output 'Coarse path collision checks passed.' diff --git a/ClumsyPilot/tests/verify_coarse_path_integration.ps1 b/ClumsyPilot/tests/verify_coarse_path_integration.ps1 deleted file mode 100644 index 6051426..0000000 --- a/ClumsyPilot/tests/verify_coarse_path_integration.ps1 +++ /dev/null @@ -1,460 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' - -$plannerRoot = Join-Path $PSScriptRoot '..\ParkrobTrajplanner' -$coarsePathReadme = Join-Path $plannerRoot 'CoarsePath\README.md' -$legacyRootReadme = Join-Path $plannerRoot 'README.md' -if (-not (Test-Path -LiteralPath $coarsePathReadme -PathType Leaf)) { - throw 'CoarsePath module README must be located inside the CoarsePath directory.' -} -if (Test-Path -LiteralPath $legacyRootReadme -PathType Leaf) { - throw 'The coarse-path-only README must not remain at the ParkrobTrajplanner root.' -} -$coarsePathReadmeContent = Get-Content -LiteralPath $coarsePathReadme -Raw -if (-not $coarsePathReadmeContent.Contains('## 总预算与取消')) { - throw 'CoarsePath README must document total timeout and cancellation semantics.' -} -if (-not $coarsePathReadmeContent.Contains('PlanningMapBuildStatus')) { - throw 'CoarsePath README must document map-stage termination status handling.' -} - -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods()) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { - $matches = $false - break - } - } - if ($matches) { return $candidate } - } - return $null -} - -function New-EmptyPlanningMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Empty integration map must be created.' - Assert-True $result.Map.PlanningReady 'Explicit empty integration map must be ready.' - return $result.Map -} - -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$plannerType = $assembly.GetType($coarsePath + 'HybridAStarPlanner', $true) -$planner = [Activator]::CreateInstance($plannerType) -$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) -$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) -$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) -$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) -$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) -$plan = Find-Method $plannerType 'Plan' @($requestType, [Threading.CancellationToken]) -Assert-True ($plan -ne $null) 'HybridAStarPlanner must expose Plan(request, cancellationToken).' - -$defaultConfiguration = [Activator]::CreateInstance($configurationType) -Assert-Near 0.15 $defaultConfiguration.GoalPositionToleranceMeters 'Default goal-position tolerance must retain the documented 0.15 m terminal acceptance radius.' - -$request = [Activator]::CreateInstance($requestType) -$request.Map = New-EmptyPlanningMap -$request.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) -$request.Goal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) -$request.Vehicle = [Activator]::CreateInstance($vehicleType) -$request.Vehicle.LengthMeters = 0.20 -$request.Vehicle.WidthMeters = 0.20 -$request.Vehicle.SafetyMarginMeters = 0.0 -$request.Vehicle.MaximumCurvaturePerMeter = 1.0 -$request.Configuration = [Activator]::CreateInstance($configurationType) -$request.Configuration.MaximumExpandedNodes = 10000 -$request.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) -$request.Configuration.GoalPositionToleranceMeters = 0.001 -$request.Configuration.GoalHeadingToleranceRadians = 0.001 -$request.Configuration.AllowReverse = $false -$request.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') -$request.StartVehicleCurvature = [double]0.20 - -$result = $plan.Invoke($planner, @($request, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $result.Status.ToString() 'Planner must publish a validated direct path on an empty map.' -Assert-True ($result.Path.Count -ge 2) 'Validated path must retain its start and goal points.' -Assert-Near 0.0 $result.Path[0].ArcLength 'The first path point must have zero arc length.' -Assert-Near 0.20 $result.Path[0].VehicleCurvature ` - 'The assembled start point must retain the requested physical vehicle curvature.' -Assert-Equal 'Start' $result.Path[0].Source.ToString() 'The first path point must retain the start source.' -Assert-Equal 'GoalTruncation' $result.Path[-1].Source.ToString() 'The terminal truncated point must retain its source.' -Assert-True ($result.Diagnostics.PeakOpenListCount -ge 1) 'Planner diagnostics must retain the actual Open List peak count.' -Assert-True ($result.Diagnostics.StaleOpenListEntryCount -ge 0) 'Planner diagnostics must retain a non-negative stale Open List count.' -Assert-True ($result.Diagnostics.GetType().GetProperty('PathSearchElapsed') -ne $null) 'Planner diagnostics must expose path-search elapsed time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) 'Successful planning must retain non-negative path-search time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) 'Path-search time must not exceed total elapsed time.' - -$nodeLimitedRequest = [Activator]::CreateInstance($requestType) -$nodeLimitedRequest.Map = $request.Map -$nodeLimitedRequest.Start = $request.Start -$nodeLimitedRequest.Goal = $request.Goal -$nodeLimitedRequest.Vehicle = $request.Vehicle -$nodeLimitedRequest.Configuration = [Activator]::CreateInstance($configurationType) -$nodeLimitedRequest.Configuration.MaximumExpandedNodes = 0 -$nodeLimitedRequest.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2) -$nodeLimitedRequest.Configuration.GoalPositionToleranceMeters = 0.001 -$nodeLimitedRequest.Configuration.GoalHeadingToleranceRadians = 0.001 -$nodeLimitedRequest.Configuration.AllowReverse = $false -$nodeLimitedRequest.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') -$nodeLimitedResult = $plan.Invoke($planner, @($nodeLimitedRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchNodeLimitExceeded' $nodeLimitedResult.Status.ToString() 'A zero node limit must fail after planner preflight.' -Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) 'Search-stage node-limit failure must retain path-search time.' -Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -le $nodeLimitedResult.Diagnostics.Elapsed) 'Failed path-search time must not exceed total elapsed time.' -Assert-True (-not [string]::IsNullOrWhiteSpace($nodeLimitedResult.Diagnostics.TerminationReason)) 'Planner diagnostics must retain a node-limit reason.' - -for ($index = 1; $index -lt $result.Path.Count; $index++) { - $previous = $result.Path[$index - 1] - $current = $result.Path[$index] - Assert-True ($current.ArcLength -ge $previous.ArcLength) 'Path arc length must be non-decreasing.' - Assert-True ([Math]::Abs($current.UnwrappedHeading - $previous.UnwrappedHeading) -le ([Math]::PI + 0.000001)) 'Unwrapped heading must remain continuous between adjacent points.' - $samePose = ([Math]::Abs($current.X - $previous.X) -le 0.000001) -and - ([Math]::Abs($current.Y - $previous.Y) -le 0.000001) -and - ([Math]::Abs($current.Heading - $previous.Heading) -le 0.000001) -and - ([Math]::Abs($current.ArcLength - $previous.ArcLength) -le 0.000001) - if ($samePose) { - Assert-True (($current.Direction -ne $previous.Direction) -and $current.IsGearSwitchPoint) 'Adjacent duplicate points are permitted only as a marked gear-switch pair.' - } -} - -Assert-True ($result.Segments.Count -ge 1) 'Validated path must include inclusive direction segments.' -Assert-Equal 0 $result.Segments[0].StartIndex 'The first segment must include the first path point.' -Assert-Equal ($result.Path.Count - 1) $result.Segments[-1].EndIndex 'The final segment must include the final path point.' -for ($index = 0; $index -lt $result.Segments.Count; $index++) { - $segment = $result.Segments[$index] - Assert-Equal $index $segment.SegmentIndex 'Segment indexes must be contiguous.' - Assert-True ($segment.StartIndex -le $segment.EndIndex) 'Each segment must use inclusive ordered indexes.' - if ($index -gt 0) { - $previousSegment = $result.Segments[$index - 1] - Assert-Equal ($previousSegment.EndIndex + 1) $segment.StartIndex 'Segments must cover each path point exactly once.' - } -} - -Write-Output 'Coarse path integration checks passed.' - -# Facade checks intentionally use only ASCII text so the script can also run in legacy PowerShell hosts. -$facade = $coarsePath + 'Facade.' -$serviceType = $assembly.GetType($facade + 'CoarsePathPlanningService', $true) -$jobType = $assembly.GetType($facade + 'CoarsePathPlanningJob', $true) -$jobResultType = $assembly.GetType($facade + 'CoarsePathPlanningJobResult', $true) -$debugOptionsType = $assembly.GetType($facade + 'PlanningDebugOptions', $true) -$debugSinkType = $assembly.GetType($facade + 'IPlanningDebugSink', $true) -$mapRequestType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true) -$boundsType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true) -$servicePlan = Find-Method $serviceType 'Plan' @($jobType, [Threading.CancellationToken]) -Assert-True ($servicePlan -ne $null) 'CoarsePathPlanningService must expose Plan(job, cancellationToken).' -Assert-True ($jobResultType.GetProperty('MapResult') -ne $null) 'Facade result must retain the map result.' -Assert-True ($jobResultType.GetProperty('PlanningResult') -ne $null) 'Facade result must retain the planning result.' -Assert-True ($jobResultType.GetProperty('DebugDiagnostics') -ne $null) 'Facade result must retain debug diagnostics.' - -function New-FacadeJob { - $job = [Activator]::CreateInstance($jobType) - $mapRequest = [Activator]::CreateInstance($mapRequestType) - $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $mapRequest.ResolutionMm = [single]50 - $mapRequest.AllowExplicitEmptyMap = $true - $job.MapRequest = $mapRequest - $job.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) - $job.Goal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) - $job.Vehicle = [Activator]::CreateInstance($vehicleType) - $job.Vehicle.LengthMeters = 0.20 - $job.Vehicle.WidthMeters = 0.20 - $job.Vehicle.SafetyMarginMeters = 0.0 - $job.Vehicle.MaximumCurvaturePerMeter = 1.0 - $job.Configuration = [Activator]::CreateInstance($configurationType) - $job.Configuration.MaximumExpandedNodes = 10000 - $job.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) - $job.Configuration.GoalPositionToleranceMeters = 0.001 - $job.Configuration.GoalHeadingToleranceRadians = 0.001 - $job.Configuration.AllowReverse = $false - $job.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') - return $job -} - -$service = [Activator]::CreateInstance($serviceType) -$firstFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), [Threading.CancellationToken]::None)) -Assert-True $firstFacadeResult.MapResult.Succeeded 'Facade must build the requested map first.' -Assert-Equal 'Success' $firstFacadeResult.PlanningResult.Status.ToString() 'Facade must pass a successful map to the planner.' -Assert-Equal 0 $firstFacadeResult.DebugDiagnostics.Count 'Default debug sink must not add diagnostics.' - -$secondFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), [Threading.CancellationToken]::None)) -Assert-True $secondFacadeResult.MapResult.Succeeded 'Repeated facade request must retain a map result.' -Assert-Equal 'Input' $secondFacadeResult.MapResult.CacheHit.ToString() 'One facade service must retain its map factory cache.' -Assert-Equal $firstFacadeResult.MapResult.Map.InputFingerprint $secondFacadeResult.MapResult.Map.InputFingerprint 'Map fingerprint must remain stable for equal input.' -Assert-Equal $firstFacadeResult.PlanningResult.Status $secondFacadeResult.PlanningResult.Status 'Map cache reuse must not change planning status.' -Assert-Equal $firstFacadeResult.PlanningResult.Path.Count $secondFacadeResult.PlanningResult.Path.Count 'Map cache reuse must not change path point count.' - -$invalidJob = [Activator]::CreateInstance($jobType) -$invalidJob.MapRequest = [Activator]::CreateInstance($mapRequestType) -$mapFailureResult = $servicePlan.Invoke($service, @($invalidJob, [Threading.CancellationToken]::None)) -Assert-False $mapFailureResult.MapResult.Succeeded 'Invalid map input must be returned as a map failure.' -Assert-Equal 'InvalidMap' $mapFailureResult.PlanningResult.Status.ToString() 'Map failure must return an empty non-search planning result.' -Assert-Equal 0 $mapFailureResult.PlanningResult.Diagnostics.ExpandedNodeCount 'Map failure must not start the search.' -Assert-Equal 0 $mapFailureResult.PlanningResult.Path.Count 'Map failure must not publish a path.' -Assert-Equal ([TimeSpan]::Zero) $mapFailureResult.PlanningResult.Diagnostics.PathSearchElapsed 'Map failure must report zero path-search time.' - -$cancelledFacadeSource = New-Object Threading.CancellationTokenSource -$cancelledFacadeSource.Cancel() -$cancelledFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), $cancelledFacadeSource.Token)) -Assert-Equal 'Cancelled' $cancelledFacadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.' -Assert-Equal 'Cancelled' $cancelledFacadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.' -Assert-Equal 0 $cancelledFacadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.' -Assert-Equal 0 $cancelledFacadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.' - -$timedOutFacadeJob = New-FacadeJob -$timedOutFacadeJob.Configuration.SearchTimeout = [TimeSpan]::Zero -$timedOutFacadeResult = $servicePlan.Invoke($service, @($timedOutFacadeJob, [Threading.CancellationToken]::None)) -Assert-Equal 'TimedOut' $timedOutFacadeResult.MapResult.Status.ToString() 'Facade total timeout must stop before publishing a map.' -Assert-Equal 'SearchTimeout' $timedOutFacadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage timeout to planning timeout.' -Assert-Equal 0 $timedOutFacadeResult.PlanningResult.Path.Count 'Timed out facade planning must publish no path.' - -$defaultDebugOptions = [Activator]::CreateInstance($debugOptionsType) -Assert-False $defaultDebugOptions.Enabled 'Debug output must be opt-in.' -Assert-True ($defaultDebugOptions.Sink -ne $null) 'Debug options must default to a no-op sink.' - -$debugSinkSource = @' -using System; -using MultiWheelC.TrajectoryPlanning.Mapping; -using MultiWheelC.TrajectoryPlanning.CoarsePath; -using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; - -public sealed class FacadeRecordingDebugSink : IPlanningDebugSink -{ - public int PublishCount { get; private set; } - public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) - { - PublishCount++; - } -} - -public sealed class FacadeThrowingDebugSink : IPlanningDebugSink -{ - public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) - { - throw new InvalidOperationException("debug sink failure"); - } -} -'@ -$runtimeDirectory = [Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() -$debugSinkReferences = @( - (Join-Path $runtimeDirectory 'mscorlib.dll'), - (Join-Path $runtimeDirectory 'System.dll'), - (Join-Path $runtimeDirectory 'System.Core.dll'), - $AssemblyPath, - 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\Facades\netstandard.dll' -) -Add-Type -TypeDefinition $debugSinkSource -ReferencedAssemblies $debugSinkReferences -$recordingSinkType = 'FacadeRecordingDebugSink' -as [type] -$throwingSinkType = 'FacadeThrowingDebugSink' -as [type] - -$disabledDebugOptions = [Activator]::CreateInstance($debugOptionsType) -$recordingSink = [Activator]::CreateInstance($recordingSinkType) -$disabledDebugOptions.Sink = $recordingSink -$disabledJob = New-FacadeJob -$disabledJob.DebugOptions = $disabledDebugOptions -$disabledResult = $servicePlan.Invoke($service, @($disabledJob, [Threading.CancellationToken]::None)) -Assert-Equal 0 $recordingSink.PublishCount 'Disabled debug output must not publish to the sink.' -Assert-Equal $firstFacadeResult.PlanningResult.Status $disabledResult.PlanningResult.Status 'Disabled debug output must not change planning status.' - -$throwingDebugOptions = [Activator]::CreateInstance($debugOptionsType) -$throwingDebugOptions.Enabled = $true -$throwingDebugOptions.Sink = [Activator]::CreateInstance($throwingSinkType) -$throwingJob = New-FacadeJob -$throwingJob.DebugOptions = $throwingDebugOptions -$throwingResult = $servicePlan.Invoke($service, @($throwingJob, [Threading.CancellationToken]::None)) -Assert-Equal $firstFacadeResult.MapResult.Map.InputFingerprint $throwingResult.MapResult.Map.InputFingerprint 'Debug sink errors must not change the map fingerprint.' -Assert-Equal $firstFacadeResult.PlanningResult.Status $throwingResult.PlanningResult.Status 'Debug sink errors must not change planning status.' -Assert-Equal $firstFacadeResult.PlanningResult.Path.Count $throwingResult.PlanningResult.Path.Count 'Debug sink errors must not change the path.' -Assert-Equal 1 $throwingResult.DebugDiagnostics.Count 'Debug sink errors must be retained as diagnostics.' - -Write-Output 'Coarse path facade checks passed.' - -# P1 UI scenario factory contract checks. These run against the same built assembly -# and intentionally fail until the factory is introduced. -$testNamespace = $coarsePath + 'Test.' -$scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false) -$scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false) -Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.' -Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.' - -$factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType) -$factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @( - $scenarioEnumType, [double], [double], [double]) -$factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @( - [double], [double], [double], [double], [double], [double]) -Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).' -Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).' -Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.' - -$scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath') -foreach ($scenarioName in $scenarioNames) { - $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) - $jobA = $factoryCreate.Invoke($null, @($scenario)) - $jobB = $factoryCreate.Invoke($null, @($scenario)) - Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job." - Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call." -} - -foreach ($scenarioName in $scenarioNames) { - $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) - $liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0)) - Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X." - Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y." - Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading." -} - -$rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour') -$liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0)) -Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.' -Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.' -Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.' -Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.' -Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.' -Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.' -Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.' -Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.' -$rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld() -$rectangleObstacle = $rectangleProjection.Obstacles[0] -Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.' -Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.' - -$multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg') -$baselineMulti = $factoryCreate.Invoke($null, @($multiScenario)) -$liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0)) -Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.' -Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.' -Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.' -$baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles -$liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles -Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.' -Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.' - -$noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath') -$liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0)) -Assert-Near 9.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.' -Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.' -Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.' - -$invalidLivePoseRejected = $false -try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) } -catch [ArgumentOutOfRangeException] { - $invalidLivePoseRejected = $true -} -catch [Reflection.TargetInvocationException] { - $invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] -} -Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.' - -$manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0)) -Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.' -Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.' -Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.' -Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.' -Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.' -Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.' - -$slowFeasibleJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 0.0, 1500.0, 2500.0, 90.0)) -$slowFeasibleJob.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(30) -$slowFeasibleJob.Configuration.MaximumExpandedNodes = 1000000 -$slowFeasibleService = [Activator]::CreateInstance($serviceType) -$slowFeasibleResult = $servicePlan.Invoke($slowFeasibleService, @($slowFeasibleJob, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $slowFeasibleResult.PlanningResult.Status.ToString() 'The previously five-second-limited feasible pose must succeed with a caller-selected longer budget.' -Assert-True ($slowFeasibleResult.PlanningResult.Diagnostics.PathSearchElapsed -le $slowFeasibleResult.PlanningResult.Diagnostics.Elapsed) 'Slow feasible path-search time must remain within total elapsed time.' - -$manualObstacleKindType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacleKind', $false) -$manualObstacleType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacle', $false) -Assert-True ($manualObstacleKindType -ne $null) 'Manual obstacle kind enum must exist.' -Assert-True ($manualObstacleType -ne $null) 'Manual obstacle value type must exist.' - -$manualCircle = Find-Method $manualObstacleType 'Circle' @([double], [double], [double]) -$manualRectangle = Find-Method $manualObstacleType 'AxisAlignedRectangle' @([double], [double], [double], [double]) -$manualObstacleFactory = $scenarioFactoryType.GetMethods() | Where-Object { - $_.Name -eq 'CreateManualObstacleDemo' -and $_.GetParameters().Length -eq 8 -} | Select-Object -First 1 -Assert-True ($manualCircle -ne $null) 'Manual obstacle type must create circles from center and radius.' -Assert-True ($manualRectangle -ne $null) 'Manual obstacle type must create rectangles from center and X/Y dimensions.' -Assert-True ($manualObstacleFactory -ne $null) 'Scenario factory must expose CreateManualObstacleDemo with six poses, obstacles and version.' - -$manualObstacles = [Array]::CreateInstance($manualObstacleType, 2) -$manualObstacles.SetValue($manualCircle.Invoke($null, @([double]6500, [double]2000, [double]200)), 0) -$manualObstacles.SetValue($manualRectangle.Invoke($null, @([double]-2000, [double]500, [double]600, [double]400)), 1) -$manualObstacleJob = $manualObstacleFactory.Invoke($null, @( - 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $manualObstacles, [long]77)) -Assert-False $manualObstacleJob.MapRequest.AllowExplicitEmptyMap 'Manual obstacles must disable the explicit-empty-map mode.' -Assert-Equal 1 $manualObstacleJob.MapRequest.ObstacleSources.Count 'Manual obstacles must create one unified source.' -Assert-Equal 'manual-user-input' $manualObstacleJob.MapRequest.ObstacleSources[0].SourceId 'Manual source ID must be stable.' -Assert-Equal 77 $manualObstacleJob.MapRequest.ObstacleSources[0].SourceVersion 'Manual source version must be preserved.' -Assert-True ($manualObstacleJob.MapRequest.Bounds.XMin -le -4300.0) 'Manual map must include the rectangle outline and padding.' -Assert-True ($manualObstacleJob.MapRequest.Bounds.XMax -ge 8700.0) 'Manual map must include the circle outline and padding.' - -$emptyManualObstacles = [Array]::CreateInstance($manualObstacleType, 0) -$emptyManualJob = $manualObstacleFactory.Invoke($null, @( - 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $emptyManualObstacles, [long]0)) -Assert-True $emptyManualJob.MapRequest.AllowExplicitEmptyMap 'Zero manual obstacles must retain explicit empty-map mode.' -Assert-Equal 0 $emptyManualJob.MapRequest.ObstacleSources.Count 'Zero manual obstacles must not create a fake source.' - -$invalidGeometryRejected = $false -try { - $null = $manualCircle.Invoke($null, @([double]1000, [double]1000, [double]0)) -} -catch [ArgumentOutOfRangeException] { $invalidGeometryRejected = $true } -catch [Reflection.TargetInvocationException] { - $invalidGeometryRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] -} -Assert-True $invalidGeometryRejected 'Invalid manual geometry must report argument range.' - -$scenarioService = [Activator]::CreateInstance($serviceType) -foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { - $job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName))) - $result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None)) - Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed." -} - -$reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch'))) -$reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None)) -Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.' - -$noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath'))) -$noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None)) -Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.' -Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.' -Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('Open List')) 'No-path diagnostics must retain the exact search exhaustion reason.' -Assert-True (-not [string]::IsNullOrWhiteSpace($noPathResult.PlanningResult.Diagnostics.TerminationReason)) 'No-path diagnostics must retain a reason.' - -$cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) -$cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) -$cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None)) -$cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None)) -Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.' -Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.' - -Write-Output 'Coarse path P1 scenario checks passed.' diff --git a/ClumsyPilot/tests/verify_coarse_path_search.ps1 b/ClumsyPilot/tests/verify_coarse_path_search.ps1 deleted file mode 100644 index 3566c91..0000000 --- a/ClumsyPilot/tests/verify_coarse_path_search.ps1 +++ /dev/null @@ -1,411 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Null($Actual, [string]$Message) { - if ($null -ne $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch { - return - } - - throw $Message -} - -function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods()) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { - $matches = $false - break - } - } - if ($matches) { return $candidate } - } - return $null -} - -function Find-NonPublicStaticMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Static,NonPublic')) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } - } - if ($matches) { return $candidate } - } - return $null -} - -function New-TestMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.AllowExplicitEmptyMap = $true - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Test map must be created.' - Assert-True $result.Map.PlanningReady 'Test map must be ready.' - return $result.Map -} - -function New-CornerBlockedMap { - $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) - $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) - $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) - $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) - $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) - $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) - $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]150, [single]0, [single]150)) - $obstacles = [Array]::CreateInstance($obstacleType, 2) - $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]75, [single]25, [single]0)), 0) - $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]25, [single]75, [single]0)), 1) - $sources = [Array]::CreateInstance($sourceType, 1) - $sources.SetValue([Activator]::CreateInstance($manualType, @('corner-blocks', [long]1, $true, $obstacles)), 0) - $request = [Activator]::CreateInstance($requestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - $request.ObstacleSources = $sources - $request.AllowExplicitEmptyMap = $false - $result = [Activator]::CreateInstance($factoryType).Create($request) - Assert-True $result.Succeeded 'Corner-blocked map must be created.' - Assert-True $result.Map.PlanningReady 'Corner-blocked map must be ready.' - return $result.Map -} - -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$search = $coarsePath + 'Search.' -$primitiveType = $assembly.GetType($search + 'MotionPrimitive', $true) -$generatorType = $assembly.GetType($search + 'MotionPrimitiveGenerator', $true) -$goalCheckerType = $assembly.GetType($search + 'GoalToleranceChecker', $true) - -Assert-True ($primitiveType.GetProperty('ActualLengthMeters') -ne $null) 'MotionPrimitive must expose actual length.' -Assert-True ($primitiveType.GetProperty('Points') -ne $null) 'MotionPrimitive must expose integration points.' -Assert-True ($primitiveType.GetProperty('IsGoalTruncation') -ne $null) 'MotionPrimitive must expose goal truncation state.' - -$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) -$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) -$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) -$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) -$directionType = $assembly.GetType($coarsePath + 'TravelDirection', $true) -$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) -$generate = Find-Method $generatorType 'Generate' @($poseType, [double], $directionType, $requestType) -Assert-True ($generate -ne $null) 'MotionPrimitiveGenerator must expose Generate(start, curvature, direction, request).' -$isSatisfied = Find-Method $goalCheckerType 'IsSatisfied' @($poseType, $poseType, $configurationType, $directionType, $goalDirectionType) -Assert-True ($isSatisfied -ne $null) 'GoalToleranceChecker must expose the planned goal check.' - -$map = New-TestMap -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = 0.20 -$vehicle.WidthMeters = 0.20 -$vehicle.SafetyMarginMeters = 0.0 -$vehicle.MaximumCurvaturePerMeter = 1.0 -$config = [Activator]::CreateInstance($configurationType) -$config.PrimitiveLengthMeters = 0.50 -$config.IntegrationStepMeters = 0.05 -$config.MaximumCollisionCheckStepMeters = 0.025 -$config.GoalPositionToleranceMeters = 0.001 -$config.GoalHeadingToleranceRadians = 0.001 -$config.CurvatureLevelCount = 5 -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anyDirection = [Enum]::Parse($goalDirectionType, 'Any') -$forwardOnly = [Enum]::Parse($goalDirectionType, 'Forward') -$start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) -$generator = [Activator]::CreateInstance($generatorType) - -$request = [Activator]::CreateInstance($requestType) -$request.Map = $map -$request.Vehicle = $vehicle -$request.Configuration = $config -$request.Goal = [Activator]::CreateInstance($poseType, @(3.0, 1.0, 0.0)) -$request.GoalDirection = $anyDirection -$straight = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) -Assert-True ($straight -ne $null) 'Straight primitive must be generated.' -Assert-Near 0.50 $straight.ActualLengthMeters 'Straight primitive must use the configured cap.' -Assert-Near 1.50 $straight.End.X 'Straight primitive must use analytic integration.' -Assert-Near 1.00 $straight.End.Y 'Straight primitive must not drift laterally.' -Assert-Near 0.00 $straight.End.Heading 'Straight primitive must keep heading.' -Assert-True ($straight.Points.Count -ge 20) 'Point spacing must honor the collision check step.' -$previous = $start -foreach ($point in $straight.Points) { - $distance = [Math]::Sqrt(($point.X - $previous.X) * ($point.X - $previous.X) + ($point.Y - $previous.Y) * ($point.Y - $previous.Y)) - Assert-True ($distance -le 0.025001) 'Point spacing must not exceed the map-safe collision step.' - $previous = $point -} - -$curve = $generate.Invoke($generator, @($start, 1.0, $forward, $request)) -Assert-True ($curve -ne $null) 'Arc primitive must be generated.' -Assert-Near ([Math]::Sin(0.50) + 1.0) $curve.End.X 'Arc X must use analytic integration.' -Assert-Near (1.0 - [Math]::Cos(0.50) + 1.0) $curve.End.Y 'Arc Y must use analytic integration.' -Assert-Near 0.50 $curve.End.Heading 'Arc heading must use signed distance.' - -$reversePrimitive = $generate.Invoke($generator, @($start, 0.0, $reverse, $request)) -Assert-True ($reversePrimitive -ne $null) 'Reverse primitive must be generated.' -Assert-Near 0.50 $reversePrimitive.End.X 'Reverse straight primitive must move behind the vehicle.' - -$request.Goal = [Activator]::CreateInstance($poseType, @(1.30, 1.0, 0.0)) -$truncated = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) -Assert-True ($truncated -ne $null) 'Goal-truncated primitive must be generated.' -Assert-Near 0.30 $truncated.ActualLengthMeters 'A 0.30m goal must truncate at its first internal point.' -Assert-True $truncated.IsGoalTruncation 'Truncated primitive must record its goal source.' -Assert-Near 1.30 $truncated.End.X 'Truncated primitive must end at the goal point.' - -$request.Goal = $start -$zeroLength = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) -Assert-True ($zeroLength -ne $null) 'Start-at-goal must produce a zero-length candidate.' -Assert-Near 0.0 $zeroLength.ActualLengthMeters 'Start-at-goal candidate must have zero length.' -Assert-True $zeroLength.IsGoalTruncation 'Start-at-goal candidate must have goal truncation source.' - -$wrapPose = [Activator]::CreateInstance($poseType, @(1.0, 1.0, (-[Math]::PI + 0.0005))) -$wrapGoal = [Activator]::CreateInstance($poseType, @(1.0, 1.0, ([Math]::PI - 0.0005))) -$config.GoalHeadingToleranceRadians = 0.002 -Assert-True $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $forward, $forwardOnly)) 'Goal heading tolerance must wrap across pi.' -Assert-False $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $reverse, $forwardOnly)) 'Goal direction constraint must reject reverse entry.' - -$levelsMethod = Find-Method $generatorType 'GetCurvatureLevels' @($vehicleType, $configurationType) -Assert-True ($levelsMethod -ne $null) 'MotionPrimitiveGenerator must expose curvature levels.' -$levels = $levelsMethod.Invoke($generator, @($vehicle, $config)) -Assert-True ($levels.Count -eq 5) 'Five configured curvature levels must be generated.' -Assert-Near -1.0 $levels[0] 'First curvature level must be negative maximum.' -Assert-Near 0.0 $levels[2] 'Middle curvature level must be straight.' -Assert-Near 1.0 $levels[4] 'Last curvature level must be positive maximum.' -$adjacent = Find-Method $generatorType 'AreCurvatureLevelsAdjacent' @([int], [int]) -Assert-True ($adjacent -ne $null) 'MotionPrimitiveGenerator must expose curvature adjacency.' -Assert-True $adjacent.Invoke($null, @(2, 3)) 'Neighboring curvature levels must be allowed.' -Assert-False $adjacent.Invoke($null, @(2, 4)) 'Non-neighboring curvature levels must be rejected.' - -$heapGenericType = $assembly.GetType($search + 'BinaryMinHeap`1', $true) -$heapType = $heapGenericType.MakeGenericType([string]) -$heap = [Activator]::CreateInstance($heapType) -$push = Find-Method $heapType 'Push' @([string], [double], [double], [double]) -$pop = Find-Method $heapType 'Pop' @() -Assert-True ($push -ne $null) 'BinaryMinHeap must expose Push(item, f, h, g).' -Assert-True ($pop -ne $null) 'BinaryMinHeap must expose Pop().' -$push.Invoke($heap, @('node-a', 8.0, 2.0, 1.0)) -$push.Invoke($heap, @('node-b', 8.0, 1.0, 1.0)) -$push.Invoke($heap, @('node-c', 8.0, 1.0, 3.0)) -$push.Invoke($heap, @('node-d', 7.0, 9.0, 0.0)) -$push.Invoke($heap, @('node-e', 8.0, 1.0, 3.0)) -Assert-Equal 'node-d' $pop.Invoke($heap, @()) 'Lower F must win.' -Assert-Equal 'node-c' $pop.Invoke($heap, @()) 'Equal F and H must prefer larger G.' -Assert-Equal 'node-e' $pop.Invoke($heap, @()) 'Equal F, H and G must preserve insertion order.' -Assert-Equal 'node-b' $pop.Invoke($heap, @()) 'Equal F must prefer lower H.' -Assert-Equal 'node-a' $pop.Invoke($heap, @()) 'Remaining item must be returned last.' -$firstDeterministicHeap = [Activator]::CreateInstance($heapType) -$secondDeterministicHeap = [Activator]::CreateInstance($heapType) -foreach ($deterministicHeap in @($firstDeterministicHeap, $secondDeterministicHeap)) { - $push.Invoke($deterministicHeap, @('repeat-a', 5.0, 2.0, 1.0)) - $push.Invoke($deterministicHeap, @('repeat-b', 5.0, 1.0, 1.0)) - $push.Invoke($deterministicHeap, @('repeat-c', 5.0, 1.0, 3.0)) -} -$firstDeterministicOrder = @($pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @())) -join ',' -$secondDeterministicOrder = @($pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @())) -join ',' -Assert-Equal $firstDeterministicOrder $secondDeterministicOrder 'Equivalent heap inputs must have deterministic pop order.' - -$calculatorType = $assembly.GetType($search + 'SearchCostCalculator', $true) -$calculator = [Activator]::CreateInstance($calculatorType) -$calculate = Find-Method $calculatorType 'Calculate' @([double], $directionType, [bool], [double], [double], [int], [double], $configurationType) -Assert-True ($calculate -ne $null) 'SearchCostCalculator must expose the planned equivalent-meter calculation.' -$costConfig = [Activator]::CreateInstance($configurationType) -$cost = $calculate.Invoke($calculator, @(2.0, $reverse, $true, 0.5, 1.0, 2, 0.25, $costConfig)) -Assert-Near 4.55 $cost 'Reverse, gear, curvature and clearance costs must be accumulated.' -$negativeWeightConfig = [Activator]::CreateInstance($configurationType) -$negativeWeightConfig.CurvatureMagnitudeWeight = -0.01 -Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $negativeWeightConfig)) } 'Negative cost weights must be rejected.' -$nonFiniteWeightConfig = [Activator]::CreateInstance($configurationType) -$nonFiniteWeightConfig.ClearanceCostWeight = [double]::NaN -Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $nonFiniteWeightConfig)) } 'Non-finite cost weights must be rejected.' - -$dijkstraType = $assembly.GetType($search + 'GridDijkstraHeuristic', $true) -$dijkstraConstructor = $dijkstraType.GetConstructor(@($map.GetType(), [int], [int])) -Assert-True ($dijkstraConstructor -ne $null) 'GridDijkstraHeuristic must expose the planned grid constructor.' -$getCost = Find-Method $dijkstraType 'GetCost' @([int], [int]) -Assert-True ($getCost -ne $null) 'GridDijkstraHeuristic must expose GetCost(row, col).' -$openHeuristic = $dijkstraConstructor.Invoke(@($map, 2, 2)) -$expectedDiagonalCost = 2.0 * [Math]::Sqrt(2.0) * $map.ResolutionMeters -Assert-Near $expectedDiagonalCost $getCost.Invoke($openHeuristic, @(0, 0)) 'Open-grid diagonal movement must cost sqrt(2) per cell.' -$cornerBlockedMap = New-CornerBlockedMap -$blockedHeuristic = $dijkstraConstructor.Invoke(@($cornerBlockedMap, 1, 1)) -Assert-True ([double]::IsPositiveInfinity($getCost.Invoke($blockedHeuristic, @(0, 0)))) 'Diagonal corner cutting through two occupied orthogonal cells must be forbidden.' - -$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $true) -$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $true) -$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @([Threading.CancellationToken], [TimeSpan]), $null) -Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' -$tryCreateDijkstra = Find-NonPublicStaticMethod $dijkstraType 'TryCreate' @($map.GetType(), [int], [int], $operationBudgetType, - $dijkstraType.MakeByRefType(), $operationStopReasonType.MakeByRefType()) -Assert-True ($tryCreateDijkstra -ne $null) 'GridDijkstraHeuristic must expose an internal budget-aware TryCreate method.' -$expiredBudget = $budgetConstructor.Invoke(@([Threading.CancellationToken]::None, [TimeSpan]::Zero)) -$dijkstraArguments = [object[]]@($map, 2, 2, $expiredBudget, $null, $null) -$dijkstraCreated = $tryCreateDijkstra.Invoke($null, $dijkstraArguments) -Assert-False $dijkstraCreated 'An expired total budget must stop Dijkstra construction.' -Assert-Null $dijkstraArguments[4] 'Stopped Dijkstra construction must not publish a partial heuristic.' -Assert-Equal 'TimedOut' $dijkstraArguments[5].ToString() 'Expired Dijkstra construction must report timeout.' - -$largeBounds = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true), - @([single]0, [single]40000, [single]0, [single]20000)) -$largeMapRequest = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true)) -$largeMapRequest.Bounds = $largeBounds -$largeMapRequest.ResolutionMm = [single]20 -$largeMapRequest.AllowExplicitEmptyMap = $true -$largeMap = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory', $true)).Create($largeMapRequest) -Assert-True $largeMap.Succeeded 'Large empty map for Dijkstra cancellation must be created before cancellation starts.' -$midDijkstraCancellation = New-Object Threading.CancellationTokenSource -$midDijkstraBudget = $budgetConstructor.Invoke(@($midDijkstraCancellation.Token, [TimeSpan]::FromSeconds(2))) -$midDijkstraCancellation.CancelAfter(1) -$midDijkstraTimer = [Diagnostics.Stopwatch]::StartNew() -$midDijkstraArguments = [object[]]@($largeMap.Map, 500, 1000, $midDijkstraBudget, $null, $null) -$midDijkstraCreated = $tryCreateDijkstra.Invoke($null, $midDijkstraArguments) -$midDijkstraTimer.Stop() -Assert-False $midDijkstraCreated 'Cancellation during Dijkstra initialization must stop construction.' -Assert-Null $midDijkstraArguments[4] 'Mid-Dijkstra cancellation must not publish a partial heuristic.' -Assert-Equal 'Cancelled' $midDijkstraArguments[5].ToString() 'Mid-Dijkstra cancellation must retain cancellation status.' -Assert-True ($midDijkstraTimer.Elapsed -lt [TimeSpan]::FromSeconds(2)) 'Dijkstra cancellation must return within the configured response bound.' - -Write-Output 'Coarse path search primitive checks passed.' - -$nodeType = $assembly.GetType($search + 'HybridAStarNode', $true) -$nodeKeyType = $assembly.GetType($search + 'HybridAStarNodeKey', $true) -$searchType = $assembly.GetType($search + 'HybridAStarSearch', $true) -$searchResultType = $assembly.GetType($search + 'HybridAStarSearchResult', $true) -$searchMethod = Find-Method $searchType 'Search' @($requestType, [Threading.CancellationToken]) -Assert-True ($searchMethod -ne $null) 'HybridAStarSearch must expose Search(request, cancellationToken).' -Assert-True ($searchResultType.GetProperty('Status') -ne $null) 'Search result must expose planning status.' -Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) 'Search result must expose its original termination reason.' -Assert-True ($searchResultType.GetProperty('SuccessNodeIndex') -ne $null) 'Search result must expose the successful node index.' -Assert-True ($searchResultType.GetProperty('ReopenedNodeCount') -ne $null) 'Search result must expose reopened node count.' -Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entry count.' -Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak count.' -Assert-True ($nodeType.GetProperty('ParentNodeIndex') -ne $null) 'Search node must expose its parent node index.' -Assert-True ($nodeKeyType.GetProperty('HeadingIndex') -ne $null) 'Search node key must contain a heading index.' -Assert-True ($nodeKeyType.GetProperty('Direction') -ne $null) 'Search node key must contain a travel direction.' -Assert-True ($nodeKeyType.GetProperty('CurvatureLevelIndex') -ne $null) 'Search node key must contain a curvature level index.' - -$goalCandidateProperty = $nodeType.GetProperty('IsGoalCandidate') -Assert-True ($goalCandidateProperty -ne $null) 'Search node must mark a goal-truncated candidate explicitly.' -$nodeConstructor = $nodeType.GetConstructor(@([int], $nodeKeyType, $poseType, [int], $primitiveType, [double], [double], [double], [bool])) -Assert-True ($nodeConstructor -ne $null) 'Search node must accept the explicit goal-candidate marker.' -$admissionMethod = $searchType.GetMethod('ShouldEnqueueSuccessor', [Reflection.BindingFlags]'NonPublic,Static') -Assert-True ($admissionMethod -ne $null) 'Search must expose its private successor admission policy for reflection regression coverage.' - -$candidateStart = [Activator]::CreateInstance($poseType, @(1.000, 1.000, 0.0)) -$candidateGoal = [Activator]::CreateInstance($poseType, @(1.024, 1.000, 0.0)) -$candidatePoints = [Array]::CreateInstance($poseType, 1) -$candidatePoints.SetValue($candidateGoal, 0) -$candidatePrimitive = [Activator]::CreateInstance($primitiveType, @($candidateStart, $forward, 0.0, 0.024, $candidatePoints, [double[]]@(1.0), $true)) -$sharedKey = [Activator]::CreateInstance($nodeKeyType, @(20, 20, 0, $forward, 2)) -$normalNode = $nodeConstructor.Invoke(@(10, $sharedKey, $candidateStart, 0, $null, 0.0, 0.250, 0.0, $false)) -$goalCandidateNode = $nodeConstructor.Invoke(@(11, $sharedKey, $candidateGoal, 10, $candidatePrimitive, 0.0, 0.300, 0.0, $true)) -$dominanceConfiguration = [Activator]::CreateInstance($configurationType) -$dominanceConfiguration.GoalPositionToleranceMeters = 0.001 -$dominanceConfiguration.GoalHeadingToleranceRadians = 0.001 -Assert-True $candidatePrimitive.IsGoalTruncation 'Regression setup must use a goal-truncated motion primitive.' -Assert-True $normalNode.Key.Equals($goalCandidateNode.Key) 'Regression setup must share one discrete state key.' -Assert-False $isSatisfied.Invoke($null, @($normalNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Lower-G normal node must remain outside the continuous goal tolerance.' -Assert-True $isSatisfied.Invoke($null, @($goalCandidateNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Goal candidate must satisfy the continuous goal tolerance.' -Assert-False $admissionMethod.Invoke($null, @($normalNode, 0.250)) 'Equal-cost normal state must be dominated by the best same-key label.' -Assert-True $admissionMethod.Invoke($null, @($goalCandidateNode, 0.250)) 'Goal candidate must enter Open List even when a lower-G normal state has the same key.' - -function New-SearchRequest([object]$Goal, [object]$GoalDirection, [bool]$AllowReverse) { - $searchRequest = [Activator]::CreateInstance($requestType) - $searchRequest.Map = $map - $searchRequest.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) - $searchRequest.Goal = $Goal - $searchRequest.Vehicle = $vehicle - $searchConfiguration = [Activator]::CreateInstance($configurationType) - $searchConfiguration.PrimitiveLengthMeters = 0.50 - $searchConfiguration.IntegrationStepMeters = 0.05 - $searchConfiguration.MaximumCollisionCheckStepMeters = 0.025 - $searchConfiguration.GoalPositionToleranceMeters = 0.001 - $searchConfiguration.GoalHeadingToleranceRadians = 0.001 - $searchConfiguration.CurvatureLevelCount = 5 - $searchConfiguration.MaximumExpandedNodes = 10000 - $searchConfiguration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) - $searchConfiguration.AllowReverse = $AllowReverse - $searchRequest.Configuration = $searchConfiguration - $searchRequest.GoalDirection = $GoalDirection - return $searchRequest -} - -$searcher = [Activator]::CreateInstance($searchType) -$searchGoal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) -$searchRequest = New-SearchRequest $searchGoal $forwardOnly $false -$searchResult = $searchMethod.Invoke($searcher, @($searchRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $searchResult.Status.ToString() 'Empty map forward search must succeed.' -Assert-True ($null -ne $searchResult.SuccessNodeIndex) 'Success must be reported only with a popped goal candidate node.' -Assert-True ($searchResult.ExpandedNodeCount -gt 0) 'A successful search must expand the selected goal candidate.' - -$atGoal = New-SearchRequest $searchRequest.Start $forwardOnly $false -$atGoalResult = $searchMethod.Invoke($searcher, @($atGoal, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $atGoalResult.Status.ToString() 'A zero-length goal candidate must enter and leave the open list successfully.' -Assert-True ($null -ne $atGoalResult.SuccessNodeIndex) 'A zero-length goal candidate must have a node index.' - -$cancelledRequest = New-SearchRequest $searchGoal $forwardOnly $false -$cancellationSource = New-Object Threading.CancellationTokenSource -$cancellationSource.Cancel() -$cancelledResult = $searchMethod.Invoke($searcher, @($cancelledRequest, $cancellationSource.Token)) -Assert-Equal 'Cancelled' $cancelledResult.Status.ToString() 'Cancellation must be observed before node expansion.' -Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) 'Cancelled search must retain a non-empty reason.' - -$limitedRequest = New-SearchRequest $searchGoal $forwardOnly $false -$limitedRequest.Configuration.MaximumExpandedNodes = 0 -$limitedResult = $searchMethod.Invoke($searcher, @($limitedRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchNodeLimitExceeded' $limitedResult.Status.ToString() 'Node limit must be checked before expansion.' -Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) 'Node-limited search must retain a non-empty reason.' - -$timedOutRequest = New-SearchRequest $searchGoal $forwardOnly $false -$timedOutRequest.Configuration.SearchTimeout = [TimeSpan]::Zero -$timedOutResult = $searchMethod.Invoke($searcher, @($timedOutRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchTimeout' $timedOutResult.Status.ToString() 'Timeout must be checked before expansion.' -Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) 'Timed-out search must retain a non-empty reason.' -Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) 'Cancelled and node-limited searches must retain different reasons.' -Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) 'Node-limited and timed-out searches must retain different reasons.' - -$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') | - Where-Object { - $_.Name -eq 'Search' -and - $_.GetParameters().Length -eq 2 -and - $_.GetParameters()[1].ParameterType -eq $operationBudgetType - } | - Select-Object -First 1 -Assert-True ($internalSearchMethod -ne $null) 'Search must retain its internal shared-budget overload.' -$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null)) -Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() 'A missing internal budget must be mapped to InternalError.' -Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) 'Internal search errors must retain the exception type.' - -Write-Output 'Coarse path Hybrid A star search checks passed.' diff --git a/ClumsyPilot/tests/verify_coarse_path_ui.ps1 b/ClumsyPilot/tests/verify_coarse_path_ui.ps1 deleted file mode 100644 index 58b2d00..0000000 --- a/ClumsyPilot/tests/verify_coarse_path_ui.ps1 +++ /dev/null @@ -1,122 +0,0 @@ -$ErrorActionPreference = 'Stop' - -function Assert-True([bool]$condition, [string]$message) { - if (-not $condition) { throw $message } -} - -function Assert-Match([string]$content, [string]$pattern, [string]$message) { - Assert-True ($content -match $pattern) $message -} - -function Assert-NotMatch([string]$content, [string]$pattern, [string]$message) { - Assert-True ($content -notmatch $pattern) $message -} - -$sourcePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\Test\MovementTest.CoarsePathTest.cs' -Assert-True (Test-Path -LiteralPath $sourcePath) 'P1 coarse-path UI movement-test source must exist.' -$source = Get-Content -Raw -Encoding UTF8 $sourcePath - -Assert-Match $source 'class\s+CoarsePathPlanningTest\s*:\s*MovementTest' 'The UI entry must inherit MovementTest.' -Assert-Match $source 'namespace\s+MultiWheelC\s*;' 'MovementTest entries must use the project discovery namespace.' -Assert-Match $source 'CoarsePathPlanningService' 'The UI must keep one shared planning service for map-cache reuse.' -Assert-Match $source 'Task\.Run\s*\(' 'Planning must run in a background Task.' -Assert-Match $source 'CancellationTokenSource' 'The UI must retain a cancellation source for its active task.' -Assert-Match $source 'override\s+void\s+TestStop\s*\(' 'The UI must implement TestStop cancellation.' -Assert-Match $source 'CoarsePathScenarioFactory\.Create\s*\(' 'The UI must expose fixed scenario entries through the factory.' -Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.' -Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.' -Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.' -Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.' -Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.' -Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.' -Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.' -Assert-Match $source 'UI\.GetInput\s*\(' 'The manual demo must accept user input.' -Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' 'The manual UI must read a finite positive timeout.' -Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' 'The manual UI must apply the timeout to the current job.' -Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' 'The manual timeout must convert seconds to TimeSpan.' -Assert-Match $source 'timeoutSeconds\s*<=\s*0' 'The manual timeout must reject zero and negative values.' -Assert-Match $source 'getCartLocation\s*\(' 'The manual demo must read the AMR body-center pose.' -Assert-Match $source 'DrawLegend\s*\(' 'The painter output must include a visual legend.' -Assert-Match $source 'PlanningGridMap' 'The painter output must consume the planning grid result.' -Assert-Match $source '\.IsOccupied\s*\(' 'The painter output must draw occupied grid cells.' -Assert-Match $source 'ResolutionMm' 'The painter output must draw the grid at its true resolution.' -Assert-Match $source 'SnapshotId' 'The painter output must show the map snapshot identity.' -Assert-Match $source 'IsGearSwitchPoint' 'The painter output must highlight gear-switch points.' -Assert-Match $source 'GoalPositionToleranceMeters' 'The painter output must draw goal tolerance.' -Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) 'The status layer and Toast must both show path-search elapsed time.' -Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' 'The failure Toast must include the termination reason.' -Assert-Match $source 'ExpandedNodeCount' 'The status layer must show expanded-node statistics.' -Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' 'The status layer must show the effective turning radius.' -Assert-NotMatch $source 'PlanningMapFactory' 'Movement tests must not build maps directly; use CoarsePathPlanningService.' -Assert-NotMatch $source '\.Result\b' 'The UI must not block on Task.Result.' -Assert-NotMatch $source '\.Wait\s*\(' 'The UI must not block the movement-test thread.' - -$runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner') -Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.' -$runnerSource = $source.Substring($runnerStart) -Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.' -Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.' -Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.' -Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.' -Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.' -Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.' -$inputFailureStart = $runnerSource.IndexOf('internal static void ShowInputFailure') -$inputFailureEnd = $runnerSource.IndexOf('private static void Finish', $inputFailureStart) -Assert-True ($inputFailureStart -ge 0 -and $inputFailureEnd -gt $inputFailureStart) 'Input failure renderer must be isolated before completion handling.' -$inputFailureSource = $runnerSource.Substring($inputFailureStart, $inputFailureEnd - $inputFailureStart) -Assert-Match $inputFailureSource 'Painter\.DrawText\s*\(' 'Invalid AMR input must be shown on the Painter status layer.' - -$readmePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\README.md' -Assert-True (Test-Path -LiteralPath $readmePath) 'CoarsePath README must exist beside its module.' -$readme = Get-Content -Raw -Encoding UTF8 $readmePath -foreach ($requiredText in @( - 'CoarsePathPlanningV1', - 'CoarsePathPlanningTest', - 'CreateManualGoalDemo', - 'getCartLocation', - 'mm', - 'deg', - 'rad', - 'CancellationTokenSource', - 'TestStop', - 'NoFeasiblePath', - 'IsGearSwitchPoint')) { - Assert-True ($readme.Contains($requiredText)) "P1 README must document $requiredText." -} - -$readmeStructure = @( - 'File Structure', - 'Planning Data Flow', - 'Build Status and Stop', - 'Coordinates and Units', - 'Minimal Call Example', - 'Cache and SourceVersion', - 'Detailed Usage Guide', - 'P1 Manual Tests and Visualization', - 'Common Errors', - 'First-Version Limits', - 'CoarsePathPlanningService.Plan(job, cancellationToken)', - 'CoarsePathPlanningJob', - 'PlanningGridMap', - 'SourceVersion', - 'CoarsePathPlanningV1', - 'CancellationTokenSource', - 'NoFeasiblePath', - 'IsGearSwitchPoint', - 'CreateManualObstacleDemo', - 'ManualCoarsePathObstacle', - 'manual-user-input', - '0-20', - 'PathSearchElapsed', - '1.20 m', - 'Reeds-Shepp', - 'AxisAlignedRectangle', - 'Create(CoarsePathTestScenario scenario, double amrXMillimeters', - 'DetectionHeadingRadians', - '../Map/README.md' -) -foreach ($requiredText in $readmeStructure) { - Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText." -} - -Write-Output 'Coarse path P1 UI source checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 b/ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 deleted file mode 100644 index 350f5a4..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$root = Join-Path $PSScriptRoot '..' -$readmePath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\README.md' -$demoPath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\Test\PathSmoothingComparisonDemo.cs' -$runnerPath = Join-Path $PSScriptRoot 'run_path_smoothing_comparison.ps1' - -function Assert-True($actual, [string]$message) { - if (-not $actual) { throw $message } -} - -Assert-True (Test-Path -LiteralPath $readmePath) 'Path smoothing README is missing.' -Assert-True (Test-Path -LiteralPath $demoPath) 'Path smoothing comparison demo is missing.' -Assert-True (Test-Path -LiteralPath $runnerPath) 'Path smoothing comparison batch runner is missing.' - -$readme = Get-Content -Raw -Encoding UTF8 $readmePath -foreach ($section in @( - 'Module Overview', - 'File Structure', - 'Smoothing Data Flow', - 'Result Status and Publication Rules', - 'Coordinates and Units', - 'Minimal Call Example', - 'Detailed Usage Guide', - 'Fixture Reports and Visualization', - 'Common Errors', - 'First-Version Limits')) { - Assert-True $readme.Contains($section) "README must contain section: $section" -} -foreach ($requiredText in @( - 'Local G2', - 'MinimumClearanceReserveMeters', - 'PathSmoothingService', - 'Complete', - 'PartialImprovement', - 'NotNeeded', - 'Unchanged', - 'SimSun', - 'Times New Roman', - '01-coarse-path-overview', - '02-all-paths-comparison', - '03-local-g2-overview', - '04-curvature-comparison', - '05-local-g2-diagnostic-candidate')) { - Assert-True $readme.Contains($requiredText) "README must document: $requiredText" -} - -$demo = Get-Content -Raw -Encoding UTF8 $demoPath -foreach ($requiredText in @( - 'PathSmoothingComparisonDemo', - 'PathSmoothingService', - 'PathSmoothingComparisonService', - 'SmoothingReportExporter', - 'SmoothingScenarioFactory', - 'PathSmoothingStatus.Complete', - 'PathSmoothingStatus.PartialImprovement', - 'PathSmoothingStatus.NotNeeded', - 'PathSmoothingStatus.Unchanged')) { - Assert-True $demo.Contains($requiredText) "Demo must use: $requiredText" -} - -$runner = Get-Content -Raw -Encoding UTF8 $runnerPath -Assert-True ($runner -match '\[switch\]\$FixtureOnly') 'Batch runner must offer -FixtureOnly.' -Assert-True $runner.Contains('obj\path_smoothing_reports') 'Batch runner must write reports below ClumsyPilot/obj/path_smoothing_reports.' -Assert-True $runner.Contains('--export-fixtures') 'Batch runner must export all eight fast fixtures by default.' -Assert-True ($runner -notmatch 'ParkrobTrajplanner\\PathSmoothing\\.*\.(svg|png|csv)') 'Batch runner must never write report files below source directories.' - -Write-Output 'Path smoothing LocalG2 documentation checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 b/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 deleted file mode 100644 index 44bcd27..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 +++ /dev/null @@ -1,119 +0,0 @@ -param( - [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json')) - -$ErrorActionPreference = 'Stop' -$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} -function Assert-ThrowsMatching([scriptblock]$Action, [string]$ExpectedPattern, [string]$Message) { - try { - & $Action - } - catch { - $exception = $_.Exception - while ($null -ne $exception.InnerException) { $exception = $exception.InnerException } - if ($exception.Message -match $ExpectedPattern) { return } - throw "$Message ExpectedPattern=$ExpectedPattern Actual=$($exception.Message)" - } - throw "$Message Expected an exception." -} -function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.' -$comparison = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison.' -$loaderType = Get-RequiredType ($root + 'SmoothingScenarioFixtureLoader') -$factoryType = Get-RequiredType ($root + 'SmoothingScenarioFactory') -$fixtureType = Get-RequiredType ($root + 'SmoothingScenarioFixture') -$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') -$preprocessorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing.PathSmoothingPreprocessor' - -$loadMethod = $loaderType.GetMethod('LoadAndVerify', [Type[]]@([string])) -Assert-True ($null -ne $loadMethod) 'Fixture loader must expose LoadAndVerify(string).' -$fixtureRequestsMethod = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) -Assert-True ($null -ne $fixtureRequestsMethod) 'Scenario factory must expose CreateFixtureRequests(string).' -$generateMethod = $assembly.GetType($root + 'SmoothingFixtureGenerator', $true).GetMethod('Generate', [Type[]]@([string], [bool])) -Assert-True ($null -ne $generateMethod) 'Fixture generator must expose Generate(string, bool).' -$tryPrepareMethod = $preprocessorType.GetMethod('TryPrepare') -Assert-True ($null -ne $tryPrepareMethod) 'Fixture paths must be checked through PathSmoothingPreprocessor.TryPrepare.' - -$fixtures = $loadMethod.Invoke($null, @((Resolve-Path $FixturePath).Path)) -$fixtureDocument = Get-Content -Raw -Encoding UTF8 $FixturePath | ConvertFrom-Json -$expected = @( - 'straight', 'single-turn', 's-bend', 'large-heading-change', - 'rectangle-detour', 'multi-obstacle-detour', - 'narrow-corridor', 'forward-reverse-switch') -Assert-Equal $expected.Count $fixtures.Count 'Fixture loader must return exactly eight fast fixtures.' - -$actualIds = @($fixtures | ForEach-Object { $_.Id }) -Assert-Equal ($expected -join ',') ($actualIds -join ',') 'Fixture IDs must be stable and in documented order.' -Assert-Equal $actualIds.Count (@($actualIds | Select-Object -Unique).Count) 'Fixture IDs must be unique.' -foreach ($fixture in $fixtures) { - Assert-True ($fixture -is $fixtureType) 'Loader must return immutable smoothing fixture values.' - Assert-True ($fixture.FixtureVersion -gt 0) "Fixture $($fixture.Id) must carry a positive version." - Assert-True $fixture.IsConfigurationFingerprintCurrent "Fixture $($fixture.Id) must match its stored configuration fingerprint." - Assert-True ($fixture.ConfigurationFingerprint -match '^sha256:[0-9a-f]{64}$') "Fixture $($fixture.Id) must expose a lowercase SHA-256 fingerprint." - Assert-True ($fixture.Path.Count -gt 1) "Fixture $($fixture.Id) must include a coarse path." - Assert-True ($fixture.Segments.Count -gt 0) "Fixture $($fixture.Id) must include direction segments." - foreach ($segment in $fixture.Segments) { - Assert-True ($segment.StartIndex -ge 0 -and $segment.EndIndex -lt $fixture.Path.Count -and $segment.EndIndex -ge $segment.StartIndex) "Fixture $($fixture.Id) must retain valid segment coverage." - } -} -foreach ($record in $fixtureDocument.scenarios) { - Assert-True ($null -ne $record.planningConfiguration) "Fixture $($record.id) must retain the planning configuration that produced its coarse path." -} - -$requests = $fixtureRequestsMethod.Invoke($null, @((Resolve-Path $FixturePath).Path)) -Assert-Equal 8 $requests.Count 'Fast fixtures must create exactly eight comparison requests without Hybrid A*.' -foreach ($request in $requests) { - Assert-True ($request -is $comparisonRequestType) 'Fast fixture factory must create comparison requests.' - $prepareArguments = [object[]]@($request.SmoothingRequest, $null, $null) - $prepared = $tryPrepareMethod.Invoke([Activator]::CreateInstance($preprocessorType), $prepareArguments) - Assert-True $prepared "Fixture path must satisfy the PathSmoothingPreprocessor input contract. Reason=$($prepareArguments[2])" -} - -$loaderSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\SmoothingScenarioFixtureLoader.cs') -Assert-True (-not $loaderSource.Contains('HybridAStarPlanner')) 'Fixture-only loading must not reference HybridAStarPlanner.' -$generatorSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\SmoothingFixtureGenerator.cs') -Assert-True $generatorSource.Contains('CoarsePathPlanningService') 'Fixture generation must snapshot actual successful coarse-path planning outputs.' -Assert-True $generatorSource.Contains('CoarsePathScenarioFactory') 'Fixture generation must use the established coarse-path scenarios where available.' - -function Write-CorruptedFixture([scriptblock]$Mutate) { - $temporaryPath = [IO.Path]::GetTempFileName() - $document = Get-Content -Raw -Encoding UTF8 $FixturePath | ConvertFrom-Json - & $Mutate $document - [IO.File]::WriteAllText($temporaryPath, ($document | ConvertTo-Json -Depth 16), [Text.UTF8Encoding]::new($false)) - return $temporaryPath -} - -$coverageFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].endIndex = 1 } -$directionFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].direction = 1 } -$switchFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].endsAtGearSwitch = $true } -try { - Assert-ThrowsMatching { $loadMethod.Invoke($null, @($coverageFixture)) } 'Fixture path contract' 'Loader must reject segment gaps before accepting the fingerprint.' - Assert-ThrowsMatching { $loadMethod.Invoke($null, @($directionFixture)) } 'Fixture path contract' 'Loader must reject segment direction mismatches before accepting the fingerprint.' - Assert-ThrowsMatching { $loadMethod.Invoke($null, @($switchFixture)) } 'Fixture path contract' 'Loader must reject illegal gear-switch topology before accepting the fingerprint.' -} -finally { - foreach ($temporaryPath in @($coverageFixture, $directionFixture, $switchFixture)) { - if ($temporaryPath -and [IO.File]::Exists($temporaryPath)) { [IO.File]::Delete($temporaryPath) } - } -} - -$generationPath = [IO.Path]::GetTempFileName() -try { - Assert-ThrowsMatching { $generateMethod.Invoke($null, @($generationPath, $false)) } '.+' 'Fixture generation must refuse to overwrite an existing target.' - $generateMethod.Invoke($null, @($generationPath, $true)) - $generatedFixtures = $loadMethod.Invoke($null, @($generationPath)) - Assert-Equal 8 $generatedFixtures.Count 'Generated fixture data must remain loadable and retain all scenarios.' -} -finally { - if ([IO.File]::Exists($generationPath)) { [IO.File]::Delete($generationPath) } -} - -Write-Output 'Path smoothing fixture checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 b/ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 deleted file mode 100644 index 8aee66e..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 +++ /dev/null @@ -1,642 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch { - return - } - throw $Message -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function New-GeometryPoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - [double]$Heading, - [double]$UnwrappedHeading, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($pointType, @( - $X, $Y, $ArcLength, $Heading, $UnwrappedHeading, - [double]1.0, $IsGearSwitch, $anchor)) -} - -function New-DirectionSegment( - [int]$Index, - $Direction, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch)) -} - -function New-DirectionSegmentWithStartCurvature( - [int]$Index, - $Direction, - [double]$StartVehicleCurvature, - [object[]]$Points, - [bool]$StartsAtGearSwitch = $false, - [bool]$EndsAtGearSwitch = $false) { - $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) - for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { - $typedPoints.SetValue($Points[$pointIndex], $pointIndex) - } - - return [Activator]::CreateInstance($segmentType, @( - $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch, $StartVehicleCurvature)) -} - -function New-CoarsePoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - $Direction, - [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($coarsePointType, @( - $X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction, - [double]0.0, [double]1.0, $IsGearSwitch, $coarseAnchor)) -} - -function Invoke-Analysis([object[]]$Segments, [double]$Spacing = 0.05) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - - $arguments = [object[]]@($typedSegments, $Spacing, $null, $null) - $accepted = $analyzeMethod.Invoke($analyzer, $arguments) - $description = [string]::Join(',', @($Segments | ForEach-Object { - "index=$($_.SegmentIndex);direction=$($_.Direction);points=$($_.Points.Count)" - })) - Assert-True $accepted ("Geometry analysis must accept the analytic candidate. Reason=" + $arguments[3] + '; Segments=' + $description) - Assert-True ($null -ne $arguments[2]) 'Successful geometry analysis must return PathGeometryAnalysis.' - return $arguments[2] -} - -function Assert-AnalysisRejected([object[]]$Segments, [string]$Message) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($segmentIndex = 0; $segmentIndex -lt $Segments.Count; $segmentIndex++) { - $typedSegments.SetValue($Segments[$segmentIndex], $segmentIndex) - } - - $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null) - $accepted = $analyzeMethod.Invoke($analyzer, $arguments) - Assert-True (-not $accepted) ($Message + '; Reason=' + $arguments[3]) -} - -function Invoke-RejectedAnalysis([object[]]$Segments, [string]$Message) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - - $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null) - $accepted = $analyzeMethod.Invoke($analyzer, $arguments) - Assert-False $accepted ($Message + '; Reason=' + $arguments[3]) -} - -function New-CoarsePathPoint( - [double]$X, - [double]$Y, - [double]$ArcLength, - $Direction, - [bool]$IsGearSwitch = $false, - [string]$SourceName = 'MotionPrimitive', - [double]$Heading = 0.0, - [double]$VehicleCurvature = 0.0) { - return [Activator]::CreateInstance($coarsePointType, @( - $X, $Y, $Heading, $Heading, $ArcLength, - $Direction, $VehicleCurvature, [double]1.0, $IsGearSwitch, - [Enum]::Parse($coarsePointSourceType, $SourceName))) -} - -function New-EmptyGeometryMap { - $mapRequest = [Activator]::CreateInstance($mapRequestType) - $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) - $mapRequest.ResolutionMm = [single]50 - $mapRequest.AllowExplicitEmptyMap = $true - $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map - Assert-True ($null -ne $map) 'Geometry test must create an explicit empty planning map.' - return $map -} - -function Invoke-GeometryValidation($Analysis, [object[]]$Segments, $Vehicle) { - $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) - for ($index = 0; $index -lt $Segments.Count; $index++) { - $typedSegments.SetValue($Segments[$index], $index) - } - - $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]](, $typedSegments)) - $arguments = [object[]]@( - $Analysis.Path, $Analysis.Segments, $preparedPath, (New-EmptyGeometryMap), $Vehicle, [double]0.05, - $null, [double]0.0, $null) - $accepted = $validateMethod.Invoke($validator, $arguments) - return [pscustomobject]@{ Accepted = $accepted; Reason = $arguments[8] } -} - -function New-CircularDirectionSegment( - $Direction, - [double]$Curvature, - [int]$Intervals, - [double]$ChordLength) { - $radius = 1.0 / $Curvature - $headingStep = 2.0 * [Math]::Asin($Curvature * $ChordLength / 2.0) - $points = New-Object System.Collections.Generic.List[object] - for ($index = 0; $index -le $Intervals; $index++) { - $theta = $headingStep * $index - $heading = if ($Direction.ToString() -eq 'Forward') { $theta } else { $theta + [Math]::PI } - [void]$points.Add((New-GeometryPoint ` - (1.0 + $radius * [Math]::Sin($theta)) ` - (1.0 + $radius * (1.0 - [Math]::Cos($theta))) ` - ($index * $ChordLength) $heading $heading)) - } - return New-DirectionSegment 0 $Direction $points.ToArray() -} - -function Get-OldPolylineCurvatureMaximum($Analysis) { - $maximum = 0.0 - $path = $Analysis.Path - for ($index = 0; $index -lt $path.Count; $index++) { - if ($path.Count -eq 1) { - $curvature = 0.0 - } - elseif ($index -eq 0) { - $curvature = ($path[1].UnwrappedHeading - $path[0].UnwrappedHeading) / - ($path[1].ArcLength - $path[0].ArcLength) - } - elseif ($index -eq $path.Count - 1) { - $curvature = ($path[$index].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) / - ($path[$index].ArcLength - $path[$index - 1].ArcLength) - } - else { - $curvature = ($path[$index + 1].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) / - ($path[$index + 1].ArcLength - $path[$index - 1].ArcLength) - } - $maximum = [Math]::Max($maximum, [Math]::Abs($curvature)) - } - return $maximum -} - -function Get-QuinticAnalyticCurvatureMaximum( - [double]$C2, - [double]$C3, - [double]$C4, - [double]$C5, - [int]$ReferenceSamples = 20000) { - $maximum = 0.0 - for ($index = 0; $index -lt $ReferenceSamples; $index++) { - $t = $index / [double]($ReferenceSamples - 1) - $firstDerivative = 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + - 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t - $secondDerivative = 2.0 * $C2 + 6.0 * $C3 * $t + - 12.0 * $C4 * $t * $t + 20.0 * $C5 * $t * $t * $t - $curvature = [Math]::Abs($secondDerivative / [Math]::Pow(1.0 + $firstDerivative * $firstDerivative, 1.5)) - $maximum = [Math]::Max($maximum, $curvature) - } - return $maximum -} - -function New-QuinticDirectionSegment( - [double]$C2, - [double]$C3, - [double]$C4, - [double]$C5, - [double]$DistributionPower, - [int]$Samples = 400) { - $points = New-Object System.Collections.Generic.List[object] - $arcLength = 0.0 - $previousX = 0.0 - $previousY = 0.0 - for ($index = 0; $index -lt $Samples; $index++) { - $t = [Math]::Pow($index / [double]($Samples - 1), $DistributionPower) - $x = 1.0 + $t - $y = $C2 * $t * $t + $C3 * $t * $t * $t + $C4 * $t * $t * $t * $t + $C5 * $t * $t * $t * $t * $t - if ($index -gt 0) { - $arcLength += [Math]::Sqrt(($x - $previousX) * ($x - $previousX) + ($y - $previousY) * ($y - $previousY)) - } - $heading = [Math]::Atan2( - 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t, - 1.0) - [void]$points.Add((New-GeometryPoint $x $y $arcLength $heading $heading)) - $previousX = $x - $previousY = $y - } - return New-DirectionSegment 0 $forward $points.ToArray() -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$processing = $root + 'Processing.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' - -$analyzerType = Get-RequiredType ($processing + 'PathGeometryAnalyzer') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D') -$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') -$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') -$analysisType = Get-RequiredType ($processing + 'PathGeometryAnalysis') -$preprocessorType = Get-RequiredType ($processing + 'PathSmoothingPreprocessor') -$resamplerType = Get-RequiredType ($processing + 'ArcLengthResampler') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') -$coarseSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap' -$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') -$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') -$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') -$smoothingRequestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$smoothingConfigurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm' -$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest' -$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory' -$validatorType = Get-RequiredType ($root + 'Validation.SmoothedPathValidator') -$rawBaselineBuilderType = Get-RequiredType ($processing + 'RawPathBaselineBuilder') - -Assert-True ($null -ne $preparedPathType) 'PreparedPath must be discoverable for smoothing algorithms.' -Assert-True ($null -ne $preprocessorType) 'PathSmoothingPreprocessor must be discoverable for request preparation.' -Assert-True ($null -ne $resamplerType) 'ArcLengthResampler must be discoverable for deterministic resampling.' -Assert-True ($null -ne $analysisType.GetProperty('MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter')) ` - 'PathGeometryAnalysis must expose peak d-kappa/d-s.' - -$analyzer = [Activator]::CreateInstance($analyzerType) -$analyzeMethod = $analyzerType.GetMethod('TryAnalyze') -Assert-True ($null -ne $analyzeMethod) 'PathGeometryAnalyzer must expose TryAnalyze.' -Assert-Equal 4 $analyzeMethod.GetParameters().Length 'TryAnalyze must accept segments, spacing, analysis, and reason.' -$validator = [Activator]::CreateInstance($validatorType) -$validateMethod = $validatorType.GetMethod('TryValidate') -Assert-True ($null -ne $validateMethod) 'SmoothedPathValidator must expose TryValidate.' -Assert-Equal 9 $validateMethod.GetParameters().Length 'SmoothedPathValidator.TryValidate must retain its public contract.' -$rawBaselineMethod = $rawBaselineBuilderType.GetMethod( - 'TryCreate', - [Reflection.BindingFlags]'Static,NonPublic') -Assert-True ($null -ne $rawBaselineMethod) 'RawPathBaselineBuilder must expose its internal TryCreate path.' - -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anchor = [Enum]::Parse($sourceType, 'Anchor') -$coarseAnchor = [Enum]::Parse($coarseSourceType, 'Start') - -# The chord-corrected estimator must retain an exact circular curvature limit for both travel directions. -$maximumAllowedCurvature = 5.0 / 6.0 -$circleVehicle = [Activator]::CreateInstance($vehicleType) -$circleVehicle.LengthMeters = 0.20 -$circleVehicle.WidthMeters = 0.20 -$circleVehicle.SafetyMarginMeters = 0.0 -$circleVehicle.MaximumCurvaturePerMeter = $maximumAllowedCurvature -foreach ($direction in @($forward, $reverse)) { - $exactLimitCircle = New-CircularDirectionSegment $direction $maximumAllowedCurvature 20 0.05 - $exactLimitAnalysis = Invoke-Analysis @($exactLimitCircle) - foreach ($point in $exactLimitAnalysis.Path) { - Assert-True ([Math]::Abs($point.VehicleCurvature) -le $maximumAllowedCurvature + 1.0e-9) ` - ('An exact-limit ' + $direction + ' circle must not exceed the curvature limit.') - } - $validation = Invoke-GeometryValidation $exactLimitAnalysis @($exactLimitCircle) $circleVehicle - Assert-True $validation.Accepted ('The validator must accept an analyzed exact-limit ' + $direction + ' circle. Reason=' + $validation.Reason) -} - -# An analyzed over-limit circle must remain detectable by the unchanged validator threshold. -$overLimitCircle = New-CircularDirectionSegment $forward ($maximumAllowedCurvature + 0.01) 20 0.05 -$overLimitAnalysis = Invoke-Analysis @($overLimitCircle) -Assert-True ($overLimitAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter -gt $maximumAllowedCurvature + 1.0e-6) ` - 'An over-limit circle must exceed the vehicle curvature limit by more than the validator tolerance.' -$overLimitValidation = Invoke-GeometryValidation $overLimitAnalysis @($overLimitCircle) $circleVehicle -Assert-False $overLimitValidation.Accepted 'The validator must reject an analyzed over-limit circle.' - -# Trusted raw fallback must apply the exact vehicle-curvature gate before reconstruction. -$trustedLimit = 0.80 -$trustedOverLimit = $trustedLimit + 0.0000005 -$analyzedCurvature = $trustedLimit + 0.01 -$chordLength = 0.05 -$headingStep = 2.0 * [Math]::Asin($analyzedCurvature * $chordLength / 2.0) -$radius = 1.0 / $analyzedCurvature -$trustedPoints = [Array]::CreateInstance($coarsePointType, 21) -for ($index = 0; $index -le 20; $index++) { - $theta = $headingStep * $index - $trustedPoints.SetValue((New-CoarsePathPoint ` - (1.0 + $radius * [Math]::Sin($theta)) ` - (1.0 + $radius * (1.0 - [Math]::Cos($theta))) ` - ($index * $chordLength) ` - $forward ` - $false ` - $(if ($index -eq 0) { 'Start' } else { 'MotionPrimitive' }) ` - $theta ` - $trustedOverLimit), $index) -} -$trustedSegments = [Array]::CreateInstance($coarseSegmentType, 1) -$trustedSegments.SetValue( - [Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 20, $false, $false)), - 0) -$trustedVehicle = [Activator]::CreateInstance($vehicleType) -$trustedVehicle.LengthMeters = 0.20 -$trustedVehicle.WidthMeters = 0.20 -$trustedVehicle.SafetyMarginMeters = 0.0 -$trustedVehicle.MaximumCurvaturePerMeter = $trustedLimit -$trustedConfiguration = [Activator]::CreateInstance($smoothingConfigurationType) -$trustedRequest = [Activator]::CreateInstance($smoothingRequestType, @( - $trustedPoints, - $trustedSegments, - (New-EmptyGeometryMap), - $trustedVehicle, - $trustedConfiguration)) -$trustedPreprocessor = [Activator]::CreateInstance($preprocessorType) -$trustedPrepareMethod = $preprocessorType.GetMethod('TryPrepare') -$trustedPrepareArguments = [object[]]@($trustedRequest, $null, $null) -Assert-True $trustedPrepareMethod.Invoke($trustedPreprocessor, $trustedPrepareArguments) ` - ('Trusted raw fallback regression must prepare successfully. Reason=' + $trustedPrepareArguments[2]) -$trustedBaselineArguments = [object[]]@( - $trustedRequest, - $trustedPrepareArguments[1], - $analyzer, - $trustedConfiguration.OutputSpacingMeters, - $validator, - $trustedConfiguration.MaximumCollisionCheckStepMeters, - $null, - $null) -Assert-False $rawBaselineMethod.Invoke($null, $trustedBaselineArguments) ` - 'Trusted raw fallback must not publish a coarse curvature above the exact vehicle maximum.' -Assert-Equal $overLimitValidation.Reason $trustedBaselineArguments[7] ` - 'Rejecting trusted raw fallback must preserve the original analyzed-path validation failure.' - -# The chord-corrected estimator must not under-estimate these smooth references more than the former polyline estimator. -foreach ($quinticCase in @( - [pscustomobject]@{ Name = 'SBend'; C2 = 0.0; C3 = 0.30; C4 = -0.45; C5 = 0.18; Power = 1.0 }, - [pscustomobject]@{ Name = 'EndpointPeak'; C2 = 0.18; C3 = -0.12; C4 = 0.0; C5 = 0.0; Power = 1.0 }, - [pscustomobject]@{ Name = 'NonUniformFinalInterval'; C2 = -0.12; C3 = 0.36; C4 = -0.30; C5 = 0.08; Power = 1.7 })) { - $quintic = New-QuinticDirectionSegment $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 $quinticCase.Power - $quinticAnalysis = Invoke-Analysis @($quintic) - $analyticMaximum = Get-QuinticAnalyticCurvatureMaximum $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 - $newDeficit = [Math]::Max(0.0, $analyticMaximum - $quinticAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter) - $oldDeficit = [Math]::Max(0.0, $analyticMaximum - (Get-OldPolylineCurvatureMaximum $quinticAnalysis)) - Assert-True ($newDeficit -le $oldDeficit + 1.0e-6) ` - ($quinticCase.Name + ' must not have greater one-sided curvature under-estimation than the old polyline estimator.') -} - -# A half-turn over a single chord has no unambiguous geometric curvature estimate. -$ambiguousTurn = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 ([Math]::PI) ([Math]::PI))) -Assert-AnalysisRejected @($ambiguousTurn) 'A two-point heading turn of π must be rejected as ambiguous.' - -# Forward straight: resampling is exactly 0.05 m, preserves the exact endpoint, and has zero curvature. -$straight = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -$straightAnalysis = Invoke-Analysis @($straight) -Assert-Equal 21 $straightAnalysis.Path.Count 'A one-metre straight must produce twenty 0.05 m intervals plus the initial point.' -Assert-Near 0.0 $straightAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 0.000000001 ` - 'A straight must have zero peak d-kappa/d-s.' -foreach ($point in $straightAnalysis.Path) { - Assert-Near 0.0 $point.VehicleCurvatureDerivative 0.000000001 ` - 'A straight point must carry zero d-kappa/d-s.' -} -for ($index = 1; $index -lt $straightAnalysis.Path.Count; $index++) { - $left = $straightAnalysis.Path[$index - 1] - $right = $straightAnalysis.Path[$index] - $distance = [Math]::Sqrt(($right.X - $left.X) * ($right.X - $left.X) + ($right.Y - $left.Y) * ($right.Y - $left.Y)) - Assert-Near 0.05 $distance 0.000000001 'Straight resampling intervals must be exactly 0.05 m.' - Assert-Near 0.0 $right.GeometricCurvature 0.000000001 'A forward straight must have zero geometric curvature.' - Assert-Near 0.0 $right.VehicleCurvature 0.000000001 'A forward straight must have zero vehicle curvature.' -} -$straightEnd = $straightAnalysis.Path[$straightAnalysis.Path.Count - 1] -Assert-Near 1.0 $straightEnd.X 0.0 'Resampling must retain the exact final X coordinate.' -Assert-Near 0.0 $straightEnd.Y 0.0 'Resampling must retain the exact final Y coordinate.' - -# Raw coarse anchors carry the vehicle pose, which may differ slightly from the chord tangent of a finite integration step. -$poseAnchoredCurve = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.2 1.0 0.4 0.4)) -$poseAnchoredAnalysis = Invoke-Analysis @($poseAnchoredCurve) -Assert-Near 0.0 $poseAnchoredAnalysis.Path[0].Heading 0.000000000001 'Geometry analysis must retain the first coarse-anchor heading rather than replace it with a chord tangent.' -$poseAnchoredEnd = $poseAnchoredAnalysis.Path[$poseAnchoredAnalysis.Path.Count - 1] -Assert-Near 0.4 $poseAnchoredEnd.Heading 0.000000000001 'Geometry analysis must retain the final coarse-anchor heading rather than replace it with a chord tangent.' - -# A forward R=2 quarter circle has positive +0.5 1/m vehicle curvature. -$forwardArcPoints = New-Object System.Collections.Generic.List[object] -for ($index = 0; $index -le 32; $index++) { - $theta = ([Math]::PI / 2.0) * $index / 32.0 - $x = 2.0 * [Math]::Sin($theta) - $y = 2.0 * (1.0 - [Math]::Cos($theta)) - $arcLength = 2.0 * $theta - [void]$forwardArcPoints.Add((New-GeometryPoint $x $y $arcLength $theta $theta)) -} -$forwardArc = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() -$forwardArcAnalysis = Invoke-Analysis @($forwardArc) -$forwardArcMidpoint = $forwardArcAnalysis.Path[[int]($forwardArcAnalysis.Path.Count / 2)] -Assert-Near 0.5 $forwardArcMidpoint.GeometricCurvature 0.01 'An R=2 quarter circle must have geometric curvature +0.5 1/m.' -Assert-Near 0.5 $forwardArcMidpoint.VehicleCurvature 0.01 'A forward R=2 quarter circle must have vehicle curvature +0.5 1/m.' - -# The same spatial R=2 circle in reverse retains geometric curvature but negates vehicle curvature. -$reverseArc = New-DirectionSegment 0 $reverse $forwardArcPoints.ToArray() -$reverseArcAnalysis = Invoke-Analysis @($reverseArc) -$reverseArcMidpoint = $reverseArcAnalysis.Path[[int]($reverseArcAnalysis.Path.Count / 2)] -Assert-Near 0.5 $reverseArcMidpoint.GeometricCurvature 0.01 'Reverse travel must not change geometric curvature.' -Assert-Near -0.5 $reverseArcMidpoint.VehicleCurvature 0.01 'A reverse R=2 quarter circle must have vehicle curvature -0.5 1/m.' - -# Gear-switch poses are intentionally duplicated: they keep equal arc length and never enter a derivative denominator. -$forwardBeforeSwitch = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true)) $false $true -$reverseAfterSwitch = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true), - (New-GeometryPoint 0.0 0.0 2.0 0.0 0.0)) $true $false -$switchAnalysis = Invoke-Analysis @($forwardBeforeSwitch, $reverseAfterSwitch) -$firstSegment = $switchAnalysis.Segments[0] -$secondSegment = $switchAnalysis.Segments[1] -$switchLeft = $switchAnalysis.Path[$firstSegment.EndIndex] -$switchRight = $switchAnalysis.Path[$secondSegment.StartIndex] -Assert-Near $switchLeft.X $switchRight.X 0.0 'Gear-switch endpoints must retain duplicate X coordinates.' -Assert-Near $switchLeft.Y $switchRight.Y 0.0 'Gear-switch endpoints must retain duplicate Y coordinates.' -Assert-Near $switchLeft.ArcLength $switchRight.ArcLength 0.0 'Gear-switch endpoints must retain duplicate arc length.' -Assert-Equal 'Forward' $switchLeft.Direction.ToString() 'The first gear-switch pose must retain its forward segment direction.' -Assert-Equal 'Reverse' $switchRight.Direction.ToString() 'The second gear-switch pose must retain its reverse segment direction.' -Assert-True (-not [double]::IsNaN($switchLeft.GeometricCurvature)) 'No derivative may cross the gear-switch duplicate point.' -Assert-True (-not [double]::IsNaN($switchRight.GeometricCurvature)) 'No reverse derivative may cross the gear-switch duplicate point.' -Assert-True (-not [double]::IsNaN($switchLeft.VehicleCurvatureDerivative)) ` - 'The forward side of a gear switch must have a finite one-sided derivative.' -Assert-True (-not [double]::IsNaN($switchRight.VehicleCurvatureDerivative)) ` - 'The reverse side of a gear switch must have a finite one-sided derivative.' - -# Curvature at a curved segment's end and the following straight reverse segment's start must remain independently differentiated. -$forwardArcToSwitch = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() $false $true -$reverseStraightAfterArc = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 2.0 2.0 0.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0) $true), - (New-GeometryPoint 2.0 1.0 1.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0))) $true $false -$curveSwitchAnalysis = Invoke-Analysis @($forwardArcToSwitch, $reverseStraightAfterArc) -$reverseStraightStart = $curveSwitchAnalysis.Path[$curveSwitchAnalysis.Segments[1].StartIndex] -Assert-Near 0.0 $reverseStraightStart.GeometricCurvature 0.000000001 'A gear-switch must not use the preceding curve to differentiate a reverse straight segment.' -Assert-Near 0.0 $reverseStraightStart.VehicleCurvatureDerivative 0.000000001 ` - 'No curvature derivative may cross from the preceding forward arc into a reverse straight.' - -$physicalStart = New-DirectionSegmentWithStartCurvature 0 $forward 0.20 @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -$physicalStartAnalysis = Invoke-Analysis @($physicalStart) -Assert-Near 0.20 $physicalStartAnalysis.Path[0].VehicleCurvature 0.000000001 ` - 'The unified analyzer must retain a real start steering-curvature boundary state.' -Assert-True ($physicalStartAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter -gt 0.0) ` - 'A real start-curvature mismatch must remain visible to the quality analyzer.' - -# A boundary that claims a gear switch must be a duplicated pose with opposite direction; discontinuities are rejected. -$invalidSwitch = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 1.2 0.0 1.0 0.0 0.0 $true), - (New-GeometryPoint 0.2 0.0 2.0 0.0 0.0)) $true $false -Assert-AnalysisRejected @($forwardBeforeSwitch, $invalidSwitch) 'A discontinuous gear-switch boundary must be rejected.' -$trailingGearSwitch = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) $false $true -Assert-AnalysisRejected @($trailingGearSwitch) 'The final direction segment must not advertise a non-existent trailing gear switch.' - -# The public preprocessor receives a raw coarse path and resets every prepared direction segment to local arc length zero. -$coarsePoints = [Array]::CreateInstance($coarsePointType, 4) -$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 0.0 $forward), 0) -$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $forward), 1) -$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $reverse $true), 2) -$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 2.0 $reverse), 3) -$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 1, $false, $true)), 0) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 2, 3, $true, $false)), 1) -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = 1.0 -$vehicle.WidthMeters = 0.5 -$vehicle.SafetyMarginMeters = 0.0 -$vehicle.MaximumCurvaturePerMeter = 1.0 -$configuration = [Activator]::CreateInstance($configurationType) -$uninitializedMap = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($mapType) -$request = [Activator]::CreateInstance($requestType, @($coarsePoints, $coarseSegments, $uninitializedMap, $vehicle, $configuration)) -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareMethod = $preprocessorType.GetMethod('TryPrepare') -$prepareArguments = [object[]]@($request, $null, $null) -$prepared = $prepareMethod.Invoke($preprocessor, $prepareArguments) -Assert-True $prepared ('Preprocessor must accept a legal forward/reverse raw coarse path. Reason=' + $prepareArguments[2]) -Assert-Near 0.0 $prepareArguments[1].Segments[1].Points[0].ArcLength 0.0 'Every prepared direction segment must begin at local arc length zero.' -Assert-True $prepareArguments[1].Segments[1].Points[0].IsGearSwitchPoint 'The reverse prepared segment must retain its gear-switch point.' - -# Finite coordinates can still overflow distance arithmetic; public resampling must reject them instead of emitting NaN/Infinity. -$overflowSegment = New-DirectionSegment 0 $forward @( - (New-GeometryPoint -1.0e308 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0e308 0.0 1.0 0.0 0.0)) -$resampler = [Activator]::CreateInstance($resamplerType) -$segmentResampleMethod = @($resamplerType.GetMethods() | Where-Object { - $_.Name -eq 'TryResample' -and $_.GetParameters()[0].ParameterType -eq $segmentType -})[0] -$resampleArguments = [object[]]@($overflowSegment, [double]0.05, $null, $null) -$resampled = $segmentResampleMethod.Invoke($resampler, $resampleArguments) -Assert-True (-not $resampled) ('Resampling must reject a distance overflow. Reason=' + $resampleArguments[3]) - -# A prepared path must reject null direction segments rather than silently dropping them during flattening. -$nullSegmentArray = [Array]::CreateInstance($segmentType, 1) -$nullSegmentRejected = $false -try { [void][Activator]::CreateInstance($preparedPathType, @($nullSegmentArray)) } catch { $nullSegmentRejected = $true } -Assert-True $nullSegmentRejected 'PreparedPath must reject a null direction segment.' - -# Unwrapped heading must not jump by 2π when tangents cross the -π/π branch cut. -$crossing = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)), - (New-GeometryPoint -1.0 ([Math]::Tan(10.0 * [Math]::PI / 180.0)) 1.015 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)), - (New-GeometryPoint -2.0 0.0 2.03 (-170.0 * [Math]::PI / 180.0) (-170.0 * [Math]::PI / 180.0))) -$crossingAnalysis = Invoke-Analysis @($crossing) -for ($index = 1; $index -lt $crossingAnalysis.Path.Count; $index++) { - $difference = [Math]::Abs($crossingAnalysis.Path[$index].UnwrappedHeading - $crossingAnalysis.Path[$index - 1].UnwrappedHeading) - Assert-True ($difference -lt [Math]::PI) 'Unwrapped headings must remain continuous across the ±π branch cut.' -} - -# The request preprocessor must reset arc length independently for every direction segment, -# while retaining the duplicated pose that represents a legal forward-to-reverse gear switch. -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareMethod = $preprocessorType.GetMethod('TryPrepare') -Assert-True ($null -ne $prepareMethod) 'PathSmoothingPreprocessor must expose TryPrepare.' -$coarsePath = [Array]::CreateInstance($coarsePointType, 5) -$coarsePath.SetValue((New-CoarsePathPoint 0.0 0.0 0.0 $forward $false 'Start'), 0) -$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 1.0 $forward), 1) -$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $forward), 2) -$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $reverse $true), 3) -$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 3.0 $reverse), 4) -$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 2, $false, $true)), 0) -$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 3, 4, $true, $false)), 1) -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = [double]0.80 -$vehicle.WidthMeters = [double]0.60 -$vehicle.SafetyMarginMeters = [double]0.05 -$vehicle.MaximumCurvaturePerMeter = [double]0.80 -$configuration = [Activator]::CreateInstance($smoothingConfigurationType) -$smoothingRequest = [Activator]::CreateInstance($smoothingRequestType, @( - $coarsePath, $coarseSegments, (New-EmptyGeometryMap), $vehicle, $configuration)) -$prepareArguments = [object[]]@($smoothingRequest, $null, $null) -Assert-True $prepareMethod.Invoke($preprocessor, $prepareArguments) ('Preprocessor must accept legal forward/reverse topology. Reason=' + $prepareArguments[2]) -$preparedPath = $prepareArguments[1] -Assert-Equal 2 $preparedPath.Segments.Count 'Preprocessor must preserve both direction segments.' -Assert-Near 0.0 $preparedPath.Segments[1].Points[0].ArcLength 0.0 'The reverse segment must restart local arc length at zero.' -Assert-True $preparedPath.Segments[1].Points[0].IsGearSwitchPoint 'The duplicate reverse gear-switch point must be retained.' - -# Segments may meet only at a paired, coincident forward/reverse gear switch. -$illegalGearJump = New-DirectionSegment 1 $reverse @( - (New-GeometryPoint 1.25 0.0 1.0 0.0 0.0 $true), - (New-GeometryPoint 0.25 0.0 2.0 0.0 0.0)) $true $false -Invoke-RejectedAnalysis @($forwardBeforeSwitch, $illegalGearJump) 'A gear-switch boundary whose poses differ must be rejected.' -$illegalNormalBoundary = New-DirectionSegment 1 $forward @( - (New-GeometryPoint 2.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 3.0 0.0 1.0 0.0 0.0)) $false $false -Invoke-RejectedAnalysis @($straight, $illegalNormalBoundary) 'A non-gear segment boundary must be rejected.' - -# Finite endpoint coordinates can still overflow while computing their separation; reject before interpolation. -$resampler = [Activator]::CreateInstance($resamplerType) -$resamplePointsMethod = $resamplerType.GetMethods() | Where-Object { - $_.Name -eq 'TryResample' -and $_.GetParameters().Length -eq 4 -and - $_.GetParameters()[0].ParameterType -eq [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($pointType) -} | Select-Object -First 1 -Assert-True ($null -ne $resamplePointsMethod) 'ArcLengthResampler must expose point-list TryResample.' -$hugePoints = [Array]::CreateInstance($pointType, 2) -$hugeCoordinate = [double]::MaxValue / 2.0 -$hugePoints.SetValue((New-GeometryPoint (-$hugeCoordinate) 0.0 0.0 0.0 0.0), 0) -$hugePoints.SetValue((New-GeometryPoint $hugeCoordinate 0.0 1.0 0.0 0.0), 1) -$resampleArguments = [object[]]@($hugePoints, [double]0.05, $null, $null) -Assert-False $resamplePointsMethod.Invoke($resampler, $resampleArguments) 'Resampling must reject an infinite geometric distance caused by finite coordinates.' - -# PreparedPath is an all-or-nothing immutable topology snapshot: null direction segments are invalid. -$nullPreparedSegments = [Array]::CreateInstance($segmentType, 1) -Assert-Throws { [Activator]::CreateInstance($preparedPathType, @($nullPreparedSegments)) } 'PreparedPath must reject null direction segments.' - -# Segment indices are deliberately dense and equal to their position in the candidate array. -$sparseSegment = New-DirectionSegment 2 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -Invoke-RejectedAnalysis @($sparseSegment) 'Prepared direction-segment indices must match their dense array position.' - -Write-Output 'Path smoothing geometry checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 deleted file mode 100644 index f75ebb8..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 +++ /dev/null @@ -1,246 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function Get-InternalProperty($Instance, [string]$Name) { - return $Instance.GetType().GetProperty( - $Name, - [Reflection.BindingFlags]'Public,NonPublic,Instance').GetValue($Instance) -} - -function Get-InternalMethod($Type, [string]$Name) { - return @($Type.GetMethods([Reflection.BindingFlags]'Public,NonPublic,Instance,Static') | - Where-Object Name -eq $Name)[0] -} - -function New-InternalInstance($Type) { - return [Activator]::CreateInstance( - $Type, - [Reflection.BindingFlags]'Instance,NonPublic,Public', - $null, - @(), - $null) -} - -$builderType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2CandidateBuilder' -$hooksType = $builderType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $hooksType) 'LocalG2CandidateBuilder must expose narrowly scoped deterministic TestHooks.' -$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $executeMethod) 'TestHooks must execute deterministic candidate scenarios.' - -function Invoke-Scenario([string]$Scenario) { - return $executeMethod.Invoke($null, @($Scenario)) -} - -$isolated = Invoke-Scenario 'Isolated' -Assert-True ($isolated.CandidateCount -gt 0 -and $isolated.CandidateCount -le 12) ` - 'An isolated event must produce a bounded non-empty candidate set.' -Assert-Near 0.0 $isolated.StartPositionError 1e-9 'Window start position must match.' -Assert-Near 0.0 $isolated.EndPositionError 1e-9 'Window end position must match.' -Assert-Near 0.0 $isolated.StartCurvatureError 1e-8 'Window start curvature must match.' -Assert-Near 0.0 $isolated.EndCurvatureError 1e-8 'Window end curvature must match.' -Assert-True $isolated.ContainsLocalG2Source 'Generated samples must identify their source.' - -$cluster = Invoke-Scenario 'Cluster' -Assert-Equal 1 $cluster.OutputRegionCount 'Overlapping events must be built as one region.' -Assert-True $cluster.InternalConnectionsAreG2 'Internal anchors must share tangent and curvature.' - -$reverse = Invoke-Scenario 'Reverse' -Assert-Equal 'Reverse' $reverse.Direction 'Reverse candidates must preserve segment direction.' -Assert-True $reverse.VehicleAndGeometricCurvatureSignsAreOpposite ` - 'Reverse candidates must convert vehicle curvature to geometric curvature exactly once.' - -$spliced = Invoke-Scenario 'Spliced' -Assert-True $spliced.NoDuplicateNonGearPoints 'Splicing must remove duplicate boundary samples.' -Assert-True $spliced.EndpointsUnchanged 'Splicing must preserve the complete segment endpoints.' - -$startBoundary = Invoke-Scenario 'StartBoundary' -Assert-True ($startBoundary.CandidateCount -gt 0) 'A start-boundary event must build a one-sided candidate.' -Assert-Near 0.0 $startBoundary.StartCurvatureError 1e-8 ` - 'A start-boundary candidate must use the preserved physical start vehicle curvature.' - -$interiorDerivative = Invoke-Scenario 'InteriorStationaryCurve' -Assert-True $interiorDerivative.Rejected ` - 'A curve with a stationary interior derivative must be rejected even when its endpoint chord is short.' - -$constantVelocity = Invoke-Scenario 'ConstantVelocityCurve' -Assert-True $constantVelocity.Accepted ` - 'A nonstationary constant-velocity curve with collinear derivative controls must be accepted.' - -$exactSplice = Invoke-Scenario 'ExactSpliceEndpoints' -Assert-True $exactSplice.EndpointsAreExact ` - 'Splicing must write interpolated exact endpoints instead of accepting approximate candidate endpoints.' - -$gearBoundary = Invoke-Scenario 'GearBoundary' -Assert-True $gearBoundary.GearBoundaryMarkerPreserved ` - 'A window touching a gear-switch segment boundary must preserve its point-level gear marker.' - -$twoRegions = Invoke-Scenario 'TwoRegionWorkOrder' -Assert-True $twoRegions.WorkOrderDescending ` - 'Same-segment regions must be processed from larger original local arc to smaller local arc.' -Assert-True $twoRegions.FrontArcPreservedAfterBackReplacement ` - 'Replacing the back region must preserve the front region original arc coordinates.' -Assert-True $twoRegions.BothReplacementsRetained ` - 'Back-then-front replacement must retain the exact LocalG2Transition interiors at (0.75, 0.20) and (2.25, 0.20).' -Assert-True $twoRegions.ForwardOrderRejected ` - 'The regression fixture must prove that front-first invalidates the original back absolute arc.' -Assert-True $twoRegions.DeterministicWorkOrder ` - 'Work ordering must repeat exactly without mutating report order.' -Assert-True $twoRegions.DeterministicReplacementGeometry ` - 'Repeated back-then-front replacement must preserve count and every point X, Y, source, and direction.' -Assert-True $twoRegions.InvalidWorkOrderRejected ` - 'Work ordering must reject null regions and cross-segment event contents.' - -$ordering = Invoke-Scenario 'MultiSegmentWorkOrder' -Assert-True $ordering.SegmentOrderAscending ` - 'Region work ordering must process segment indices in ascending order.' -Assert-True $ordering.EqualArcUsesReportOrder ` - 'Equal first-local-arc regions must retain their original report-index order.' - -$evaluatorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2CandidateEvaluator' -$evaluatorHooksType = $evaluatorType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $evaluatorHooksType) 'LocalG2CandidateEvaluator must expose narrowly scoped deterministic TestHooks.' -$evaluateMethod = $evaluatorHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $evaluateMethod) 'TestHooks must execute deterministic candidate quality-gate scenarios.' - -function Invoke-EvaluationScenario([string]$Scenario) { - return $evaluateMethod.Invoke($null, @($Scenario)) -} - -$tooFar = Invoke-EvaluationScenario 'TooFar' -Assert-Equal 'DeviationExceeded' $tooFar.FailureReason ` - 'A collision-free candidate more than 0.10 m from the raw window must be rejected.' - -$overshoot = Invoke-EvaluationScenario 'Overshoot' -Assert-Equal 'CurvatureOvershoot' $overshoot.FailureReason ` - 'A candidate outside the raw regional curvature range must be rejected.' - -$noOp = Invoke-EvaluationScenario 'NoOp' -Assert-Equal 'InsufficientImprovement' $noOp.FailureReason ` - 'A safe no-op must not be accepted.' - -$oscillating = Invoke-EvaluationScenario 'Oscillating' -Assert-Equal 'VariationCostRegression' $oscillating.FailureReason ` - 'Repeated curvature oscillation must fail the variation-cost gate.' - -$lowClearance = Invoke-EvaluationScenario 'LowClearance' -Assert-Equal 'InsufficientClearance' $lowClearance.FailureReason ` - 'A path with less than 0.02 m checked clearance must be rejected.' - -$improved = Invoke-EvaluationScenario 'Improved' -Assert-Equal 'Accepted' $improved.Status ` - 'A safe candidate with at least 20 percent peak improvement must be accepted.' - -$smallestDeviation = Invoke-EvaluationScenario 'SmallestDeviation' -$best = Invoke-EvaluationScenario 'Best' -Assert-Equal $smallestDeviation.CandidateIndex $best.CandidateIndex ` - 'Among sufficient candidates, minimum deviation must win before extra smoothness.' - -# Exercise the real builder → evaluator seam that a hand-built TestHook candidate does not cover. -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$factoryType = Get-RequiredType ($root + 'Test.SmoothingScenarioFactory') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$methodType = Get-RequiredType ($root + 'SmoothingMethod') -$preprocessorType = Get-RequiredType ($root + 'Processing.PathSmoothingPreprocessor') -$optionsType = Get-RequiredType ($root + 'LocalG2.LocalG2OptionsSnapshot') -$detectorType = Get-RequiredType ($root + 'LocalG2.CurvatureTransitionDetector') -$plannerType = Get-RequiredType ($root + 'LocalG2.LocalG2WindowPlanner') -$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json' -$fixtures = (Get-InternalMethod $factoryType 'CreateFixtureRequests').Invoke($null, @((Resolve-Path $fixturePath).Path)) -$baseRequest = $fixtures[1].SmoothingRequest # single-turn -$configuration = [Activator]::CreateInstance($configurationType) -$configuration.Method = [Enum]::Parse($methodType, 'LocalG2Quintic') -$singleTurnRequest = [Activator]::CreateInstance($requestType, @( - $baseRequest.CoarsePath, $baseRequest.Segments, $baseRequest.Map, $baseRequest.Vehicle, $configuration)) -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareArgs = [object[]]@($singleTurnRequest, $null, $null) -Assert-True ((Get-InternalMethod $preprocessorType 'TryPrepare').Invoke($preprocessor, $prepareArgs)) ` - 'SingleTurn must prepare before evaluating Local G2 candidates.' -$preparedPath = $prepareArgs[1] -$options = [Activator]::CreateInstance($optionsType, [Reflection.BindingFlags]'Instance,NonPublic,Public', $null, @($configuration), $null) -$detector = New-InternalInstance $detectorType -$detectArgs = [object[]]@($singleTurnRequest, [double]$singleTurnRequest.Vehicle.MaximumCurvaturePerMeter, $options, $null, $null) -Assert-True ((Get-InternalMethod $detectorType 'TryDetect').Invoke($detector, $detectArgs)) ` - 'SingleTurn must detect its Local G2 transition.' -$planner = New-InternalInstance $plannerType -$planArgs = [object[]]@($preparedPath, $detectArgs[3], $options, $null, $null) -Assert-True ((Get-InternalMethod $plannerType 'TryPlan').Invoke($planner, $planArgs)) ` - 'SingleTurn must plan a Local G2 region.' -$region = $null -foreach ($plannedRegion in $planArgs[3]) { $region = $plannedRegion; break } -$preparedSegment = (Get-InternalProperty $preparedPath 'Segments')[(Get-InternalProperty $region 'SegmentIndex')] -$realBuilder = New-InternalInstance $builderType -$realCandidates = (Get-InternalMethod $builderType 'Build').Invoke($realBuilder, @( - $preparedSegment, $region, [double]$configuration.OutputSpacingMeters, $options, [Threading.CancellationToken]::None)) -Assert-True ($realCandidates.Count -gt 0) 'SingleTurn must build real Local G2 candidates.' -$realEvaluator = New-InternalInstance $evaluatorType -$duplicateFailures = @() -$acceptedSplitCandidates = @() -$maximumAnchorError = 0.0 -$maximumConnectionPositionError = 0.0 -$maximumTangentDirectionError = 0.0 -$maximumCurvatureError = 0.0 -foreach ($realCandidate in $realCandidates) { - $evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @( - $preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options, - [Threading.CancellationToken]::None)) - $failureReason = Get-InternalProperty $evaluation 'FailureReason' - if ($failureReason -eq 2) { $duplicateFailures += (Get-InternalProperty $realCandidate 'CandidateIndex') } - - $hasDifferentScale = $false - foreach ($diagnostic in (Get-InternalProperty $realCandidate 'InternalScaleDiagnostics')) { - $anchorError = [double](Get-InternalProperty $diagnostic 'AnchorPositionErrorMeters') - $maximumAnchorError = [Math]::Max($maximumAnchorError, $anchorError) - $maximumConnectionPositionError = [Math]::Max( - $maximumConnectionPositionError, - [double](Get-InternalProperty $diagnostic 'ConnectionPositionErrorMeters')) - $maximumTangentDirectionError = [Math]::Max( - $maximumTangentDirectionError, - [double](Get-InternalProperty $diagnostic 'TangentDirectionErrorRadians')) - $maximumCurvatureError = [Math]::Max( - $maximumCurvatureError, - [double](Get-InternalProperty $diagnostic 'CurvatureErrorPerMeter')) - $incoming = [double](Get-InternalProperty $diagnostic 'IncomingDerivativeScale') - $outgoing = [double](Get-InternalProperty $diagnostic 'OutgoingDerivativeScale') - if ([Math]::Abs($incoming - $outgoing) -gt 1e-10) { $hasDifferentScale = $true } - } - if ((Get-InternalProperty $evaluation 'Accepted') -and $hasDifferentScale) { - $acceptedSplitCandidates += $realCandidate - } -} -Assert-Equal 0 $duplicateFailures.Count ` - 'SingleTurn candidates must not fail raw-window analysis on coincident boundary points.' -Assert-True ($maximumAnchorError -le 1e-9) ` - 'Every internal primitive boundary must remain at its original coordinate.' -Assert-True ($maximumConnectionPositionError -le 1e-9) ` - 'Every real split-scale connection must be position continuous.' -Assert-True ($maximumTangentDirectionError -le 1e-8) ` - 'Every real split-scale connection must preserve unit tangent direction.' -Assert-True ($maximumCurvatureError -le 1e-8) ` - 'Every real split-scale connection must preserve geometric curvature.' -Assert-True ($acceptedSplitCandidates.Count -gt 0) ` - 'SingleTurn must accept a zero-coordinate-offset candidate with different incoming/outgoing scales.' - -Write-Output 'Path smoothing Local G2 candidate checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 deleted file mode 100644 index 2187dc4..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Assert-Rejected($Result, [string]$Message) { - if ($Result.Accepted) { throw $Message } - Assert-True (-not [string]::IsNullOrWhiteSpace($Result.Reason)) 'Rejected curves must provide a stable reason.' -} - -function Assert-Throws([scriptblock]$Action, [string]$Message) { - try { - & $Action - } - catch [ArgumentOutOfRangeException] { - return - } - catch [System.Reflection.TargetInvocationException] { - Assert-True ($_.Exception.InnerException -is [ArgumentOutOfRangeException]) $Message - return - } - throw $Message -} - -$curveType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.QuinticHermiteCurve2D', $true) -$tryCreate = $curveType.GetMethod('TryCreate', [Reflection.BindingFlags]'Static,NonPublic') -$evaluate = $curveType.GetMethod('Evaluate', [Reflection.BindingFlags]'Instance,NonPublic') -Assert-True ($null -ne $tryCreate) 'QuinticHermiteCurve2D must expose its internal TryCreate factory.' -Assert-True ($null -ne $evaluate) 'QuinticHermiteCurve2D must expose its internal Evaluate method.' - -function Invoke-Curve( - [double]$X0, [double]$Y0, [double]$Dx0, [double]$Dy0, [double]$Ddx0, [double]$Ddy0, - [double]$X1, [double]$Y1, [double]$Dx1, [double]$Dy1, [double]$Ddx1, [double]$Ddy1) { - $arguments = [object[]]@($X0, $Y0, $Dx0, $Dy0, $Ddx0, $Ddy0, $X1, $Y1, $Dx1, $Dy1, $Ddx1, $Ddy1, $null, $null) - $accepted = $tryCreate.Invoke($null, $arguments) - Assert-True $accepted "Quintic Hermite curve creation must succeed. Reason=$($arguments[13])" - return $arguments[12] -} - -function Invoke-InvalidCurve([string]$CaseName) { - $arguments = switch ($CaseName) { - 'ZeroFirstDerivative' { [object[]]@(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 0.0, 0.0, $null, $null) } - 'NonFiniteInput' { [object[]]@(0.0, 0.0, [double]::NaN, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 0.0, 0.0, $null, $null) } - default { throw "Unknown invalid curve case: $CaseName" } - } - return [PSCustomObject]@{ - Accepted = $tryCreate.Invoke($null, $arguments) - Reason = [string]$arguments[13] - } -} - -function Invoke-Evaluate($Curve, [double]$U) { - $arguments = [object[]]@($U, $null, $null, $null, $null, $null, $null) - $evaluate.Invoke($Curve, $arguments) - return [PSCustomObject]@{ - X = [double]$arguments[1]; Y = [double]$arguments[2] - Dx = [double]$arguments[3]; Dy = [double]$arguments[4] - Ddx = [double]$arguments[5]; Ddy = [double]$arguments[6] - } -} - -function Get-Curvature($Sample) { - return ($Sample.Dx * $Sample.Ddy - $Sample.Dy * $Sample.Ddx) / - [Math]::Pow($Sample.Dx * $Sample.Dx + $Sample.Dy * $Sample.Dy, 1.5) -} - -function Assert-Boundary($Expected, $Actual, [string]$Name) { - Assert-Near $Expected.X $Actual.X 1e-10 "$Name X must match." - Assert-Near $Expected.Y $Actual.Y 1e-10 "$Name Y must match." - Assert-Near $Expected.Dx $Actual.Dx 1e-10 "$Name dX must match." - Assert-Near $Expected.Dy $Actual.Dy 1e-10 "$Name dY must match." - Assert-Near $Expected.Ddx $Actual.Ddx 1e-10 "$Name ddX must match." - Assert-Near $Expected.Ddy $Actual.Ddy 1e-10 "$Name ddY must match." -} - -$straight = Invoke-Curve 0 0 1 0 0 0 2 0 1 0 0 0 -$straightStart = Invoke-Evaluate $straight 0.0 -$straightEnd = Invoke-Evaluate $straight 1.0 -$straightMid = Invoke-Evaluate $straight 0.5 -Assert-Boundary ([PSCustomObject]@{ X = 0.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.0 }) $straightStart 'Straight start' -Assert-Boundary ([PSCustomObject]@{ X = 2.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.0 }) $straightEnd 'Straight end' -Assert-Near 0.0 $straightMid.Y 1e-12 'A straight Hermite curve must remain on the axis.' - -$curved = Invoke-Curve 0 0 1 0 0 0.4 1 1 0 1 -0.4 0 -$curvedStart = Invoke-Evaluate $curved 0.0 -$curvedEnd = Invoke-Evaluate $curved 1.0 -Assert-Boundary ([PSCustomObject]@{ X = 0.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.4 }) $curvedStart 'Curved start' -Assert-Boundary ([PSCustomObject]@{ X = 1.0; Y = 1.0; Dx = 0.0; Dy = 1.0; Ddx = -0.4; Ddy = 0.0 }) $curvedEnd 'Curved end' -Assert-Near 0.4 (Get-Curvature $curvedStart) 1e-10 'Start curvature must match boundary derivatives.' -Assert-Near 0.4 (Get-Curvature $curvedEnd) 1e-10 'End curvature must match boundary derivatives.' - -$allFinite = $true -foreach ($u in @(0.0, 0.25, 0.5, 0.75, 1.0)) { - $sample = Invoke-Evaluate $curved $u - foreach ($value in @($sample.X, $sample.Y, $sample.Dx, $sample.Dy, $sample.Ddx, $sample.Ddy)) { - $allFinite = $allFinite -and (-not [double]::IsNaN($value)) -and (-not [double]::IsInfinity($value)) - } -} -Assert-True $allFinite 'All sampled values must be finite.' - -Assert-Rejected (Invoke-InvalidCurve 'ZeroFirstDerivative') ` - 'A curve endpoint with zero first derivative must be rejected.' -Assert-Rejected (Invoke-InvalidCurve 'NonFiniteInput') ` - 'A curve with a non-finite boundary value must be rejected.' -Assert-Throws { Invoke-Evaluate $curved -0.001 } 'Evaluate must reject parameters below zero.' -Assert-Throws { Invoke-Evaluate $curved 1.001 } 'Evaluate must reject parameters above one.' - -Write-Output 'Local G2 quintic curve checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 deleted file mode 100644 index 68142c9..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -$detectorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.CurvatureTransitionDetector' -$hooksType = $detectorType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $hooksType) 'CurvatureTransitionDetector must expose its narrowly scoped nested TestHooks helper.' -$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') -Assert-True ($null -ne $executeMethod) 'TestHooks must expose deterministic scenario execution for reflection tests.' - -function Invoke-Scenario([string]$Scenario) { - return $executeMethod.Invoke($null, @($Scenario)) -} - -# Same direction: 0 -> 0.4167 is detected once. -$singleTransition = Invoke-Scenario 'SingleTransition' -Assert-Equal 1 $singleTransition.TransitionCount 'One primitive curvature jump must be detected.' -Assert-Near 0.4167 $singleTransition.MaximumJump 0.0001 'The jump magnitude must be retained.' - -# Same pose at a forward/reverse boundary: no event crosses the stop. -$gearSwitch = Invoke-Scenario 'GearSwitch' -Assert-Equal 0 $gearSwitch.TransitionCount 'A stopped gear switch must not be a smoothing event.' - -# A 0.01 1/m numerical change is below max(0.001, 5% of 0.8333). -$noise = Invoke-Scenario 'Noise' -Assert-Equal 0 $noise.TransitionCount 'Sub-threshold curvature noise must be ignored.' - -# Two 0.50 m windows whose ranges overlap are merged. -$overlap = Invoke-Scenario 'Overlap' -Assert-Equal 1 $overlap.RegionCount 'Overlapping windows must form one joint region.' -Assert-Equal 2 $overlap.TransitionCountInFirstRegion 'The merged region must retain both events.' - -# Near a segment start, the window becomes asymmetric without crossing the hard boundary. -$nearStart = Invoke-Scenario 'NearStart' -Assert-Near 0.0 $nearStart.StartArcLength 0.000000001 'A start window must be clamped to the segment.' -Assert-True ($nearStart.RightWindowLength -gt $nearStart.LeftWindowLength) ` - 'Unavailable left length must be shifted to the right.' - -# Finite endpoint curvatures may still overflow during subtraction; detection must fail rather than publish infinity. -$coarsePathPointType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.CoarsePathPoint' -$pathSegmentType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.PathSegment' -$directionType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.TravelDirection' -$coarseSourceType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.CoarsePathPointSource' -$requestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingRequest' -$configurationType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingConfiguration' -$optionsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2OptionsSnapshot' -$forward = [Enum]::Parse($directionType, 'Forward') -$motionPrimitive = [Enum]::Parse($coarseSourceType, 'MotionPrimitive') -$overflowPoints = [Array]::CreateInstance($coarsePathPointType, 2) -$overflowPoints.SetValue([Activator]::CreateInstance($coarsePathPointType, @( - [double]0.0, [double]0.0, [double]0.0, [double]0.0, [double]0.0, $forward, - -[double]::MaxValue, [double]1.0, $false, $motionPrimitive)), 0) -$overflowPoints.SetValue([Activator]::CreateInstance($coarsePathPointType, @( - [double]0.1, [double]0.0, [double]0.0, [double]0.0, [double]0.1, $forward, - [double]::MaxValue, [double]1.0, $false, $motionPrimitive)), 1) -$overflowSegments = [Array]::CreateInstance($pathSegmentType, 1) -$overflowSegments.SetValue([Activator]::CreateInstance($pathSegmentType, @(0, $forward, 0, 1, $false, $false)), 0) -$configuration = [Activator]::CreateInstance($configurationType) -$optionsConstructor = $optionsType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, @($configurationType), $null) -Assert-True ($null -ne $optionsConstructor) 'Local G2 options must be constructible for detector reflection tests.' -$options = $optionsConstructor.Invoke(@($configuration)) -$overflowRequest = [Activator]::CreateInstance($requestType, @($overflowPoints, $overflowSegments, $null, $null, $configuration)) -$detector = [Activator]::CreateInstance($detectorType, $true) -$tryDetect = $detectorType.GetMethod('TryDetect', [Reflection.BindingFlags]'Instance,NonPublic') -Assert-True ($null -ne $tryDetect) 'CurvatureTransitionDetector must retain its internal TryDetect contract.' -$overflowArguments = [object[]]@($overflowRequest, [double]0.8333, $options, $null, $null) -$overflowAccepted = $tryDetect.Invoke($detector, $overflowArguments) -Assert-False $overflowAccepted 'Curvature subtraction overflow must reject detection.' -Assert-True (-not [string]::IsNullOrWhiteSpace([string]$overflowArguments[4])) ` - 'Rejected overflow detection must provide a stable reason.' -Assert-Equal 0 $overflowArguments[3].Count 'Rejected overflow detection must not publish a curvature event.' - -$plannerType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2WindowPlanner' -$plannerHooksType = $plannerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $plannerHooksType) 'LocalG2WindowPlanner must expose narrow deterministic TestHooks.' -$planScenario = $plannerHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') - -$separated = $planScenario.Invoke($null, @('SeparatedByOneMeter')) -Assert-Equal 2 $separated.RegionCount 'Events 1.0 m apart cannot share a 0.80 m total window.' -Assert-Equal '1,1' $separated.TransitionCounts 'Separated events must remain one event per region.' - -$mergeable = $planScenario.Invoke($null, @('Mergeable')) -Assert-Equal 1 $mergeable.RegionCount 'Events covered by one legal total window must merge.' -Assert-Equal '2' $mergeable.TransitionCounts 'The merged region must retain both events.' - -$partition = $planScenario.Invoke($null, @('ThreeEventPartition')) -Assert-Equal 2 $partition.RegionCount 'Three events must split at the first infeasible joint window.' -Assert-Equal '2,1' $partition.TransitionCounts 'Only a feasible consecutive subgroup may merge.' - -$boundary = $planScenario.Invoke($null, @('NearBoundary')) -Assert-True ($boundary.FirstRightLength -gt $boundary.FirstLeftLength) ` - 'A boundary-clamped total window must transfer missing length to the available side.' -Assert-True ($boundary.MaximumWindowLength -le 0.80 + 1e-9) ` - 'No candidate window may exceed 0.80 m total length.' - -$coverage = $planScenario.Invoke($null, @('InteriorCoverage')) -Assert-True $coverage.RepresentativeTargetsFirst ` - 'Preferred, minimum, and maximum balanced targets must precede asymmetric variants.' -Assert-True $coverage.HasAsymmetricVariant ` - 'Default window planning must retain a legal asymmetric variant after balanced coverage.' -Assert-True ($coverage.FirstRegionVariantCount -le 12) ` - 'Window planning must obey the configured default cap.' - -foreach ($snapshot in @($separated, $mergeable, $partition, $boundary, $coverage)) { - Assert-True $snapshot.ExactEnvelope 'Region envelope must equal the extrema of actual legal variants.' - Assert-True ($snapshot.MaximumWindowLength -le 0.80 + 1e-9) ` - 'MaximumWindowLengthMeters is a total, not a per-side length.' -} - -$repeat = $planScenario.Invoke($null, @('ThreeEventPartition')) -Assert-Equal $partition.Signature $repeat.Signature ` - 'Repeated planning must preserve grouping, candidate numbering and variant order.' - -Write-Output 'Path smoothing Local G2 detection checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 deleted file mode 100644 index 3fcb115..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$root = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing' -$sourcePath = Join-Path $root 'Contracts\SmoothedPathPointSource.cs' -$evidencePath = Join-Path $root 'Test\Fixtures\local-g2-diagnostic-single-turn.json' - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -$source = Get-Content -Raw -Encoding UTF8 $sourcePath -Assert-True ($source -match 'LocalG2Transition\s*=\s*4') ` - 'LocalG2Transition must retain serialized value 4 for diagnostic evidence compatibility.' - -$evidence = Get-Content -Raw -Encoding UTF8 $evidencePath | ConvertFrom-Json -foreach ($point in $evidence.CandidatePoints) { - Assert-True ($point.Source -eq 4) 'Diagnostic evidence points must use the LocalG2Transition serialized value.' -} - -Write-Output 'Local G2 diagnostic serialization contract checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 deleted file mode 100644 index d70bbb1..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -$ErrorActionPreference = 'Stop' - -function Assert-Equal($expected, $actual, [string]$message) { - if ($expected -ne $actual) { - throw "$message Expected: '$expected'. Actual: '$actual'." - } -} - -function Assert-True([bool]$condition, [string]$message) { - if (-not $condition) { - throw $message - } -} - -$repositoryRoot = Split-Path -Parent $PSScriptRoot -$assemblyPath = Join-Path $repositoryRoot 'bin\Debug\netstandard2.0\ClumsyPilot.dll' -$fixturePath = Join-Path $repositoryRoot 'ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json' - -if (-not (Test-Path -LiteralPath $assemblyPath)) { - throw "ClumsyPilot assembly was not found: $assemblyPath" -} - -$assembly = [System.Reflection.Assembly]::LoadFrom($assemblyPath) -$loaderType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.LocalG2DiagnosticEvidenceLoader', $true) -$loader = [System.Activator]::CreateInstance($loaderType) -$loadAndVerify = $loaderType.GetMethod('LoadAndVerify') -$evidence = $loadAndVerify.Invoke($loader, @($fixturePath)) - -Assert-Equal 'single-turn' $evidence.ScenarioId 'Diagnostic evidence scenario must be stable.' -Assert-Equal 'single-turn/s0/r0/w5/seed2' $evidence.CandidateStableKey 'Diagnostic evidence key must be stable.' -Assert-Equal 5 $evidence.CandidateIndex 'Diagnostic evidence candidate index must be stable.' -Assert-Equal 10 $evidence.CandidatePoints.Count 'Diagnostic evidence must retain all ten recorded samples.' -Assert-Equal 'InsufficientClearance' $evidence.EvaluatorResult 'Diagnostic evidence must retain the observed evaluator result.' -Assert-Equal 'Clearance' $evidence.StopGate 'Diagnostic evidence must retain the observed stop gate.' -Assert-Equal 'Unchanged' $evidence.PublishedStatus 'Diagnostic evidence must retain the strict final status.' - -function Assert-Rejected([string]$tamperedContent, [string]$message) { - $temporaryEvidencePath = Join-Path ([System.IO.Path]::GetTempPath()) ("local-g2-diagnostic-evidence-{0}.json" -f [System.Guid]::NewGuid()) - try { - [System.IO.File]::WriteAllText($temporaryEvidencePath, $tamperedContent, [System.Text.Encoding]::UTF8) - - $rejected = $false - try { - $null = $loadAndVerify.Invoke($loader, @($temporaryEvidencePath)) - } - catch { - $rejected = $true - } - - Assert-True $rejected $message - } - finally { - if (Test-Path -LiteralPath $temporaryEvidencePath) { - Remove-Item -LiteralPath $temporaryEvidencePath -Force - } - } -} - -$fixtureContent = Get-Content -LiteralPath $fixturePath -Raw -Assert-Rejected ($fixtureContent.Replace('"StopGate": "Clearance"', '"StopGate": "Collision"')) 'Tampered stop gate must be rejected.' -Assert-Rejected ($fixtureContent.Replace('"HeadingRadians": 0.835253334801853', '"HeadingRadians": 0.935253334801853')) 'Tampered middle candidate heading must be rejected.' -Assert-Rejected ($fixtureContent.Replace('"WindowStartArcLengthMeters": 1.9199999999999982', '"WindowStartArcLengthMeters": 1.8199999999999982')) 'Tampered window must be rejected even when candidate endpoints are unchanged.' -Assert-Rejected ([regex]::Replace($fixtureContent, '(?m)^\s*"SegmentIndex": 0,\r?\n', '')) 'Evidence missing SegmentIndex must be rejected.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 deleted file mode 100644 index 3bf80b6..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -param( - [string]$FixturePath = (Join-Path $PSScriptRoot "..\\ParkrobTrajplanner\\PathSmoothing\\Test\\Fixtures\\path-smoothing-fixtures.json"), - [string]$EvidencePath = (Join-Path $PSScriptRoot "..\\ParkrobTrajplanner\\PathSmoothing\\Test\\Fixtures\\local-g2-diagnostic-single-turn.json") -) - -$hostProject = Join-Path $PSScriptRoot "PathSmoothingPngVerificationHost\\PathSmoothingPngVerificationHost.csproj" -$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path -$resolvedEvidencePath = (Resolve-Path -LiteralPath $EvidencePath).Path - -& dotnet run --project $hostProject --no-restore -- --verify-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 deleted file mode 100644 index d4e7c75..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -param( - [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') -) - -$ErrorActionPreference = 'Stop' -$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { - if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } -} -function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { - throw "$Message Expected=$Expected Actual=$Actual" - } -} -function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$facade = $root + 'Facade.' -$test = $root + 'Test.' -$serviceType = Get-RequiredType ($facade + 'PathSmoothingService') -$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') -$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') -$factoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory') -$service = [Activator]::CreateInstance($serviceType) -$smooth = $serviceType.GetMethod('Smooth', [Type[]]@($requestType, [Threading.CancellationToken])) -$createFixtures = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) -Assert-True ($null -ne $smooth) 'Local G2 must use the public service entrypoint.' - -$publishedStatuses = @('Complete', 'PartialImprovement', 'NotNeeded', 'Unchanged') - -function New-LocalG2Request($BaseRequest, [scriptblock]$Configure = $null) { - $configuration = [Activator]::CreateInstance($configurationType) - if ($null -ne $Configure) { & $Configure $configuration } - return [Activator]::CreateInstance($requestType, @( - $BaseRequest.CoarsePath, $BaseRequest.Segments, $BaseRequest.Map, - $BaseRequest.Vehicle, $configuration)) -} - -function Invoke-LocalG2($Request, [Threading.CancellationToken]$CancellationToken = [Threading.CancellationToken]::None) { - return $smooth.Invoke($service, @($Request, $CancellationToken)) -} - -function Assert-PublishedPath($Result, [string]$Name) { - Assert-True ($Result.Path.Count -gt 0) "$Name must publish a complete path." - Assert-Equal 0 $Result.Segments[0].StartIndex "$Name segments must start at zero." - Assert-Equal ($Result.Path.Count - 1) $Result.Segments[-1].EndIndex "$Name segments must cover the path." - Assert-True $Result.Diagnostics.Metrics.IsFeasible "$Name diagnostics must be feasible." - Assert-True ($Result.Diagnostics.Metrics.MinimumBodyClearanceMeters -ge 0.0) "$Name must remain collision-free at the configured 0 m reserve." -} - -function Assert-Equivalent($First, $Second, [string]$Name) { - Assert-Equal $First.Status $Second.Status "$Name must retain deterministic status." - Assert-Equal $First.Path.Count $Second.Path.Count "$Name must retain deterministic point count." - Assert-Equal $First.RegionReports.Count $Second.RegionReports.Count "$Name must retain deterministic report count." - for ($index = 0; $index -lt $First.Path.Count; $index++) { - Assert-Near $First.Path[$index].X $Second.Path[$index].X "$Name point $index must retain X." - Assert-Near $First.Path[$index].Y $Second.Path[$index].Y "$Name point $index must retain Y." - } - for ($index = 0; $index -lt $First.RegionReports.Count; $index++) { - Assert-Equal $First.RegionReports[$index].SelectedCandidateIndex $Second.RegionReports[$index].SelectedCandidateIndex "$Name report $index must retain candidate selection." - Assert-Equal $First.RegionReports[$index].Status $Second.RegionReports[$index].Status "$Name report $index must retain status." - } -} - -$fixtures = $createFixtures.Invoke($null, @((Resolve-Path $FixturePath).Path)) -for ($index = 0; $index -lt $fixtures.Count; $index++) { - $request = New-LocalG2Request $fixtures[$index].SmoothingRequest - $first = Invoke-LocalG2 $request - $second = Invoke-LocalG2 $request - Assert-Equivalent $first $second "Fixture $index" - $status = $first.Status.ToString() - Assert-True ($status -in @('Complete', 'PartialImprovement', 'NotNeeded', 'Unchanged', 'Failed', 'InvalidInput', 'Infeasible')) "Fixture $index must return a defined Local G2 result status." - if ($publishedStatuses -contains $status) { - Assert-PublishedPath $first "Fixture $index" - } - else { - Assert-Equal 0 $first.Path.Count "Unpublished fixture $index must not expose a path." - Assert-Equal 0 $first.Segments.Count "Unpublished fixture $index must not expose segments." - } -} - -$cancelSource = [Threading.CancellationTokenSource]::new() -try { - $cancelSource.Cancel() - $cancelled = Invoke-LocalG2 (New-LocalG2Request $fixtures[1].SmoothingRequest) $cancelSource.Token - Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancellation must propagate through Local G2.' - Assert-Equal 0 $cancelled.Path.Count 'A cancelled run must not publish a partial path.' -} -finally { - $cancelSource.Dispose() -} - -Write-Output 'Local G2 service integration checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 deleted file mode 100644 index ee829cd..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -param( - [string]$PathSmoothingRoot = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing') -) - -$ErrorActionPreference = 'Stop' - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-FileExists([string]$RelativePath) { - Assert-True (Test-Path -LiteralPath (Join-Path $PathSmoothingRoot $RelativePath) -PathType Leaf) "Missing required file: $RelativePath" -} - -function Assert-DirectoryExists([string]$RelativePath) { - Assert-True (Test-Path -LiteralPath (Join-Path $PathSmoothingRoot $RelativePath) -PathType Container) "Missing required directory: $RelativePath" -} - -function Assert-PathAbsent([string]$RelativePath) { - Assert-True (-not (Test-Path -LiteralPath (Join-Path $PathSmoothingRoot $RelativePath))) "Removed legacy path still exists: $RelativePath" -} - -Assert-DirectoryExists 'Contracts' -Assert-DirectoryExists 'Facade' -Assert-DirectoryExists 'LocalG2' -Assert-DirectoryExists 'Processing' -Assert-DirectoryExists 'Validation' -Assert-DirectoryExists 'Output' -Assert-DirectoryExists 'Output\Comparison' -Assert-DirectoryExists 'Output\Visualization' -Assert-DirectoryExists 'Test' -Assert-FileExists 'README.md' - -foreach ($legacyPath in @( - 'Algorithms', - 'Comparison', - 'Visualization', - 'Contracts\CubicBSplineOptions.cs', - 'Contracts\LocalCubicBezierOptions.cs', - 'Contracts\PiecewiseQuinticOptions.cs', - 'Output\Comparison\SmoothingMethodRanker.cs' -)) { - Assert-PathAbsent $legacyPath -} - -$methodSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PathSmoothingRoot 'Contracts\SmoothingMethod.cs') -Assert-True ($methodSource -match 'LocalG2Quintic') 'SmoothingMethod must retain LocalG2Quintic.' -foreach ($legacyMethod in @('CubicBSpline', 'LocalCubicBezier', 'PiecewiseQuintic')) { - Assert-True (-not $methodSource.Contains($legacyMethod)) "SmoothingMethod must not retain $legacyMethod." -} - -$configurationSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PathSmoothingRoot 'Contracts\PathSmoothingConfiguration.cs') -foreach ($legacyConfiguration in @('CubicBSpline', 'LocalCubicBezier', 'PiecewiseQuintic', 'SmoothingStrength', 'RetryStrengthScales', 'AllowFallbackToCoarsePath')) { - Assert-True (-not $configurationSource.Contains($legacyConfiguration)) "PathSmoothingConfiguration must not retain $legacyConfiguration." -} -Assert-True ($configurationSource.Contains('MinimumClearanceReserveMeters = 0d;')) 'The requested 0 m clearance-reserve default must remain.' - -$readme = Get-Content -Raw -Encoding UTF8 (Join-Path $PathSmoothingRoot 'README.md') -foreach ($heading in @( - '# PathSmoothing', - 'Module Overview', - 'File Structure', - 'Smoothing Data Flow', - 'Result Status and Publication Rules', - 'Coordinates and Units', - 'Minimal Call Example', - 'Detailed Usage Guide', - 'Fixture Reports and Visualization', - 'Common Errors', - 'First-Version Limits' -)) { - Assert-True ($readme.Contains($heading)) "README is missing required section: $heading" -} - -Write-Output 'PathSmoothing LocalG2-only layout checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_png.ps1 b/ClumsyPilot/tests/verify_path_smoothing_png.ps1 deleted file mode 100644 index 1ef1bd8..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_png.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param( - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') -) - -$ErrorActionPreference = 'Stop' - -$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' -if (-not (Test-Path -LiteralPath $hostProject)) { - throw "Path smoothing PNG verification host was not found: $hostProject" -} - -$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path -& dotnet run --project $hostProject --no-restore -- $resolvedFixturePath -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} - -& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'verify_path_smoothing_local_g2_diagnostic_visualization.ps1') -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -$runnerPath = Join-Path $PSScriptRoot 'run_local_g2_diagnostic_visualization.ps1' -$runnerOutput = Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-png-smoke' -& powershell -ExecutionPolicy Bypass -File $runnerPath -OutputDirectory $runnerOutput -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -$primaryPng = Join-Path $runnerOutput '05-local-g2-diagnostic-candidate.png' -if (-not (Test-Path -LiteralPath $primaryPng)) { - throw "Local G2 diagnostic runner did not publish $primaryPng" -} - -Write-Output 'Path smoothing PNG checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_validation.ps1 b/ClumsyPilot/tests/verify_path_smoothing_validation.ps1 deleted file mode 100644 index 8935482..0000000 --- a/ClumsyPilot/tests/verify_path_smoothing_validation.ps1 +++ /dev/null @@ -1,175 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) - -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Assert-True($Actual, [string]$Message) { - if (-not $Actual) { throw $Message } -} - -function Assert-False($Actual, [string]$Message) { - if ($Actual) { throw $Message } -} - -function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { - if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { - throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" - } -} - -function Get-RequiredType([string]$Name) { - return $assembly.GetType($Name, $true) -} - -function New-Map([bool]$WithObstacle) { - $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]4000, [single]0, [single]4000)) - $request = [Activator]::CreateInstance($mapRequestType) - $request.Bounds = $bounds - $request.ResolutionMm = [single]50 - if ($WithObstacle) { - $obstacle = [Activator]::CreateInstance($rectangleType, @([single]1900, [single]2100, [single]1800, [single]2200)) - $obstacles = [Array]::CreateInstance($obstacleType, 1) - $obstacles.SetValue($obstacle, 0) - $source = [Activator]::CreateInstance($manualSourceType, @('validator-obstacle', [long]1, $true, $obstacles)) - $sources = [Array]::CreateInstance($obstacleSourceType, 1) - $sources.SetValue($source, 0) - $request.ObstacleSources = $sources - } else { - $request.ObstacleSources = [Array]::CreateInstance($obstacleSourceType, 0) - $request.AllowExplicitEmptyMap = $true - } - $result = [Activator]::CreateInstance($mapFactoryType).Create($request) - Assert-True $result.Succeeded 'Validation test map must be built.' - Assert-True $result.Map.PlanningReady 'Validation test map must be ready.' - return $result.Map -} - -function New-SmoothedPoint([double]$X, [double]$Y, [double]$ArcLength, $Direction, - [double]$VehicleCurvature = 0.0, [bool]$IsGearSwitch = $false, [double]$Clearance = 999.0, - [double]$VehicleCurvatureDerivative = 0.0) { - return [Activator]::CreateInstance($smoothedPointType, @( - $X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction, - $VehicleCurvature, $VehicleCurvature, $VehicleCurvatureDerivative, $Clearance, $IsGearSwitch, $anchor)) -} - -function New-PreparedPoint([double]$X, [double]$Y, [double]$ArcLength, [bool]$IsGearSwitch = $false) { - return [Activator]::CreateInstance($smoothingPointType, @( - $X, $Y, $ArcLength, [double]0.0, [double]0.0, [double]999.0, $IsGearSwitch, $anchor)) -} - -function New-OneSegmentCase([double]$X0, [double]$Y0, [double]$X1, [double]$Y1, [double]$VehicleCurvature = 0.0, - [double]$StartArcLength = 0.0, [double]$VehicleCurvatureDerivative = 0.0) { - $candidatePath = [Array]::CreateInstance($smoothedPointType, 2) - $candidatePath.SetValue((New-SmoothedPoint $X0 $Y0 $StartArcLength $forward 0.0 $false 999.0 $VehicleCurvatureDerivative), 0) - $candidatePath.SetValue((New-SmoothedPoint $X1 $Y1 ($StartArcLength + 1.0) $forward $VehicleCurvature $false 999.0 $VehicleCurvatureDerivative), 1) - $candidateSegments = [Array]::CreateInstance($smoothedSegmentType, 1) - $candidateSegments.SetValue([Activator]::CreateInstance($smoothedSegmentType, @(0, $forward, 0, 1, $false, $false)), 0) - $preparedPoints = [Array]::CreateInstance($smoothingPointType, 2) - $preparedPoints.SetValue((New-PreparedPoint $X0 $Y0 0.0), 0) - $preparedPoints.SetValue((New-PreparedPoint $X1 $Y1 1.0), 1) - $preparedSegments = [Array]::CreateInstance($preparedSegmentType, 1) - $preparedSegments.SetValue([Activator]::CreateInstance($preparedSegmentType, @(0, $forward, $preparedPoints, $false, $false)), 0) - return [PSCustomObject]@{ - CandidatePath = $candidatePath - CandidateSegments = $candidateSegments - Original = [Activator]::CreateInstance($preparedPathType, [object[]]@(,$preparedSegments)) - } -} - -function Invoke-Validation($Case, $Map) { - $arguments = [object[]]@($Case.CandidatePath, $Case.CandidateSegments, $Case.Original, $Map, $vehicle, - [double]0.05, $null, [double]0.0, $null) - $accepted = $validateMethod.Invoke($validator, $arguments) - return [PSCustomObject]@{ Accepted = $accepted; Path = $arguments[6]; MinimumClearance = $arguments[7]; Reason = $arguments[8] } -} - -function New-MovedGearSwitchCase { - $candidatePath = [Array]::CreateInstance($smoothedPointType, 4) - $candidatePath.SetValue((New-SmoothedPoint 0.5 0.5 0.0 $forward), 0) - $candidatePath.SetValue((New-SmoothedPoint 1.5 0.5 1.0 $forward), 1) - $candidatePath.SetValue((New-SmoothedPoint 1.6 0.5 1.0 $reverse 0.0 $true), 2) - $candidatePath.SetValue((New-SmoothedPoint 0.5 0.5 2.0 $reverse), 3) - $candidateSegments = [Array]::CreateInstance($smoothedSegmentType, 2) - $candidateSegments.SetValue([Activator]::CreateInstance($smoothedSegmentType, @(0, $forward, 0, 1, $false, $true)), 0) - $candidateSegments.SetValue([Activator]::CreateInstance($smoothedSegmentType, @(1, $reverse, 2, 3, $true, $false)), 1) - $preparedFirst = [Array]::CreateInstance($smoothingPointType, 2) - $preparedFirst.SetValue((New-PreparedPoint 0.5 0.5 0.0), 0) - $preparedFirst.SetValue((New-PreparedPoint 1.5 0.5 1.0), 1) - $preparedSecond = [Array]::CreateInstance($smoothingPointType, 2) - $preparedSecond.SetValue((New-PreparedPoint 1.5 0.5 0.0 $true), 0) - $preparedSecond.SetValue((New-PreparedPoint 0.5 0.5 1.0), 1) - $preparedSegments = [Array]::CreateInstance($preparedSegmentType, 2) - $preparedSegments.SetValue([Activator]::CreateInstance($preparedSegmentType, @(0, $forward, $preparedFirst, $false, $true)), 0) - $preparedSegments.SetValue([Activator]::CreateInstance($preparedSegmentType, @(1, $reverse, $preparedSecond, $true, $false)), 1) - return [PSCustomObject]@{ - CandidatePath = $candidatePath - CandidateSegments = $candidateSegments - Original = [Activator]::CreateInstance($preparedPathType, [object[]]@(,$preparedSegments)) - } -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$processing = $root + 'Processing.' -$validation = $root + 'Validation.' -$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' -$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' - -$validatorType = Get-RequiredType ($validation + 'SmoothedPathValidator') -$smoothedPointType = Get-RequiredType ($root + 'SmoothedPathPoint') -$smoothedSegmentType = Get-RequiredType ($root + 'SmoothedPathSegment') -$smoothingPointType = Get-RequiredType ($processing + 'SmoothingPoint2D') -$preparedSegmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') -$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') -$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') -$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') -$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') -$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm') -$obstacleType = Get-RequiredType ($mapping + 'IMapObstacle') -$rectangleType = Get-RequiredType ($mapping + 'AxisAlignedRectangleObstacle') -$obstacleSourceType = Get-RequiredType ($mapping + 'IMapObstacleSource') -$manualSourceType = Get-RequiredType ($mapping + 'ManualObstacleSource') -$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest') -$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory') - -$validator = [Activator]::CreateInstance($validatorType) -$validateMethod = $validatorType.GetMethod('TryValidate') -Assert-True ($null -ne $validateMethod) 'SmoothedPathValidator must expose TryValidate.' -Assert-True ($validateMethod.GetParameters().Length -eq 9) 'TryValidate must accept path, segments, originals, map, vehicle, step, path, clearance, and reason.' -$forward = [Enum]::Parse($directionType, 'Forward') -$reverse = [Enum]::Parse($directionType, 'Reverse') -$anchor = [Enum]::Parse($sourceType, 'Anchor') -$vehicle = [Activator]::CreateInstance($vehicleType) -$vehicle.LengthMeters = 0.20 -$vehicle.WidthMeters = 0.20 -$vehicle.SafetyMarginMeters = 0.0 -$vehicle.MaximumCurvaturePerMeter = 1.0 -$vehicle.MinimumTurningRadiusMeters = 1.0 - -$emptyMap = New-Map $false -$obstacleMap = New-Map $true -$valid = Invoke-Validation (New-OneSegmentCase 0.5 0.5 1.5 0.5) $obstacleMap -Assert-True $valid.Accepted ('A valid straight candidate must pass. Reason=' + $valid.Reason) -Assert-True ($null -ne $valid.Path) 'A valid candidate must return clearance-recomputed points.' -Assert-True ($valid.Path[0].BodyClearance -lt 999.0) 'Validated output must replace an overclaimed candidate clearance.' -Assert-True ($valid.MinimumClearance -ge 0.0) 'A valid candidate must report non-negative conservative clearance.' -$derivativePreserved = Invoke-Validation (New-OneSegmentCase 0.5 0.5 1.5 0.5 0.0 0.0 0.25) $obstacleMap -Assert-True $derivativePreserved.Accepted ('A valid derivative-bearing candidate must pass. Reason=' + $derivativePreserved.Reason) -Assert-Near 0.25 $derivativePreserved.Path[0].VehicleCurvatureDerivative 0.000000001 ` - 'Clearance recomputation must preserve the first point curvature derivative.' -Assert-Near 0.25 $derivativePreserved.Path[1].VehicleCurvatureDerivative 0.000000001 ` - 'Clearance recomputation must preserve the final point curvature derivative.' - -$pointCollision = Invoke-Validation (New-OneSegmentCase 2.0 2.0 2.5 2.0) $obstacleMap -Assert-False $pointCollision.Accepted 'A smoothing candidate that touches an obstacle must be rejected.' -$sweptCollision = Invoke-Validation (New-OneSegmentCase 1.5 2.0 2.5 2.0) $obstacleMap -Assert-False $sweptCollision.Accepted 'A smoothing candidate whose sweep cuts through an obstacle must be rejected.' -$outsideBounds = Invoke-Validation (New-OneSegmentCase 0.05 0.5 0.5 0.5) $emptyMap -Assert-False $outsideBounds.Accepted 'A smoothing candidate outside map bounds must be rejected.' -$overCurvature = Invoke-Validation (New-OneSegmentCase 0.5 0.5 1.5 0.5 2.0) $emptyMap -Assert-False $overCurvature.Accepted 'A smoothing candidate above vehicle maximum curvature must be rejected.' -$nonzeroStartArc = Invoke-Validation (New-OneSegmentCase 0.5 0.5 1.5 0.5 0.0 1.0) $emptyMap -Assert-False $nonzeroStartArc.Accepted 'A smoothing candidate must start at global arc length zero.' -$movedSwitch = Invoke-Validation (New-MovedGearSwitchCase) $emptyMap -Assert-False $movedSwitch.Accepted 'A smoothing candidate with a moved gear-switch pose must be rejected.' - -Write-Output 'Path smoothing validation checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_adapter.ps1 b/ClumsyPilot/tests/verify_planning_map_adapter.ps1 deleted file mode 100644 index 7e97700..0000000 --- a/ClumsyPilot/tests/verify_planning_map_adapter.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } -$boundsType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true) -$gridType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.EnvironmentGridMap', $true) -$circleType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.CircleObstacle', $true) -$rectangleType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.AxisAlignedRectangleObstacle', $true) -$rasterizer = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapObstacleRasterizer', $true) -$adapter = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapAdapter', $true) -$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]105, [single]0, [single]105)) -$grid = [Activator]::CreateInstance($gridType, @($bounds, [single]20)) -Assert-Equal $true ($grid.IsWorldInBounds([single]104.9, [single]104.9)) 'Last partial cell must be inside.' -Assert-Equal $false ($grid.IsWorldInBounds([single]105, [single]50)) 'XMax must be exclusive.' -Assert-Equal $true ($grid.IsOccupiedWorld([single]-0.1, [single]20)) 'Outside world must be occupied.' -$circle = [Activator]::CreateInstance($circleType, @([single]40, [single]40, [single]0)) -$rasterizer.GetMethod('Rasterize').Invoke($null, @($grid, $circle)) | Out-Null -Assert-True ($grid.OccupiedCount -ge 4) 'Circle touching a grid intersection must conservatively occupy adjacent cells.' -$rectangle = [Activator]::CreateInstance($rectangleType, @([single]80, [single]100, [single]80, [single]100)) -$rasterizer.GetMethod('Rasterize').Invoke($null, @($grid, $rectangle)) | Out-Null -Assert-True ($grid.IsOccupiedWorld([single]90, [single]90)) 'Rectangle must occupy its intersecting cells.' -$outside = [Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]10)) -$before = $grid.OccupiedCount -$rasterizer.GetMethod('Rasterize').Invoke($null, @($grid, $outside)) | Out-Null -Assert-Equal $before $grid.OccupiedCount 'Outside obstacle must not mark cells.' -$planning = $adapter.GetMethod('Create').Invoke($null, @($grid)) -Assert-True ($planning.IsOccupiedWorld(0.04, 0.04)) 'Planning map must use metre world coordinates.' -Assert-Equal 0.0 ($planning.GetConservativeObstacleDistanceMeters(0.04, 0.04)) 'Occupied cell clearance must be zero.' -Assert-Equal 0.0 ($planning.GetConservativeObstacleDistanceMeters(-1.0, 0.0)) 'Outside map clearance must be zero.' -$singleBounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) -$singleGrid = [Activator]::CreateInstance($gridType, @($singleBounds, [single]50)) -$singleCircle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0)) -$rasterizer.GetMethod('Rasterize').Invoke($null, @($singleGrid, $singleCircle)) | Out-Null -Assert-Equal 1 $singleGrid.OccupiedCount 'Zero radius circle at a cell center must occupy exactly one cell.' -$singlePlanning = $adapter.GetMethod('Create').Invoke($null, @($singleGrid)) -$singleDistance = $singlePlanning.GetConservativeObstacleDistanceMeters(1.275, 1.275) -$singleNearestCellCenterDistance = [Math]::Sqrt(0.25 * 0.25 + 0.25 * 0.25) -Assert-True ((-not [double]::IsNaN($singleDistance)) -and (-not [double]::IsInfinity($singleDistance))) 'Single occupied cell clearance must be finite.' -Assert-True ($singleDistance -le $singleNearestCellCenterDistance) 'Single occupied cell clearance must not exceed nearest occupied cell geometry distance.' -$emptyGrid = [Activator]::CreateInstance($gridType, @($bounds, [single]20)) -$emptyPlanning = $adapter.GetMethod('Create').Invoke($null, @($emptyGrid)) -Assert-True ([double]::IsPositiveInfinity($emptyPlanning.GetConservativeObstacleDistanceMeters(0.01, 0.01))) 'Empty map clearance must be positive infinity.' -$invalidResolutionRejected = $false -try { [Activator]::CreateInstance($gridType, @($bounds, [single]10)) | Out-Null } catch { $invalidResolutionRejected = $true } -Assert-True $invalidResolutionRejected 'Resolution below 20 mm must fail.' -Write-Output 'Planning map adapter checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_documentation.ps1 b/ClumsyPilot/tests/verify_planning_map_documentation.ps1 deleted file mode 100644 index 48156a5..0000000 --- a/ClumsyPilot/tests/verify_planning_map_documentation.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -param([string]$MapRoot = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map')) - -$ErrorActionPreference = 'Stop' - -function Assert-Contains([string]$Content, [string]$Expected, [string]$Message) { - if (-not $Content.Contains($Expected)) { throw "$Message Missing=$Expected" } -} - -function Assert-DocumentationCount([string]$RelativePath, [int]$MinimumCount) { - $path = Join-Path $MapRoot $RelativePath - $content = Get-Content -LiteralPath $path -Raw - $count = [regex]::Matches($content, '/// ').Count - if ($count -lt $MinimumCount) { throw "Insufficient public API documentation in $RelativePath ExpectedAtLeast=$MinimumCount Actual=$count" } -} - -$readmePath = Join-Path $MapRoot 'README.md' -if (-not (Test-Path -LiteralPath $readmePath)) { throw "Map README is required: $readmePath" } -$readme = Get-Content -LiteralPath $readmePath -Raw -Assert-Contains $readme 'PlanningMapFactory' 'README must identify the public creation facade.' -Assert-Contains $readme 'IMapObstacleSource' 'README must explain unified obstacle sources.' -Assert-Contains $readme 'PlanningGridMap' 'README must explain the planning snapshot output.' -Assert-Contains $readme 'SourceVersion' 'README must explain cache invalidation versions.' -Assert-Contains $readme 'PlanningMapBuildStatus' 'README must explain explicit map build termination states.' -Assert-Contains $readme 'PNG' 'README must explain debug image output.' -Assert-Contains $readme 'TrapMap' 'README must describe the legacy-map boundary.' - -Assert-DocumentationCount 'PlanningMapFactory.cs' 2 -Assert-DocumentationCount 'PlanningMapRequest.cs' 5 -Assert-DocumentationCount 'PlanningMapBuildResult.cs' 9 -Assert-DocumentationCount 'Core\MapBuildRequest.cs' 4 -Assert-DocumentationCount 'Core\EnvironmentMapBuildResult.cs' 7 -Assert-DocumentationCount 'Core\MapBoundsMm.cs' 12 -Assert-DocumentationCount 'Core\EnvironmentGridMap.cs' 13 -Assert-DocumentationCount 'Core\EnvironmentMapBuilder.cs' 2 -Assert-DocumentationCount 'Obstacles\IMapObstacle.cs' 2 -Assert-DocumentationCount 'Obstacles\CircleObstacle.cs' 6 -Assert-DocumentationCount 'Obstacles\AxisAlignedRectangleObstacle.cs' 7 -Assert-DocumentationCount 'Obstacles\MapObstacleRasterizer.cs' 2 -Assert-DocumentationCount 'Sources\IMapObstacleSource.cs' 5 -Assert-DocumentationCount 'Sources\ManualObstacleSource.cs' 6 -Assert-DocumentationCount 'Sources\TwoLegProjectionInput.cs' 13 -Assert-DocumentationCount 'Sources\TwoLegObstacleSource.cs' 6 -Assert-DocumentationCount 'Sources\TwoLegObstacleProjector.cs' 2 -Assert-DocumentationCount 'Sources\ObstacleProjectionResult.cs' 8 -Assert-DocumentationCount 'Sources\ObstacleSourceStatus.cs' 5 -Assert-DocumentationCount 'Planning\PlanningGridMap.cs' 15 -Assert-DocumentationCount 'Planning\PlanningMapAdapter.cs' 2 -Assert-DocumentationCount 'Planning\ObstacleDistanceField.cs' 2 -Assert-DocumentationCount 'Planning\EuclideanDistanceTransform.cs' 2 -Assert-DocumentationCount 'Planning\PlanningMapCache.cs' 5 -Assert-DocumentationCount 'Test\MovementTest.MapTest.cs' 3 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportRequest.cs' 3 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportResult.cs' 8 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExporter.cs' 6 -Assert-DocumentationCount 'Test\Visualization\PlanningMapImageRenderer.cs' 2 -Assert-DocumentationCount 'Test\Visualization\ValidatedPngWriter.cs' 2 - -Write-Output 'Planning map documentation checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_factory.ps1 b/ClumsyPilot/tests/verify_planning_map_factory.ps1 deleted file mode 100644 index 2f3c763..0000000 --- a/ClumsyPilot/tests/verify_planning_map_factory.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } -function Assert-False($Actual, [string]$Message) { if ($Actual) { throw $Message } } -function Assert-Null($Actual, [string]$Message) { if ($null -ne $Actual) { throw $Message } } -function Find-NonPublicInstanceMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { - foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Instance,NonPublic')) { - if ($candidate.Name -ne $Name) { continue } - $parameters = $candidate.GetParameters() - if ($parameters.Length -ne $ParameterTypes.Length) { continue } - $matches = $true - for ($index = 0; $index -lt $parameters.Length; $index++) { - if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } - } - if ($matches) { return $candidate } - } - return $null -} -$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' -$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) -$obstacleType = $assembly.GetType($ns + 'IMapObstacle', $true) -$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) -$circleType = $assembly.GetType($ns + 'CircleObstacle', $true) -$manualType = $assembly.GetType($ns + 'ManualObstacleSource', $true) -$twoLegInputType = $assembly.GetType($ns + 'TwoLegProjectionInput', $true) -$twoLegType = $assembly.GetType($ns + 'TwoLegObstacleSource', $true) -$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) -$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) -$mapResultType = $assembly.GetType($ns + 'PlanningMapBuildResult', $true) -$mapBuildStatusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) -$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $false) -$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $false) -Assert-True ($operationBudgetType -ne $null) 'PlanningOperationBudget must exist for shared planning cancellation and timeout handling.' -Assert-True ($operationStopReasonType -ne $null) 'PlanningOperationStopReason must exist for shared planning cancellation and timeout handling.' -Assert-True $operationStopReasonType.IsEnum 'PlanningOperationStopReason must be an enum.' -foreach ($expectedStopReason in @('None', 'Cancelled', 'TimedOut')) { - Assert-True ($operationStopReasonType.GetEnumNames() -contains $expectedStopReason) "PlanningOperationStopReason must contain $expectedStopReason." -} -Assert-True ($mapResultType.GetProperty('Status') -ne $null) 'PlanningMapBuildResult must expose an explicit build status.' -foreach ($expectedMapStatus in @('Success', 'Failed', 'Cancelled', 'TimedOut')) { - Assert-True ($mapBuildStatusType.GetEnumNames() -contains $expectedMapStatus) "PlanningMapBuildStatus must contain $expectedMapStatus." -} -$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]2000, [single]0, [single]2000)) -$obstacles = [Array]::CreateInstance($obstacleType, 1) -$obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]100)), 0) -$manual = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) -$twoLegInput = [Activator]::CreateInstance($twoLegInputType, @($true, [single]1000, [single]1000, [double]([Math]::PI / 2), [single]100, [single]0, [single]-100, [single]0, [single]40, 'test snapshot')) -$twoLeg = [Activator]::CreateInstance($twoLegType, @('two-leg', [long]1, $false, $twoLegInput)) -$sources = [Array]::CreateInstance($sourceType, 2); $sources.SetValue($twoLeg, 0); $sources.SetValue($manual, 1) -function New-Request($sourceArray, [bool]$allowEmpty) { $request = [Activator]::CreateInstance($requestType); $request.Bounds = $bounds; $request.ResolutionMm = [single]50; $request.ObstacleSources = $sourceArray; $request.AllowExplicitEmptyMap = $allowEmpty; return $request } -$factory = [Activator]::CreateInstance($factoryType) -$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, - @([Threading.CancellationToken], [TimeSpan]), $null) -Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' -$createWithBudget = Find-NonPublicInstanceMethod $factoryType 'Create' @($requestType, $operationBudgetType) -Assert-True ($createWithBudget -ne $null) 'PlanningMapFactory must expose an internal budget-aware Create overload.' -$cancelledSource = New-Object Threading.CancellationTokenSource -$cancelledSource.Cancel() -$cancelledBudget = $budgetConstructor.Invoke(@($cancelledSource.Token, [TimeSpan]::FromSeconds(1))) -$cancelled = $createWithBudget.Invoke($factory, @((New-Request $sources $false), $cancelledBudget)) -Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancelled map construction must retain the cancellation status.' -Assert-False $cancelled.Succeeded 'Cancelled map construction must not succeed.' -Assert-Null $cancelled.Map 'Cancelled map construction must not publish a map.' -Assert-Equal 'None' $cancelled.CacheHit.ToString() 'Cancelled map construction must not publish a cache hit.' -$first = $factory.Create((New-Request $sources $false)) -Assert-True $first.Succeeded 'Mixed source request must build.' -Assert-True $first.Map.PlanningReady 'Applied source geometry must make the map ready.' -Assert-True ($first.Map.IsOccupiedWorld(1.0, 1.1)) 'TwoLeg must project detection-time local coordinates into world metres.' -$second = $factory.Create((New-Request $sources $false)) -Assert-Equal 'Input' $second.CacheHit.ToString() 'Same snapshot must hit complete input cache.' -Assert-True ([object]::ReferenceEquals($first.Map, $second.Map)) 'Same snapshot must return the exact immutable map object.' -$manualVersionTwo = [Activator]::CreateInstance($manualType, @('manual', [long]2, $true, $obstacles)) -$sourcesVersionTwo = [Array]::CreateInstance($sourceType, 2); $sourcesVersionTwo.SetValue($manualVersionTwo, 0); $sourcesVersionTwo.SetValue($twoLeg, 1) -$third = $factory.Create((New-Request $sourcesVersionTwo $false)) -Assert-Equal 'Occupancy' $third.CacheHit.ToString() 'Version change with equal occupancy must reuse occupancy buffers.' -Assert-True ($third.Map.SnapshotId -gt $first.Map.SnapshotId) 'Occupancy reuse must still issue a fresh snapshot id.' -$emptySources = [Array]::CreateInstance($sourceType, 0) -$emptyBlocked = $factory.Create((New-Request $emptySources $false)) -Assert-True (-not $emptyBlocked.Map.PlanningReady) 'Implicit empty map must be blocked.' -$emptyAllowed = $factory.Create((New-Request $emptySources $true)) -Assert-True $emptyAllowed.Map.PlanningReady 'Explicit empty map must be accepted.' -$invalidManual = [Activator]::CreateInstance($manualType, @('invalid', [long]0, $true, $null)) -$invalidSources = [Array]::CreateInstance($sourceType, 1); $invalidSources.SetValue($invalidManual, 0) -$invalid = $factory.Create((New-Request $invalidSources $false)) -Assert-True (-not $invalid.Succeeded) 'Required invalid source must reject the full build.' -Write-Output 'Planning map factory checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_image.ps1 b/ClumsyPilot/tests/verify_planning_map_image.ps1 deleted file mode 100644 index b674c57..0000000 --- a/ClumsyPilot/tests/verify_planning_map_image.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) -$ErrorActionPreference = 'Stop' -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) -function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } -$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' -$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) -$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) -$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) -$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) -$imageRequestType = $assembly.GetType($ns + 'PlanningMapImageExportRequest', $true) -$exporterType = $assembly.GetType($ns + 'PlanningMapImageExporter', $true) -$mapRequest = [Activator]::CreateInstance($requestType) -$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]1000, [single]0, [single]1000)) -$mapRequest.ResolutionMm = [single]50 -$mapRequest.ObstacleSources = [Array]::CreateInstance($sourceType, 0) -$mapRequest.AllowExplicitEmptyMap = $true -$map = [Activator]::CreateInstance($factoryType).Create($mapRequest).Map -$imageRequest = [Activator]::CreateInstance($imageRequestType) -$imageRequest.Map = $map -$imageRequest.OutputRootDirectory = (Join-Path $PSScriptRoot '..\obj\planning_map_image_test') -$export = $exporterType.GetMethod('ExportIfEnabled').Invoke($null, @($true, $imageRequest)) -Assert-True $export.Saved ('Image export failed: ' + $export.Message) -Assert-True (Test-Path -LiteralPath $export.FilePath -PathType Leaf) 'Image output is missing.' -$png = [IO.File]::ReadAllBytes($export.FilePath) -Assert-True ($png.Length -gt 45) 'PNG is too small.' -Assert-True ($png[0] -eq 137 -and $png[1] -eq 80 -and $png[2] -eq 78 -and $png[3] -eq 71) 'PNG signature is invalid.' -Assert-True ([Text.Encoding]::ASCII.GetString($png, 12, 4) -eq 'IHDR') 'PNG must begin with IHDR.' -Assert-True ([Text.Encoding]::ASCII.GetString($png, $png.Length - 8, 4) -eq 'IEND') 'PNG must end with IEND.' -$source = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\Visualization\PlanningMapImageExporter.cs') -Raw -Encoding UTF8 -Assert-True ($source -notmatch 'GridMapData|TrapMapVehiclePose|TwoLegDetect') 'New exporter must consume only PlanningGridMap.' -Write-Output 'Planning map image checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_test_config.ps1 b/ClumsyPilot/tests/verify_planning_map_test_config.ps1 deleted file mode 100644 index ad52397..0000000 --- a/ClumsyPilot/tests/verify_planning_map_test_config.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -param([string]$SourcePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\MovementTest.MapTest.cs')) - -$ErrorActionPreference = 'Stop' -$source = Get-Content -LiteralPath $SourcePath -Raw - -function Assert-Contains([string]$Expected, [string]$Message) { - if (-not $source.Contains($Expected)) { throw "$Message Missing=$Expected" } -} - -Assert-Contains 'EnableTerminalDebugLog' 'MapTest must expose a terminal logging switch.' -Assert-Contains 'SavePng' 'MapTest must expose a PNG export switch.' -$begin = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5F00, 0x59CB) -$sources = -join [char[]](0x969C, 0x788D, 0x7269, 0x6765) -$projection = -join [char[]](0x6295, 0x5F71, 0x7ED3, 0x679C) -$snapshot = -join [char[]](0x5730, 0x56FE, 0x5FEB, 0x7167) -$end = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5B8C, 0x6210) -Assert-Contains 'FormatSourceStatus' 'MapTest must translate projection status for terminal output.' -Assert-Contains 'FormatCacheHit' 'MapTest must translate cache hits for terminal output.' -Assert-Contains 'FormatImageResult' 'MapTest must translate image export results for terminal output.' -if ($source.Contains('PLANNING_MAP_CREATE_BEGIN')) { throw 'MapTest must not expose English terminal log markers.' } -Assert-Contains 'PlanningMapFactory' 'MapTest must continue using the public map facade.' - -Write-Output 'Planning map MovementTest configuration checks passed.' diff --git a/ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1 b/ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1 deleted file mode 100644 index ba3a6b8..0000000 --- a/ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$page = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Trajplanner_output\trajectory-planning-flow-demo.html' -if (-not (Test-Path -LiteralPath $page)) -{ - throw "Missing demo page: $page" -} - -$html = Get-Content -LiteralPath $page -Raw -Encoding UTF8 -$required = @( - '', - 'data-stage="map"', - 'data-stage="coarse"', - 'data-stage="smooth"', - 'data-stage="em"', - 'data-stage="control"', - 'id="planning-map"', - 'id="timeline"', - 'coarse-path', - 'smooth-path', - 'EmTrajectory', - 'TrajectoryControlCommand', - 'mapSnapshotId', - 'referencePathId', - 'SignedLongitudinalVelocity', - 'RequestDirectionChange', - 'HoldBrake', - 'IsTrajectoryComplete', - 'function render()', - 'function controlCommandForPoint(', - 'addEventListener' -) - -foreach ($token in $required) -{ - if (-not $html.Contains($token)) - { - throw "Missing required demo contract: $token" - } -} - -foreach ($forbidden in @('http://', 'https://', 'fetch(', 'XMLHttpRequest', 'WebSocket')) -{ - if ($html.Contains($forbidden)) - { - throw "Demo must remain offline: $forbidden" - } -} - -$readme = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Trajplanner_output\README.md') -Raw -Encoding UTF8 -if (-not $readme.Contains('trajectory-planning-flow-demo.html')) -{ - throw 'README must link to the trajectory-planning-flow demo.' -} - -Write-Output 'Trajectory planning flow demo checks passed.' diff --git a/ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs b/Shared/Chassis/MultiWheelChassisAdapter.cs similarity index 100% rename from ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs rename to Shared/Chassis/MultiWheelChassisAdapter.cs diff --git a/Shared/Fleet/FleetKinematics.cs b/Shared/Fleet/FleetKinematics.cs new file mode 100644 index 0000000..46b6899 --- /dev/null +++ b/Shared/Fleet/FleetKinematics.cs @@ -0,0 +1 @@ +// 把车队整体速度分解为每辆车的局部速度 diff --git a/ClumsyPilot/Shared/Mathematics/AngleMath.cs b/Shared/Mathematics/AngleMath.cs similarity index 100% rename from ClumsyPilot/Shared/Mathematics/AngleMath.cs rename to Shared/Mathematics/AngleMath.cs diff --git a/ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs b/Shared/Mathematics/FrameTransform2D.cs similarity index 100% rename from ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs rename to Shared/Mathematics/FrameTransform2D.cs diff --git a/ClumsyPilot/Shared/Mathematics/InterpolationMath.cs b/Shared/Mathematics/InterpolationMath.cs similarity index 100% rename from ClumsyPilot/Shared/Mathematics/InterpolationMath.cs rename to Shared/Mathematics/InterpolationMath.cs diff --git a/ClumsyPilot/Shared/Models/ChassisCommand.cs b/Shared/Models/ChassisCommand.cs similarity index 99% rename from ClumsyPilot/Shared/Models/ChassisCommand.cs rename to Shared/Models/ChassisCommand.cs index 73545dd..5f073fd 100644 --- a/ClumsyPilot/Shared/Models/ChassisCommand.cs +++ b/Shared/Models/ChassisCommand.cs @@ -180,4 +180,4 @@ namespace MyParking.Shared } -} +} \ No newline at end of file diff --git a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-final.md b/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-final.md deleted file mode 100644 index 4e5c484..0000000 --- a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-final.md +++ /dev/null @@ -1,95 +0,0 @@ -# EM Longitudinal Rolling Planning — Final Acceptance Handoff - -## Scope and commits - -- Phase 4 start baseline: `6cfbaf6` (`feat: reuse prior trajectory in longitudinal planning`) -- Task 8: `eb050b1` (`test: cover rolling-to-stop EM planning flow`) -- Task 9: `302798b` (`docs: explain rolling longitudinal planning diagnostics`) -- Phase 4 code/documentation end: `302798b` - -Only original-plan Tasks 8, 9, and 10 were executed. No fifth phase is defined or created. - -## Delivered behavior - -### Task 8 — service-level rolling-to-stop regressions - -- Added service coverage for `RollingContinuation -> ApproachStopBoundary -> ExactStopAtBoundary`. -- The rolling case uses `DistanceHorizonMeters=5m`, `TimeHorizonSeconds=2s`, `OutputTimeStepSeconds=0.1s`, and - `MaximumForwardSpeedMetersPerSecond=0.2m/s`; it publishes 21 points, stops short of 5m, and retains nonzero terminal - speed. -- Goal and `CreateGearPairReferencePath()` exact-stop cases verify the true boundary anchor, following QP stationary - point, zero speed, and the absence of points from the next direction segment. -- The parameterized gear-pair fixture retains its existing 2m default and supports a short first segment for exact-stop - service tests. -- The existing rolling integration regression now explicitly proves the 21-knot / 5m-LS / 2s-ST semantics. - -### Task 9 — observation diagnostics and documentation - -- The one-time configuration diagnostic reports the worst forward/reverse jerk-limited stopping capability: - `maximumJerkLimitedStopDistance`, `maximumJerkLimitedStopDuration`, and `requiredDistanceHorizon`. -- Published trajectory summaries now report `longitudinalMode`, `terminalSpeed`, and `terminalAcceleration`. -- Failure summaries preserve every upstream value and explicitly mark any missing longitudinal context as `unavailable`; - the required fields are `longitudinalMode`, `remainingToBoundary`, `minimumStoppingDistance`, - `minimumStoppingDuration`, and `maximumStoppedReachableDistance`. -- EMPlanner and MovementTest documentation now distinguish the L-S distance window from the S-T duration and state that - rolling observation remains `OBSERVE_ONLY`. - -## TDD and debugging record - -Task 8 began with new service assertions and an `em-core-all` red run. The first failure was an undersized ±3m fixture -map for a 10m rolling path. Subsequent red runs established that a scripted solver warm start is not a validated exact -stop solution. The final fixture uses the pre-existing, discretely feasible exact-stop service scenario while the -separate 5m/2s case covers rolling semantics. No acceleration, jerk, solver residual, or publication-validation limit -was changed. - -Task 9 began with new required-field assertions. `trajectory-observation` failed because the configuration diagnostic -did not report jerk-limited stopping capability; the minimal implementation then made that test pass. - -## Verification evidence - -All commands below were run from the repository root. - -| Command | Exit | Key output | -| --- | ---: | --- | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all` (Task 8 red) | 1 | New fixture exposed map-boundary and unvalidated exact-stop warm-start failures. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration` | 0 | `PASS longitudinal-integration` after reverting an unsuccessful seed experiment. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all` (Task 8 green) | 0 | `PASS longitudinal-model`, `PASS longitudinal-integration`, `PASS trajectory`, `PASS em-planning-service`. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation` (Task 9 red) | 1 | Missing `maximumJerkLimitedStopDistance=` assertion. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation` (Task 9 green) | 0 | `PASS trajectory-observation`. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all` | 0 | All four core PASS lines. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator` | 0 | `PASS coordinator`. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor` | 0 | `PASS executor`. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation` | 0 | `PASS trajectory-observation`. | -| `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true` | 0 | Build succeeded with 0 warnings and 0 errors. | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-real-osqp-probe` | 0 | `PASS longitudinal-real-osqp`. | -| `git diff --check` | 0 | No whitespace errors; only CRLF notices from unrelated user edits. | -| `git status --short` / `git log --oneline -12` | 0 | Task commits are present; unrelated user work remains unstaged. | - -The automated C# runs emitted the pre-existing obsolete-API warnings in `MovementTests.TireFollowing.cs` and -`TireFollowing.cs`; the Task 10 build with `ExcludeLegacyAutoAvoidance=true` itself reported 0 warnings. - -## OSQP and observation-only target verification - -The real OSQP probe succeeded on this Windows workspace host, so the native DLL was loadable for the probe. The manual -`TrajectoryObservationMovementTest` observation-only run remains pending on the deployed vehicle/MDCS host: it is a -vehicle-UI MovementTest that requires live localization and chassis-speed read interfaces and has no standalone -non-interactive launcher in this workspace. It must confirm the startup configuration prints once, the -`RollingContinuation -> ApproachStopBoundary -> ExactStopAtBoundary` progression, no point-21 -`JerkLimitExceeded`, and continuing `OBSERVE_ONLY: no chassis command is sent.` output. - -## Preserved user worktree and safety constraints - -The branch remains `trajplanner`. Unrelated modified, deleted, and untracked user work in PathSmoothing, CoarsePath, -Map, reports, configuration, MovementTest, and planning/spec documents remains untouched and unstaged. No `reset`, -`checkout`, `clean`, `stash`, broad `git add`, or worktree cleanup was used. - -No chassis command, trajectory follower, or watchdog was added. `OBSERVE_ONLY` remains unchanged. - -## Retained invariants - -- `DistanceHorizonMeters` remains the L-S reference window; `TimeHorizonSeconds` remains one S-T output duration. -- Only real Goal/GearSwitch boundaries use exact zero-speed terminals and an in-QP stationary S/U/A tail. -- `RollingContinuation` and `ApproachStopBoundary` may publish nonzero terminal speed and do not gain a synthetic hold. -- `ZeroSpeedHoldSeconds` remains outside the QP horizon. -- Previous-trajectory PathS/speed arrays remain objective soft references, never terminal hard constraints. -- No jerk, acceleration, solver, or publication-validation threshold was relaxed. diff --git a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md b/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md deleted file mode 100644 index 40310e7..0000000 --- a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md +++ /dev/null @@ -1,72 +0,0 @@ -# EM Longitudinal Rolling Planning — Phase 1 Handoff - -## Scope and commits - -- Phase start baseline: `6c2f406` (`docs: split EM rolling plan into four phases`) -- Task 1 commit: `e56220f` (`feat: add complete jerk-limited stopping math`) -- Task 2 commit / phase code end: `21b20d0` (`feat: separate rolling horizons from stop boundaries`) - -This phase implemented only original-plan Tasks 1 and 2. It did not implement the Task 3 speed-envelope behavior, Task 4 conditional QP constraints, Task 5 optimizer seeding, trajectory publication behavior, or any chassis command. `OBSERVE_ONLY` remains unchanged. - -## Delivered changes - -Task 1 added `JerkLimitedStoppingMath` and `JerkLimitedStoppingProfile` in `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs`. The public API is: - -```csharp -JerkLimitedStoppingMath.TryCalculate(double speed, double acceleration, - double maximumDeceleration, double maximumJerk, - out JerkLimitedStoppingProfile profile, out string failureReason) -JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(double availableDistance, - double conservativeInitialAcceleration, double maximumDeceleration, - double maximumJerk, double directionMaximumSpeed) -JerkLimitedStoppingMath.CalculateMaximumStoppedDistance(double initialSpeed, - double initialAcceleration, double maximumSpeed, double maximumAcceleration, - double maximumDeceleration, double maximumJerk, double timeHorizon) -``` - -The profile exposes `DistanceMeters`, `DurationSeconds`, `FinalSpeedMetersPerSecond`, and `FinalAccelerationMetersPerSecondSquared`. The model uses the ST constant-jerk integration equations and releases both speed and acceleration to zero. `PathSpeedLimitBuilder` now uses it for its existing stopping precheck; the old duplicate stopping classes were removed. - -Task 1 files were `JerkLimitedStoppingMath.cs`, `PathSpeedLimitBuilder.cs`, and `LongitudinalModelChecks.cs`. Two existing direct callers of the removed internal types also required minimal compilation adaptation: `PlanningHorizonSelector.cs` and `SequentialLongitudinalOptimizer.cs`. They now call the new math but otherwise preserve their pre-Task-2 behavior. - -Task 2 added `EmLongitudinalMode` (`RollingContinuation`, `ApproachStopBoundary`, `ExactStopAtBoundary`) and `LongitudinalTerminalSchedule.GetStabilizationStartIndex(IReadOnlyList, double)`. - -`PlanningHorizonSelection` now records `WindowEndReferenceS`, `WindowEndBoundaryType`, `TerminalType`, `LongitudinalMode`, `StopBoundaryReferenceS`, and `HasStopBoundary`. `TerminalReferenceS` remains as a read-only compatibility alias for the window end. - -`PlanningHorizonSelector` now: - -- rejects an already insufficient real segment stopping distance using the complete jerk model; -- sets the LS window with only `DistanceHorizonMeters`; -- uses only real `Goal` and `GearSwitchApproach` segment ends as stop boundaries; -- selects rolling when the boundary is outside the window, approach when visible but not time-reachable with the mandatory internal stabilization step, and exact stop otherwise; -- does not deduct `ZeroSpeedHoldSeconds` from the ST horizon. - -`LongitudinalPlanningInput` now accepts `EmLongitudinalMode` and exposes `PathUpperBoundS`, `HasStopBoundary`, `StopBoundaryPathS`, `StopBoundaryType`, and `Mode`. Its original constructor remains as a compatibility overload, mapping rolling terminals to `RollingContinuation` and Goal/GearSwitch terminals to `ExactStopAtBoundary`; new code must use the mode-explicit constructor. `TerminalPathS` remains a compatibility alias for now, because Tasks 3–5 must migrate its upper-bound and stop-boundary uses deliberately. - -`EmPlanningRequestValidator` checks the worst forward/reverse full jerk-limited stop at configured maximum positive acceleration plus one replan-period reserve against `DistanceHorizonMeters`, and checks that the ST knot horizon can reserve a full internal stabilization interval. `EmPlanningService` slices with `WindowEndReferenceS` and passes the selected longitudinal mode into the ST input. - -Task 2 files were `EmLongitudinalMode.cs`, `LongitudinalTerminalSchedule.cs`, `PlanningHorizonSelector.cs`, `LongitudinalPlanningInput.cs`, `EmPlanningRequestValidator.cs`, `EmPlanningService.cs`, `LongitudinalModelChecks.cs`, and `FoundationChecks.cs`. `LongitudinalIntegrationChecks.cs` was additionally updated only to pass `Mode` at every direct `LongitudinalPlanningInput` construction. - -## Verification evidence - -The phase-end commands were run from commit `21b20d0`; each exited `0`: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -# PASS foundation - -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -# PASS longitudinal-model - -git diff --check -# exit 0 -``` - -The .NET commands still print two pre-existing obsolete warnings from `MovementTests.TireFollowing.cs` and `TireFollowing.cs`; phase 1 added no warning source. The staged-file checks before each commit contained only the listed phase files and the explicitly noted direct-caller adaptations. - -## Remaining work and constraints for phase 2 - -Phase 2 is original-plan Tasks 3–5 only. It must make speed envelopes, QP terminal constraints, solution validation, and optimizer seeds conditional on `LongitudinalPlanningInput.Mode`. It must replace legacy `TerminalPathS` semantics with `PathUpperBoundS` versus `StopBoundaryPathS` at the relevant use sites, without changing trajectory publication (Task 6+) or adding any chassis command. - -Do not loosen jerk, acceleration, velocity, QP residual, or trajectory-validation tolerances. `DistanceHorizonMeters` remains the LS spatial look-ahead; `TimeHorizonSeconds` remains one ST solve duration. A rolling window end is not a stop boundary. Exact stops must reserve at least one full `OutputTimeStepSeconds` static interval inside the QP horizon. `ZeroSpeedHoldSeconds` stays outside that horizon and is not phase-2 work. - -The working tree still contains extensive unrelated user PathSmoothing, CoarsePath, Map, configuration, report, and untracked-file changes. Preserve them; do not reset, clean, broadly stage, or commit them. No target-machine OSQP probe was run in phase 1; it belongs to the later roadmap phase. diff --git a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-2.md b/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-2.md deleted file mode 100644 index 843f74a..0000000 --- a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-2.md +++ /dev/null @@ -1,60 +0,0 @@ -# EM Longitudinal Rolling Planning — Phase 2 Handoff - -## Scope and commits - -- Phase start baseline: `21b20d0` (`feat: separate rolling horizons from stop boundaries`) -- Task 3: `94a9be9` (`feat: keep rolling speed envelopes open`) -- Task 4: `efa03c1` (`feat: apply ST stop constraints only at real boundaries`) -- Task 3 compatibility fixup: `dbc7b7c` -- Task 4 initial-state/envelope fixup: `26bd822` -- Task 5 / phase code end: `59d13e5` (`feat: seed rolling and exact-stop ST profiles`) - -Only original-plan Tasks 3, 4, and 5 were implemented. The two fixups could not be autosquashed because the shared worktree has unrelated unstaged user changes and Git refused rebase. They are intentionally retained as separate local commits; their changes belong to Tasks 3 and 4 respectively. - -No trajectory publication code, service-flow work, external hold behavior, chassis command, or Task 6+ production code was started. `OBSERVE_ONLY` remains unchanged. - -## Delivered behavior - -`PathSpeedLimit` now exposes `HasStopBoundary` and `PathUpperBoundS`; `TerminalPathS` remains a warning-free compatibility alias. `PathSpeedLimitBuilder` gives `RollingContinuation` a nonzero open end and no stop boundary. `ApproachStopBoundary` and `ExactStopAtBoundary` use `JerkLimitedStoppingMath.MaximumInitialSpeedForDistance` at every PathS sample against `StopBoundaryPathS`; only the actual stop-boundary sample is exactly zero. The old square-root limit and discrete deceleration tail approximation were removed. - -`LongitudinalConstraintBuilder` applies no terminal S/U/A equality to Rolling or Approach. Exact mode uses `LongitudinalTerminalSchedule.GetStabilizationStartIndex` and pins every internal tail knot to `StopBoundaryPathS`, zero speed, and zero acceleration. Its row count is `8*K-2+3*M`; the other modes remain `8*K-2`. - -`LongitudinalSolutionValidator` always checks knot layout, finite values, PathS monotonicity/bounds, jerk, and exact constant-jerk dynamics. Approach and Exact independently test each knot against its complete jerk-limited stopping distance; Exact also canonicalizes the entire internal static tail. The Task 4 fixup preserves the supplied physical state at knot zero: the conservative maximum-positive-acceleration envelope applies from the following knot, while the actual-state stopping check still applies at knot zero. - -`SequentialLongitudinalOptimizer` dispatches seed construction by `EmLongitudinalMode`. Rolling forward-integrates without forcing the LS path end. Approach uses the stopping-speed envelope while producing only this ST time prefix. Exact uses the stabilization schedule, tries a cruise-plus-jerk-brake profile, and otherwise constructs a terminal-equality seed before filling the QP-internal static tail. Exact-only outer iteration pins tail progress; ordinary knots use `PathUpperBoundS`. The final-knot forward-probe index guard remains, without restoring an unconditional terminal-position pin. - -`LongitudinalObjectiveBuilder` uses `PathUpperBoundS` for normalization and omits the terminal-acceleration soft residual only in Exact mode. Integration fixtures now explicitly distinguish generic Approach OSQP scenarios from the independent Exact-stop tail scenario; this avoids incorrectly demanding an exact stop for a distance that cannot fit inside the ST motion horizon. - -## Interfaces the next phase must preserve - -- `JerkLimitedStoppingMath`, `JerkLimitedStoppingProfile` -- `EmLongitudinalMode`: `RollingContinuation`, `ApproachStopBoundary`, `ExactStopAtBoundary` -- `LongitudinalTerminalSchedule.GetStabilizationStartIndex` -- `PlanningHorizonSelection.WindowEndReferenceS`, `WindowEndBoundaryType`, `LongitudinalMode`, `StopBoundaryReferenceS`, `HasStopBoundary` -- Mode-explicit `LongitudinalPlanningInput`, `PathUpperBoundS`, `StopBoundaryPathS`, `HasStopBoundary` -- `PathSpeedLimit.HasStopBoundary` and `PathSpeedLimit.PathUpperBoundS` - -`TerminalPathS` is compatibility-only. Use `PathUpperBoundS` for every path/window upper bound and `StopBoundaryPathS` only for a real Goal/GearSwitch stop. - -## Verification evidence - -The following were rerun at phase end and exited `0`: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -# PASS longitudinal-model - -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -# PASS longitudinal-integration - -git diff --check -# exit 0 -``` - -The .NET runs report two pre-existing obsolete warnings from `MovementTests.TireFollowing.cs` and `TireFollowing.cs`; phase 2 introduced none. `git diff --check` also prints CRLF notices for unrelated pre-existing dirty files but returned success. - -`git log -5 --oneline` ended with `59d13e5`, `26bd822`, `dbc7b7c`, `efa03c1`, and `94a9be9`. The worktree still contains extensive unrelated modified, deleted, and untracked PathSmoothing, CoarsePath, Map, report, and configuration files. Preserve them; never reset, clean, broadly stage, or commit them. - -## Phase 3 boundary - -Phase 3 is only original-plan Tasks 6 and 7: mode-aware trajectory sampling/publication validation, followed by previous-trajectory longitudinal soft references. It must not begin Task 8, 9, or 10. `ZeroSpeedHoldSeconds` remains outside the QP horizon and becomes an external hold only for Exact mode. Rolling and Approach must publish their ST horizon unchanged, with no synthetic terminal stop or boundary marker. diff --git a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-3.md b/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-3.md deleted file mode 100644 index fbdd9cf..0000000 --- a/docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-3.md +++ /dev/null @@ -1,110 +0,0 @@ -# EM Longitudinal Rolling Planning — Phase 3 Handoff - -## Scope and commits - -- Phase 3 implementation baseline: `59d13e5` (`feat: seed rolling and exact-stop ST profiles`) -- Task 6: `4159ae0` (`feat: publish rolling trajectories without stop tails`) -- Task 7 / phase 3 code end: `6cfbaf6` (`feat: reuse prior trajectory in longitudinal planning`) - -Only original-plan Tasks 6 and 7 were implemented. Task 8, Task 9, and Task 10 have not been started. This handoff and the companion phase-4 prompt are documentation-only follow-up artifacts; they do not change the phase-3 code end above. - -The preceding Task 3 and Task 4 fixups remain separate commits, `dbc7b7c` and `26bd822`, because the shared dirty worktree prevented autosquash. Preserve them as part of the actual history; do not rebase, reset, clean, or stash user files merely to rewrite it. - -## Delivered behavior - -### Task 6: mode-aware publication and validation - -`EmTrajectoryMetadata` now carries `EmLongitudinalMode`, supplied by `EmPlanningService`. `TrajectorySampleSchedule` is mode-aware: - -- `RollingContinuation` and `ApproachStopBoundary` publish exactly the ST candidate knots, without an artificial zero-speed hold or a terminal boundary anchor. -- `ExactStopAtBoundary` locates the internal stable tail with `LongitudinalTerminalSchedule.GetStabilizationStartIndex`, requires its constant `PathS`, `U=0`, `A=0`, and zero tail jerk, marks only its first point as the boundary anchor, then appends `ZeroSpeedHoldSeconds` after the QP horizon with strictly increasing timestamps. - -`EmTrajectoryAssembler` marks an anchor only for the first exact stable-tail knot. `EmTrajectoryValidator` retains finite-value, ordinary kinematics, jerk, and collision checks for every mode, but applies terminal-anchor, zero terminal speed/acceleration/yaw, and static-tail requirements only to `ExactStopAtBoundary`. It now reports `TerminalAccelerationNotZero` when appropriate. No validation, solver, acceleration, or jerk threshold was relaxed, and `OBSERVE_ONLY` behavior is unchanged. - -Task 6 changed only: - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs` - -### Task 7: previous-trajectory longitudinal soft reference - -`LongitudinalPreviousTrajectorySeed` and `LongitudinalPreviousTrajectorySeedBuilder` were added. The builder samples a compatible previous `EmTrajectory` at `newEffectiveAtUtc + knotTime`, linearly interpolates world position and signed speed, monotonically projects the position onto the current `LateralPath`, and publishes non-negative progress speed. Missing trajectories, incompatible segment/direction, malformed input, and out-of-range sampling return `LongitudinalPreviousTrajectorySeed.Empty`; none of those conditions throw into the service publication path. - -After lateral planning succeeds, `EmPlanningService` creates the current ST knot times, builds the seed with the prior trajectory/current path/effective time/segment/direction, and passes its `PathS` and `ProgressSpeedMetersPerSecond` arrays into the existing explicit-mode `LongitudinalPlanningInput`. These arrays activate the existing previous-S and previous-U objective terms only as soft references. They are not terminal constraints. LS slicing remains based on `WindowEndReferenceS`. - -Task 7 changed only: - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs` (new) -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -The service test fixture now captures the emitted ST quadratic program to prove that valid seeds add nonzero previous-S/previous-U objective terms, while an invalid seed adds neither and planning still succeeds. Its scripted solver now returns the supplied warm start for normal longitudinal QPs, allowing phase-1/2 constraints and the phase-3 soft reference to be exercised without fabricating an incompatible primal solution. - -## Interfaces the next phase must preserve - -- `EmLongitudinalMode`: `RollingContinuation`, `ApproachStopBoundary`, `ExactStopAtBoundary` -- `LongitudinalTerminalSchedule.GetStabilizationStartIndex` -- Explicit-mode `LongitudinalPlanningInput`, including `PreviousPathS` and `PreviousProgressSpeedMetersPerSecond` -- `PathUpperBoundS`, `StopBoundaryPathS`, and `PathSpeedLimit.HasStopBoundary` -- `EmTrajectoryMetadata.LongitudinalMode` -- `LongitudinalPreviousTrajectorySeed.PathS`, `ProgressSpeedMetersPerSecond`, and `Empty` -- `LongitudinalPreviousTrajectorySeedBuilder.Build(EmTrajectory, LateralPath, DateTimeOffset, IReadOnlyList, int, TravelDirection)` - -`TerminalPathS` remains compatibility-only. Use `PathUpperBoundS` for the LS/path-window upper bound; use `StopBoundaryPathS` only for an actual Goal or GearSwitch stop boundary. - -## Verification evidence - -All commands below exited `0` at the stated checkpoint: - -```powershell -# Before Task 6 commit 4159ae0 -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -# PASS trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -# PASS coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -# PASS executor -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -# PASS trajectory-observation - -# Before Task 7 commit 6cfbaf6 -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -# PASS longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -# PASS em-planning-service - -# Fresh phase-end checks after Task 7 -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -# PASS trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -# PASS em-planning-service -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -# PASS coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -# PASS executor - -git diff --check -# exit 0 -``` - -The fresh `trajectory` run emitted two pre-existing obsolete-API warnings from `MovementTests.TireFollowing.cs` and `TireFollowing.cs`; no failure resulted. `git diff --check` returned success but printed CRLF conversion notices for extensive unrelated dirty user files. - -At phase end, `git log -5 --oneline` began with `6cfbaf6`, `4159ae0`, `72592f0`, `59d13e5`, and `26bd822`. The worktree still contains many unrelated modified, deleted, and untracked files in PathSmoothing, CoarsePath, Map, reports, configuration, and other user work. They were not staged or committed. Preserve them exactly: do not run reset, checkout, clean, stash, `git add .`, or `git add -A`. - -## Phase 4 boundary - -Phase 4 is original-plan Tasks 8, 9, and 10 only: - -1. Add service-level multi-cycle regressions for `RollingContinuation -> ApproachStopBoundary -> ExactStopAtBoundary`, including Goal/GearSwitch and independent 5 m / 2 s horizon semantics. -2. Add observation diagnostics and documentation for mode, terminal state, and jerk-limited stopping capability while retaining `OBSERVE_ONLY`. -3. Run the complete regression/build set and the target-machine real-OSQP probe. If the current machine lacks the native dependency, record that as pending target-machine verification rather than changing longitudinal logic or thresholds. - -Do not alter the phase-3 behavior to make the new Task 8 fixtures easier. In particular, rolling and approach are allowed to end with nonzero speed, only exact stops carry a boundary anchor/static tail, `ZeroSpeedHoldSeconds` is outside QP time, and previous-trajectory references remain soft only. diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-01.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-01.md deleted file mode 100644 index ad23e9f..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-01.md +++ /dev/null @@ -1,84 +0,0 @@ -# EM 完整方向段规划与可视化修复:阶段 01 交接 - -## 状态 - -完成(此前阻塞的构造调用传播已获用户明确授权扩展范围)。 - -## 环境、入口与前置核验 - -- 执行时间:2026-08-06T20:56:08.7268822+08:00;Windows / PowerShell / .NET SDK 10.0.302。 -- 入口分支与 HEAD:`trajplanner` / `57ea36b8595c621783aba844e18d9969bccea378`。 -- 恢复交接祖先:`fcf7df17d54d84904278e993104e131afe8e405d`。 -- 以下提交均以 `git merge-base --is-ancestor` 返回 0:`c354f113060a5f95aa3b06c6f2b0be2d0c7d5da1`、`dff223c33a439d545537c803cb3531a673c7f2a3`、`46d5f9762d3d9972daea918c9fb43562ce25ce3b`、`57ea36b8595c621783aba844e18d9969bccea378`、`fcf7df17d54d84904278e993104e131afe8e405d`。 - -## 实现提交 - -- Task 1:`f4e89b4b4fe67f4924420ba8b3e06dea1d4ccc8b` — `feat: define full-direction EM planning scope` -- Task 2:`048b4f618e2237cd0cb3d257bf6ee04a1d13670b` — `feat: select complete EM direction segments` - -## 接口与实际修改 - -- 新增 `EmPlanningScope.RollingHorizon`、`FullDirectionSegment`;request 与 metadata 均保存不可变 scope。 -- 新增 desired speed、优化结点/发布上限、终端位置/yaw 容差、复制和交叉校验;默认值为 1.0/0.5 m/s、0.20 s、0.10 m、401、5001、0.03 m、5°。 -- 新增 `NoProgress`、`TerminalPoseMismatch`、`FullSegmentResourceLimitExceeded` 状态。 -- `PlanningHorizonSelector.Select` 显式接收 scope;full 模式仅接受 Goal/GearSwitch,选择真实方向段末端并使用 `ExactStopAtBoundary`;rolling 分支保持原有窗口/approach/exact 行为。 -- `EmPlanningService` 传递 scope,metadata 与 diagnostics 均携带 scope。 -- 用户随后授权的必需构造调用传播:`TrajectoryObservationPipeline.cs` 及 EM 验证宿主中的 coordinator/executor/trajectory/observation 构造夹具;所有旧调用显式使用 `RollingHorizon`,没有改变 MovementTest 的规划行为。 - -实际修改文件为两个实现提交的 `git show --stat` 所列 21 个文件;Task 2 没有修改 `EmTrajectoryAssembler.cs`,因为现有 assembler 已基于 metadata 保留终端边界,完整段选择无需额外 assembly 行为。 - -## 新鲜 TDD 与回归证据 - -### Task 1 - -RED: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -首次失败为缺少 `EmPlanningScope`(CS0246/CS0103)。同次曾出现 `TrajectoryPlanningVisualization.dll` 瞬时写锁;单独复现后锁未持续,已按 systematic-debugging 认定为外部构建竞争,未作为功能 RED。之后完整构造调用传播获授权。 - -GREEN:相同两个命令均退出 0,输出:`PASS foundation`、`PASS em-planning-service`。 - -### Task 2 - -RED: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -失败为 `PlanningHorizonSelector.Select` 没有接收新增 scope 参数(CS1501),符合预期。 - -GREEN: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -均退出 0,输出:`PASS longitudinal-model`、`PASS em-planning-service`。测试覆盖 10 m/投影 3 m/legacy 1 m 的 full 真实末端、rolling 4 m 截断及 GearSwitch 边界;服务测试覆盖 scope metadata 和发布起止边界。 - -阶段回归: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -``` - -退出 0,完整摘要:`PASS longitudinal-model`、`PASS longitudinal-integration`、`PASS trajectory`、`PASS em-planning-service`。首次回归时 `TrajectoryChecks` 的速度夹具受批准的硬上限变更影响,先触发加速度而非速度;夹具改为 1.10 m/s 以继续隔离速度上限,之后回归通过。 - -## 脏工作区与范围保护 - -- `EmPlannerConfiguration.cs` 的用户 `DistanceHorizonMeters = 500d` 和两条中文注释先保存 diff、始终未暂存;提交前以 index-only staging 核验其不在两个实现提交中。 -- `MovementTest.TrajectoryObservationTest.cs` 未编辑、未暂存、未提交。 -- 既有大量脏修改/未跟踪项均未清理;暂存区在本交接创建前为空。 -- 未使用 `git stash`、`git reset --hard`、`git checkout --`、`git add .` 或 `git add -A`。 -- `git diff --check 57ea36b8595c621783aba844e18d9969bccea378..048b4f618e2237cd0cb3d257bf6ee04a1d13670b` 通过。 - -## 未执行内容与阶段 02 输入 - -阶段 02–09 均未实现。尤其没有实现自适应 knot/T_end、静止起步、NoProgress 发布门禁、终端世界位姿门禁、MovementTest one-shot、Web 或 Painter。 - -阶段 02 从 `048b4f618e2237cd0cb3d257bf6ee04a1d13670b` 开始,消费本阶段的 scope/configuration。只允许总计划 Task 3 列出的 Longitudinal 文件、`EmPlanningService.cs`、`LongitudinalModelChecks.cs`、`LongitudinalIntegrationChecks.cs` 和 `phase-02.md`;不得触及静止起步、终端位姿或任何 MovementTest/UI。先运行 Task 3 RED `longitudinal-model`,再运行 `longitudinal-model` 与 `longitudinal-integration` GREEN,提交 `feat: derive adaptive full-segment ST schedule`,运行 `em-core-all`,最后单独提交 `phase-02.md`。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-02.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-02.md deleted file mode 100644 index fc22dad..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-02.md +++ /dev/null @@ -1,34 +0,0 @@ -# EM Complete Direction Visualization — Phase 02 Handoff - -## Status - -Complete on 2026-08-06. - -Implementation commit: `3269d55` — `feat: derive adaptive full-segment ST schedule` - -## Delivered scope - -- Full-direction ST optimization knots and `T_end` are derived from the actual Local G2 `PathS` speed/stopping envelope and physical jerk/acceleration reachability. -- `LongitudinalKnotSchedule` is an optimization-only contract: knot times, `PathS`/speed references, and an explicit terminal-hold start index. It intentionally contains no reference candidate. -- Full exact stops first execute a bounded deterministic S/U/A/J feasibility QP through the injected `IQpSolver`. It uses nonuniform exact dynamics, actual-`PathS` speed-envelope relinearization, initial/terminal/hold equalities, hard acceleration/jerk bounds, and a shared solver budget. -- Only a strictly validated candidate can become a fallback. A timeout before one exists returns `SolverTimedOut`; a later timeout returns `SuccessWithFallback`. -- Full-scope publication resamples the optimized candidate and does not append the rolling zero-speed hold twice. Rolling-horizon publication behavior remains unchanged. - -## Tests - -The following commands were run sequentially with exit code 0: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -``` - -`em-core-all` was rerun after the implementation commit and passed. The only warnings were existing obsolete API warnings for `Lidar2dDetect2LegTray.LegWidth` and `MultiWheelChassis.GetSteerWheels()`. - -## Worktree safeguards - -The existing user changes in `EmPlannerConfiguration.cs` and `MovementTest.TrajectoryObservationTest.cs`, together with unrelated dirty changes, were not edited or staged. - -Phase 03+ work (static start, NoProgress, terminal world pose, MovementTest, Web, Painter) remains out of scope. diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-03.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-03.md deleted file mode 100644 index de059d9..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-03.md +++ /dev/null @@ -1,153 +0,0 @@ -# EM 完整方向段可视化修复 — Phase 03 交接 - -## 状态 - -完成,2026-08-07。 - -启动分支为 `trajplanner`,启动 HEAD 为 `162f1a2`(Phase 02 handoff)。已确认 `3269d55` 与 `162f1a2` 均为本阶段提交的祖先。 - -本阶段实现提交: - -- `0bba8d7` — `fix: accelerate EM trajectories from rest` -- `e844fe0` — `feat: validate EM terminal world pose` - -本交接文件单独提交;其真实提交哈希应由提交后的 Phase 03 最终答复提供给 Phase 04。 - -## 目标与非目标 - -完成主计划 Task 4–5:完整方向段可从静止起步,非终端全零候选被精确拒绝,并且在发布前验证真实终端位姿和样本数量上限。 - -未进入 MovementTest、UI、Web、Native Painter、跨段衔接或任何后续阶段;未新增执行器或硬件写输出。 - -## Task 4:静止起步与 NoProgress - -- 完整方向段的纵向目标跟踪自适应 knot schedule 的参考速度,而不是在每个结点只追踪当前速度上限;滚动窗口保持原有速度上限目标。 -- 静止、零加速度的完整段优先构造并严格验证可达的 S/U/A/J 精确停车种子,因此清晰 5 m 段能够起步、加速、推进并在边界停车。 -- 停车包络使用实际初始加速度。此前预计算阶段按最大正加速度计算,过早将可用速度压为零;现在服务层的预计算和纵向输入使用同一实际状态。 -- 自适应 schedule 在加速→巡航、巡航→减速和换向的转折点保留站点;时间可达性计算保持原始请求时间,不会因原地改写时间而产生非递增结点。 -- `LongitudinalSolutionValidator` 对路径长度超过终端位置容差的 `FullDirectionSegment` 检查实际推进量。若不超过空间容差,返回精确的 `EmPlanningStatus.NoProgress` 与诊断;合法的终端容差内保持不误判。 -- 初始可行投影把 `NoProgress` 直接传回服务层;服务结果不包含 trajectory。 - -Task 4 修改: - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -`PathSpeedLimitBuilder.cs` 和 `FullDirectionSegmentScheduleBuilder.cs` 是为复现并修复静止起步包络/结点前提而获用户明确授权纳入的最小附加范围;`EmPlannerConfiguration.cs` 未修改。 - -## Task 5:终端位姿、样本上限与发布门禁 - -- `EmTrajectoryValidator` 新增接收 `Pose2D terminalPose` 的验证重载。只有真正的 `Goal` 与 `GearSwitchApproach` 精确终端锚点才检查世界位置和航向;滚动安全窗终点不进入此门禁。 -- 位置误差使用欧氏距离并受 `TerminalPositionToleranceMeters`(3 cm)约束;航向误差归一化到 `[-π, π]`,再受 `TerminalYawToleranceRadians`(5°)约束。失败码为 `EmTrajectoryValidationFailure.TerminalPoseMismatch`。 -- 服务层把 Local G2 的最后一个 `LateralPathPoint` 作为期望终端世界位姿,并把该验证失败映射为 `EmPlanningStatus.TerminalPoseMismatch`;失败不发布 trajectory。 -- `TrajectorySampleSchedule.ExceedsMaximumSampleCount` 在组装点列表前按实际采样规则预检:包含终端锚点与停止保持样本。超过 `MaximumPublishedSampleCount` 时 `EmTrajectoryAssembler.TryAssemble` 返回 `FullSegmentResourceLimitExceeded`,trajectory 为 `null`,不会截断、放宽采样周期或返回半成品。 -- 原有时间严格递增、终端锚点、N-1 jerk、S/U/A/J 停止尾和世界碰撞验证保持不变。 - -Task 5 修改: - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -新增测试覆盖 2.9 cm/3.1 cm、4.9°/5.1°、179°/-179° yaw 绕回、滚动安全窗豁免、恰好多一个发布样本、组装拒绝无部分 trajectory,以及服务状态映射。 - -## RED / GREEN / 回归证据 - -Task 4 的静止起步集成测试先暴露了全零参考/不可行的停止包络;修复后以下命令均以退出码 0 通过: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Task 5 的 RED 是新增测试后编译失败,原因精确为缺少带终端 `Pose2D` 的验证重载、`TerminalPoseMismatch` 失败码和拒绝式 `TryAssemble` 接口。实现后以下命令均以退出码 0 通过: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -最终新鲜回归: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -git diff --check -``` - -`em-core-all` 退出码为 0,输出依次为 `PASS longitudinal-model`、`PASS longitudinal-integration`、`PASS trajectory`、`PASS em-planning-service`。`git diff --check` 通过。 - -已知且保留的两条编译 warning:过时的 `Lidar2dDetect2LegTray.LegWidth` 与 `MultiWheelChassis.GetSteerWheels()`。 - -## 工作区保护 - -保留并未编辑/暂存用户原有的脏工作区内容,包括 `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs`、`ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs`、PathSmoothing、CoarsePath、Map、文档和未跟踪报告等改动。未使用 `git reset --hard`、`git checkout --`、`git clean`、`git add .` 或 `git add -A`。 - -## Phase 04 完整启动提示词 - -```text -执行 EM 全方向段可视化修复的 Phase 04:MovementTest 每个方向段仅规划一次。 - -仓库:D:\Users\Desktop\项目\prakrobot\ParkingRobot -预期分支:trajplanner -Phase 03 实现提交必须可达:0bba8d7、e844fe0。 -Phase 03 handoff:docs/superpowers/handoffs/em-full-direction-visualization/phase-03.md;同时要求调用方提供并验证该 handoff 的提交哈希可达。 - -只执行主计划 Task 6;不得进入 Task 7+、Web、Painter、跨段规划扩展或实车操作。MovementTest 必须保持 OBSERVE_ONLY,严禁新增底盘、转向、制动、电机或换挡写输出。不得派生子智能体,也不要重新 brainstorming 或重设计已批准行为。 - -开始时必须: -1. 读取 executing-plans、test-driven-development、verification-before-completion;仅在真实失败或意外行为后读取 systematic-debugging。 -2. 完整阅读功能设计、主计划 Task 6、分阶段执行计划 Phase 04、阶段执行设计及本 Phase 03 handoff;发生矛盾则停止并写阻塞交接,请求用户裁决。 -3. 运行并记录: - git branch --show-current - git rev-parse HEAD - git log -5 --oneline - git status --short - git diff --cached --name-only - git merge-base --is-ancestor 3269d55 HEAD - git merge-base --is-ancestor 162f1a2 HEAD - git merge-base --is-ancestor 0bba8d7 HEAD - git merge-base --is-ancestor e844fe0 HEAD - 以及调用方提供的 Phase 03 handoff 哈希。 -4. 保留所有既有脏文件;修改已脏目标前先记录 diff 并逐 hunk 保留用户改动。严禁 git reset --hard、git checkout --、git clean、git add .、git add -A。 - -本阶段唯一允许修改/新增: -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs -- docs/superpowers/handoffs/em-full-direction-visualization/phase-04.md - -按 TDD 先编写 Task 6 指定的失败测试,然后运行: -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -确认首个有效 RED 是完整方向段仍按协调器节拍重复开始计划周期。 - -实现且仅实现:TrajectoryObservationSettings.PlanningScope(默认 FullDirectionSegment)、经过验证快照/请求的 scope 传播、批准的观察默认值,以及每个活动方向段最多一次计划尝试。尝试标志必须在启动异步计划前置位;只有 TryAdvanceSegment 确认 N→N+1 后才复位。失败的完整段计划保持可见但不自动滚动重试。保留已有停车保持和三条带符号速度样本握手、显式等待文案及 active segment 高亮。 - -GREEN 后运行: -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -git diff --check -期望 trajectory-observation PASS,且观察运行时源中没有执行器调用。 - -只显式暂存本阶段白名单实现/测试文件,并提交: -git commit -m "feat: observe one full EM direction segment" - -随后仅用 apply_patch 创建 phase-04.md,记录入口 HEAD、实现提交、RED/GREEN、执行器审计、保留脏工作区、warning、未运行项及 Phase 05 事实;只提交该 handoff: -git commit -m "docs: record EM full-direction phase 04 handoff" - -成功门禁:完整范围由显式设置启用;每个方向段仅计划一次;观察继续、停车/换向握手保持;无硬件写输出。若被阻塞,创建阻塞 handoff 和同阶段恢复提示词,不得进入 Phase 05。 -``` diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-04.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-04.md deleted file mode 100644 index 4dc862b..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-04.md +++ /dev/null @@ -1,102 +0,0 @@ -# EM 完整方向段可视化修复 — Phase 04 交接 - -## 状态 - -完成,2026-08-07。 - -启动分支为 `trajplanner`,启动 HEAD 为 `57d8abb`(Phase 03 handoff)。已确认 `3269d55`、`162f1a2`、`0bba8d7`、`e844fe0`、`57d8abb` 均为本阶段提交的祖先。 - -本阶段实现提交: - -- `ab3204b` — `feat: observe one full EM direction segment` - -本交接文件单独提交;其真实提交哈希由提交后的最终答复提供给 Phase 05。 - -## 目标与非目标 - -完成主计划 Task 6:MovementTest 每个活动方向段最多一次完整规划,换向确认后才允许下一段的新计划。 - -未进入 Task 7+、Web、Native Painter、跨段规划扩展或实车操作。MovementTest 保持 `OBSERVE_ONLY`,未新增任何底盘、转向、制动、电机或换挡写输出。 - -## 实现摘要 - -- `TrajectoryObservationSettings.PlanningScope` 默认 `EmPlanningScope.FullDirectionSegment`,经过 `CreateValidatedSnapshot` 复制并验证。 -- 批准的观察默认值:Web 默认启用、Native Painter 默认关闭、`OutputTimeStepSeconds` 保持 `0.1 s`;`TimeHorizonSeconds` 仅作为滚动兼容字段标注。 -- `TrajectoryObservationController` 保存经过验证的设置快照,并把 `PlanningScope` 传播到每个 `EmPlanningRequest`。 -- 控制器新增 `plannedSegmentIndex` 与 `planAttemptedForActiveSegment`:完整模式下 `ShouldStartCycle` 只允许每个活动方向段一次;`StartCycle` 在启动异步计划前置位;只有 `TryAdvanceSegment` 确认 N 到 N+1 后才复位。 -- 完整段规划失败保持可见且不自动滚动重试。 -- 保留已有停车保持、三个正确带符号速度样本、显式等待文案和 active segment 高亮。 -- 静态快照配置组新增 `PlanningScope`。 - -## 修改文件 - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -`TrajectoryObservationSegmentChecks.cs` 保持白名单内但本阶段无改动。 - -## 新增或改变的接口 - -- `TrajectoryObservationSettings.PlanningScope`,默认 `FullDirectionSegment`,快照复制并在 `Validate` 中拒绝未定义枚举值。 -- `TrajectoryObservationMovementTest.UseFullDirectionSegmentPlanning`,默认 `true`,映射为 settings 的 `PlanningScope`。 -- `TrajectoryObservationController.ShouldStartCycle`:完整模式不再委托协调器节拍。 -- `TrajectoryObservationController.StartCycle`:使用已验证 scope,并在启动异步任务前设置 one-shot 标志。 -- `TrajectoryObservationController.TryAdvanceSegment`:确认换向后复位 one-shot 标志。 - -## RED / GREEN / 回归证据 - -首次 RED 被既有用户脏配置 `EmPlannerConfiguration.cs` 的 `DistanceHorizonMeters = 500d` 拦住:原测试硬编码 `distanceHorizon=5.00m`。未修改该脏文件;改为从 `CreateEffectiveConfigurationSnapshot` 动态读取实际距离视界后断言。 - -随后确认 Task 6 有效 RED: - -```text -full direction segment is planned only once per active segment expected 1 but was 4. -``` - -即当前完整方向段仍按协调器节拍重复开始计划。 - -GREEN: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -退出码 0,输出 `PASS trajectory-observation`。 - -执行器源审计: - -```powershell -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -``` - -无匹配。 - -`git diff --check` 退出码 0;输出仅有既有 LF/CRLF 提示,无空白错误。 - -## 工作区保护 - -保留并未回退用户既有脏工作区,包括但不限于 `EmPlannerConfiguration.cs`、`MovementTest.TrajectoryObservationTest.cs` 中用户原有的 `TimeHorizonSeconds = 20d`、`EnableWebVisualization = true`、`EnableNativePainterVisualization = true`,以及 PathSmoothing、CoarsePath、Map、文档和未跟踪报告等改动。 - -本阶段只修改白名单实现/测试文件;未使用 `git reset --hard`、`git checkout --`、`git clean`、`git add .` 或 `git add -A`。 - -## 已知警告与偏差 - -- 既有两条编译 warning 保留:`Lidar2dDetect2LegTray.LegWidth` 与 `MultiWheelChassis.GetSteerWheels()`。 -- 既有诊断测试因用户脏配置从硬编码 `5.00m` 改为读取实际生效配置;未改写用户脏文件。 -- MovementTest 入口的 Native Painter 保持用户脏配置 `true`;`TrajectoryObservationSettings` 类的批准默认值仍为 `false`。 - -## 未运行项 - -- 未进入 Phase 05 或后续任何阶段。 -- 未执行 Web、Painter、跨段扩展或实车操作。 -- 未运行 `em-core-all`;本阶段验证命令按任务要求仅执行 `trajectory-observation`、执行器审计和 `git diff --check`。 - -## Phase 05 事实 - -对应主计划 Task 7,阶段执行计划 Task 5:统一快照、路径图层、曲线单位、`s_end`、车辆与换向语义。 - -Phase 05 的 Phase 04 实现提交为 `ab3204b`,上一交接文件为本文件;调用方需在最终答复中提供本交接提交的真实哈希并验证可达。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-05.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-05.md deleted file mode 100644 index 17e35de..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-05.md +++ /dev/null @@ -1,142 +0,0 @@ -# EM 完整方向段可视化修复 — Phase 05 交接 - -## 状态 - -完成,2026-08-07。 - -启动分支为 `trajplanner`,启动 HEAD 为 `5d1c875`(Phase 04 handoff)。已确认 `ab3204b`、`5d1c875` 均为本阶段提交的祖先。 - -本阶段实现提交: - -- `d33257c` — `fix: publish distinct EM observation semantics` - -本交接文件单独提交;其真实提交哈希由提交后的最终答复提供给 Phase 06。 - -## 目标与非目标 - -完成主计划 Task 7:统一快照、路径图层、曲线单位、`s_end`、车辆与换向语义。 - -未进入 Task 8+、Web DOM、Native Painter、跨段规划扩展或实车操作。MovementTest 保持 `OBSERVE_ONLY`,未新增任何底盘、转向、制动、电机或换挡写输出。 - -## 实现摘要 - -- 新增 `VisualizationChartAnnotation`,`VisualizationChart` 增加不可变 `Annotations`,旧构造函数保持空注解兼容。 -- 静态与动态快照使用 `gear-switch-end` / `final-goal` 标记,并补充车辆、计划起点标记;完整段不再重复发布 `current-horizon`。 -- LS/ST/世界均标记 `s_end`;换向边界使用“换向点 N / s_end”,最终目标使用“终点 / s_end”。 -- 动态快照构建器接收 `VehicleParameters`,曲率硬上限改为真实车辆 `MaximumCurvaturePerMeter`,横摆角速度轴改为 `ω (rad/s)`。 -- MovementTest 网页发布回调把 `bootstrap.Vehicle` 传入动态快照构建器。 - -## 修改文件 - -实际修改: - -- `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` - -白名单内未改动:`VisualizationGeometry.cs`。现有折线/标记/方向段几何契约已满足 Task 7 语义,无需重复修改。 - -## 新增或改变的接口 - -- `VisualizationChartAnnotation(id, kind, labelChinese, x, double? y = null)`。 -- `VisualizationChart.Annotations`;旧 6 参数构造器委托到带空注解的新构造器。 -- `TrajectoryObservationDynamicSnapshotBuilder.Build(..., VehicleParameters vehicle, EmPlannerConfiguration configuration)`。 -- `TrajectoryObservationKinematicChartBuilder.Build(..., VehicleParameters vehicle, EmPlannerConfiguration configuration)`。 -- 曲线硬上限取 `VehicleParameters.MaximumCurvaturePerMeter`;没有直接曲率时回退到 `MinimumTurningRadiusMeters`。 - -## RED / GREEN / 回归证据 - -RED: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -首个有效失败为 `static snapshot marks gear-switch s_end was false`,可视化宿主为 `chart exposes immutable annotations`。测试集还包含同一 current 不重复发布为 `current-horizon`、曲率上限必须来自车辆几何的断言;`Verification` 在首个失败处停止,因此未单独打印后续断言。 - -GREEN(串行运行,避免共享 `obj` DLL 锁): - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -退出码 0,输出 `PASS trajectory-observation`。 - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -退出码 0,输出 `PASS trajectory-planning-visualization`。 - -## 执行器审计与清理 - -```powershell -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -``` - -无匹配(rg 退出码 1,符合“无匹配”预期)。 - -`git diff --check` 与暂存后 `git diff --cached --check` 均退出码 0,仅有既有 LF/CRLF 提示,无新空白错误。 - -## 工作区保护 - -保留并未回退用户既有脏工作区与未跟踪内容,包括但不限于 `.gitignore`、`EmPlannerConfiguration.cs`、PathSmoothing/CoarsePath/Map 相关改动、文档和未跟踪报告。 - -实现提交只显式暂存本阶段白名单文件;提交后 `git diff --cached --name-only` 为空,未使用 `git reset --hard`、`git checkout --`、`git clean`、`git add .` 或 `git add -A`。 - -## 已知警告与偏差 - -- 既有两条编译 warning 保留:`Lidar2dDetect2LegTray.LegWidth` 与 `MultiWheelChassis.GetSteerWheels()`。 -- `git diff --check` 持续输出仓库既有 LF/CRLF 提示,不影响空白检查结果。 -- `VisualizationGeometry.cs` 本阶段未改动;如果后续阶段发现几何契约不足,应在 Phase 08 Painter 范围内处理,而不是本阶段扩展。 - -## 未运行项 - -- 未进入 Phase 06 或后续任何阶段。 -- 未执行 Web DOM/jsdom、Native Painter、跨段规划扩展或实车操作。 -- 未运行 `em-core-all`;本阶段验证命令按任务要求仅执行两个宿主、执行器审计和 `git diff --check`。 - -## Phase 06 事实 - -对应主计划 Task 8,阶段执行计划 Task 6:真实 DOM/SVG 测试与 Web 渲染修复。 - -Phase 06 允许创建/修改: - -- 创建:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json` -- 创建:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json` -- 创建:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- 修改:`ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` -- 交接:`docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md` - -接口与验证要求: - -- 使用 Node 24 + `jsdom` 29.1.1 的开发专用依赖验证真实 DOM/SVG。 -- 修复分页隐藏、空白总览、y 轴刻度/单位、稳定数据域、语义图层顺序、车辆/边界标记和论文风细线。 -- 测试钩子 `window.__TRAJECTORY_VISUALIZATION_TEST__ === true` 只暴露不可变输入渲染钩子,不启动 fetch/SSE。 - -必须执行: - -```powershell -npm install --package-lock-only --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -期望 RED 是 hidden-section CSS、y ticks、语义类或 empty-state 断言失败;GREEN 是全部 DOM 测试通过且 .NET 宿主打印 `PASS trajectory-planning-visualization`。 - -实现提交信息:`fix: repair EM observation web charts`。 - -交接文件单独提交信息:`docs: record EM full-direction phase 06 handoff`。 - -Phase 06 退出门禁:现有四页布局不重做,正确渲染数据/轴/图层;本阶段不实现缩放。 - -Phase 06 的 Phase 05 实现提交为 `d33257c`,上一交接文件为本文件;调用方需在最终答复中提供本交接提交的真实哈希并验证可达。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md deleted file mode 100644 index 38b0ad3..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md +++ /dev/null @@ -1,144 +0,0 @@ -# EM 完整方向段可视化修复 — Phase 06 交接 - -## 状态 - -完成,2026-08-07。 - -启动分支为 `trajplanner`,启动 HEAD 为 `8b82467618759294c4ccf810bf0699ca804171b8`(Phase 05 handoff)。已确认 `d33257c`、`8b82467` 均为本阶段启动 HEAD 的祖先。 - -本阶段实现提交: - -- `05cfd673f504d5ecee000ab9b85c7346436f2076` — `fix: repair EM observation web charts` - -本交接文件单独提交;其真实提交哈希由提交后的最终答复提供给 Phase 07。 - -## 目标与非目标 - -完成主计划 Task 8、阶段执行计划 Task 6:真实 DOM/SVG 测试与 Web 渲染修复。 - -未进入 Task 9+、Native Painter、跨段规划扩展或实车操作,未实现缩放。MovementTest 保持 `OBSERVE_ONLY`,本阶段未触碰任何底盘、转向、制动、电机或换挡写输出。 - -## 修改文件 - -实际修改: - -- 创建:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json` -- 创建:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json` -- 创建:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- 修改:`ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` - -白名单内 `index.html` 未改动:现有四页结构已满足 Task 8,无需重做布局。 - -## 新增或改变的接口 - -- 新增测试专用 Node 包,锁定 `jsdom` 29.1.1,`engines.node >= 24`,脚本为 `node --test dashboard.dom.test.mjs`。 -- `window.__TRAJECTORY_VISUALIZATION_TEST__ === true` 时,`app.js` 暴露冻结的 `window.__trajectoryVisualizationTestHooks`: - `setBootstrap`、`receiveFrame`、`renderActiveTab`、`installTabs`;不暴露内部 `state`,不启动 fetch/SSE。生产路径仍调用 `boot()`。 -- `app.css` 使用 `main > section[hidden] { display: none !important; }` 修复分页隐藏覆盖,并增加 y 轴刻度、语义路径、车辆/边界标记和空状态类。 -- `app.js` 新增 `niceDomain`、`formatTick`,渲染 x/y 双轴刻度及 `.axis-label-x` / `.axis-label-y`;非零常数域围绕实际值生成可读对称范围。 -- 世界图层按 coarse、Local G2、方向段、previous、current 的语义顺序绘制,并渲染车辆小轮廓、细朝向线、计划起点、换向点和最终目标标记。 -- 路径总览无可绘制路径时显示“未收到 Local G2 路径”,不留下无解释空白画布。 - -## RED / GREEN / 回归证据 - -RED: - -```powershell -npm install --package-lock-only --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -首个有效 DOM 失败为分页隐藏:点击 `ls-st` 后 `getComputedStyle(overview).display` 仍为 `grid`,预期为 `none`;随后依次失败于 y ticks、语义路径/标记、empty-state 和测试钩子。.NET 宿主首个有效失败为 `hidden sections cannot be overridden by grid rules`。 - -为运行 DOM 测试,lockfile 生成后补充执行了 `npm install --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom` 安装 jsdom;验证完成后删除 `node_modules`,未提交任何依赖目录。 - -GREEN: - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -``` - -退出码 0,6 个 DOM 测试全部通过,0 失败。 - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -退出码 0,输出 `PASS trajectory-planning-visualization`。 - -```powershell -git diff --check -``` - -退出码 0,仅有仓库既有 LF/CRLF 提示,无新空白错误。 - -## 执行器审计与清理 - -```powershell -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -``` - -无匹配(rg 退出码 1,符合“无匹配”预期)。 - -```powershell -rg -n "TrajectoryPlanningVisualizationWebDom|jsdom" ClumsyPilot/TrajectoryPlanningVisualization -g "*.csproj" -g "*.cs" -g "*.html" -g "*.css" -g "*.js" -``` - -无匹配(rg 退出码 1),确认 npm 测试包不进入生产 Web/插件输出。 - -## 工作区保护 - -保留并未回退用户既有脏工作区与未跟踪内容,包括但不限于 `.gitignore`、`EmPlannerConfiguration.cs`、PathSmoothing/CoarsePath/Map 相关改动、文档和未跟踪报告。 - -实现提交只显式暂存本阶段白名单文件;提交后 `git diff --cached --name-only` 为空,未使用 `git reset --hard`、`git checkout --`、`git clean`、`git add .` 或 `git add -A`。 - -## 已知警告与偏差 - -- `npm install` 输出 npm 12 升级提示,不影响依赖锁定。 -- `git diff --check` 持续输出仓库既有 LF/CRLF 提示,不影响空白检查结果。 -- 仓库 `.gitignore` 未忽略 `node_modules/`;本阶段临时生成的 `node_modules` 已删除,避免污染工作区与提交范围。 -- `index.html` 在本次白名单内但未改动,现有四页布局未重做。 - -## 未运行项 - -- 未进入 Phase 07 或后续任何阶段。 -- 未实现图表框选、滚轮缩放、重置、全屏或任何缩放功能。 -- 未执行 Native Painter、跨段规划扩展、实车操作或 `em-core-all`。 -- 本阶段验证命令按任务要求仅执行 jsdom 测试、.NET 可视化宿主、源审计和 `git diff --check`。 - -## Phase 07 事实 - -对应主计划 Task 9,阶段执行计划 Task 7:每张图独立视域缩放,不改变规划数据。 - -Phase 07 允许创建/修改: - -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- 修改:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` -- 交接:`docs/superpowers/handoffs/em-full-direction-visualization/phase-07.md` - -接口与验证要求: - -- 产生每张图的 `chartViewports[id]`,仅包含可见 x/y 数据域。 -- 产生框选放大、滚轮缩放、重置和全屏;缩放前后输入快照数组必须保持字节不变。 -- SSE 新周期到达时使用仍有效的本地视域重绘,不改写原始数据,也不向规划器发送参数。 - -必须执行: - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -git diff --check -``` - -实现提交信息:`feat: add observation chart viewport zoom`。 - -交接文件单独提交信息:`docs: record EM full-direction phase 07 handoff`。 - -Phase 07 退出门禁:每张图都有独立局部导航且不修改规划数据;不包含 Native Painter 改动。 - -Phase 07 的 Phase 06 实现提交为 `05cfd673f504d5ecee000ab9b85c7346436f2076`,上一交接文件为本文件;调用方需在最终答复中提供本交接提交的真实哈希并验证可达。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-07.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-07.md deleted file mode 100644 index d2f4cda..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-07.md +++ /dev/null @@ -1,105 +0,0 @@ -# EM 完整方向段可视化修复 - Phase 07 交接 - -## 状态 -完成,2026-08-07。 - -## 目标与非目标 -完成主计划 Task 9、分阶段执行计划 Task 7:每张图独立视域缩放,仅修改浏览器本地视图,不修改规划数据。 - -未进入 Task 10+、Native Painter、跨段规划扩展、实车操作或 Phase 06 已批准 Web 渲染修复的回退/重做。MovementTest 保持 `OBSERVE_ONLY`,本阶段未触碰任何底盘、转向、制动、电机或换挡写输出。 - -## 启动事实 -- 分支:`trajplanner` -- 入口 HEAD:`b7772370dca119d8ff36ed6a690274e460d489ca`(Phase 06 handoff) -- 上一阶段实现提交:`05cfd673f504d5ecee000ab9b85c7346436f2076` -- 上一阶段 handoff:`docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md` -- 祖先检查:`05cfd67`、`b777237` 均为入口 HEAD 可达祖先 - -## 实现提交 -`8a51629edbe682de8a0fd30a82894e0420ac6c86` - `feat: add observation chart viewport zoom` - -仅提交白名单内实际修改文件: -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- 修改:`ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- 修改:`ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` - -`index.html` 在白名单内但未修改,现有 chart host 结构已足够,工具条由 `renderChart` 动态创建。 - -## 新增接口与语义 -- `state.chartViewports[id]` 仅保存当前图表的可见 `x0/x1/y0/y1` 数据域。 -- 框选放大:`pointerdown` 到 `pointerup` 距离小于 4 CSS 像素时忽略。 -- 滚轮缩放:以指针在图表数据坐标中的位置为锚点。 -- 重置:只删除当前图表 `id` 的视域。 -- 全屏:只请求当前图表 host 的 `requestFullscreen()`,并附加 `is-fullscreen` CSS 回退。 -- SSE 新周期到达:`receiveFrame` 重绘时继续使用仍有限且有效的 `chartViewports[id]`;不修改快照数组,不发送规划器参数。 -- 缩放不进行数据重采样、不回写快照、不修改服务端参数。 - -## RED 证据 -先写入冻结输入测试,再执行: -```powershell -npm install --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -``` - -`npm test` 退出码 1;11 个测试中 8 个通过、3 个失败。 -首个有效失败为 `wheel and box zoom change ST ticks and path without mutating frozen snapshot`:框选后 ST 轴刻度仍为 `-0.5|0.25|1|1.75|2.5`,路径 `d` 未变化,确认是缩放控件与 `chartViewports` 状态缺失。`reset` 与 `fullscreen` 测试随后因 `button[data-action]` 不存在而失败。 - -## GREEN / 回归证据 -最终代码状态执行: -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -git diff --check -``` - -- `npm test`:退出码 0,11 个 DOM 测试全部通过,0 失败。 -- .NET 宿主:退出码 0,输出 `PASS trajectory-planning-visualization`。 -- `git diff --check`:退出码 0;仅有仓库既有 LF/CRLF 提示,无新空白错误。 -- `node --check app.js`、`node --check dashboard.dom.test.mjs`:均通过。 - -## 执行器审计与清理 -```powershell -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -``` -无匹配,确认未新增 MovementTest 硬件写输出。 - -```powershell -rg -n "TrajectoryPlanningVisualizationWebDom|jsdom" ClumsyPilot/TrajectoryPlanningVisualization -g "*.csproj" -g "*.cs" -g "*.html" -g "*.css" -g "*.js" -``` -无匹配,npm 测试包不进入生产 Web/插件输出。 - -临时 `node_modules` 在测试完成后已安全删除并确认不存在;未提交依赖目录。 - -## 工作区保护 -保留入口时所有既有脏文件、删除和未跟踪内容,未执行 `git reset --hard`、`git checkout --`、`git clean`、`git add .` 或 `git add -A`。实现提交仅显式暂存并提交上述 3 个白名单文件;提交后 `git diff --cached --name-only` 为空。 - -## 已知警告与偏差 -- `git diff --check` 持续输出仓库既有 LF/CRLF 提示,不影响本阶段空白检查结论。 -- `index.html` 未修改,因此实现提交实际文件数少于阶段白名单可允许数量。 -- 本阶段未运行浏览器截图或真实设备烟雾;DOM 行为由 jsdom 覆盖。 - -## 未运行项 -- 未进入 Phase 08 Native Painter 修复。 -- 未进入 Phase 09 文档、全量自动验收或实车验收。 -- 未执行 `em-core-all`、`--smoke-seconds`、浏览器人工操作或实车操作。 - -## Phase 08 事实 -- 阶段:Phase 08,对应主计划 Task 10、分阶段执行计划 Task 8。 -- 仓库:`D:\Users\Desktop\项目\prakrobot\ParkingRobot` -- 预期分支:`trajplanner` -- 本阶段实现提交:`8a51629edbe682de8a0fd30a82894e0420ac6c86` -- 本 handoff 真实哈希由 Phase 08 调用方在最终回答中提供。 -- 允许修改: - - `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` - - `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - - `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` - - 交接:`docs/superpowers/handoffs/em-full-direction-visualization/phase-08.md` -- 实现提交信息:`fix: correct EM observation painter geometry` -- 交接提交信息:`docs: record EM full-direction phase 08 handoff` -- 核心接口:世界路径 x/y 等比例、LS 横轴 `ReferenceS (m)`、ST 纵轴真实 `PathS (m)`、小车轮廊与细朝向线、`s_end`/换向/最终目标语义;不使用大黑三角。 -- RED/GREEN 焦点命令: - ```powershell - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation - ``` -- 回归/审计命令:同一 `trajectory-observation` 宿主、MovementTest 执行器 `rg`、`git diff --check`。 -- Phase 08 禁止:进入 Task 11、实车操作、跨段规划扩展、重新设计 Web 或 Phase 07 视域缩放。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-08.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-08.md deleted file mode 100644 index a0dee69..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-08.md +++ /dev/null @@ -1,82 +0,0 @@ -# EM 完整方向段可视化修复 - Phase 08 交接 - -## 状态 -完成(2026-08-07)。 - -## 目标与非目标 -完成主计划 Task 10、分阶段执行计划 Task 8:Native Painter 几何与语义修复。 -实现世界路径 x/y 等比例、LS 横轴 `ReferenceS (m)`、ST 纵轴真实 `PathS (m)`、小车轮廓与细朝向线、`s_end`/换向/最终目标语义。 -未进入 Task 11 或 Phase 09;未修改 Web 或规划器;未做跨段规划扩展、实车操作或 Painter 重设计。 -MovementTest 保持 `OBSERVE_ONLY`,本阶段未新增底盘、转向、制动、电机或换挡写输出。 - -## 入口事实 -- 分支:`trajplanner` -- 入口 HEAD:`d019b080746a083fc3c655e21b229e082051706a`(Phase 07 handoff) -- 上一阶段实现提交:`8a51629edbe682de8a0fd30a82894e0420ac6c86` -- 祖先检查:`8a51629` 与 `d019b08` 均为入口 HEAD 可追溯祖先 -- 入口暂存区为空;既有脏工作区与未跟踪内容全部保留 - -## 实现提交 -`37c27bdb1b41502ad23783a041a42512b3a49648` - `fix: correct EM observation painter geometry` - -仅提交白名单文件: -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` - -## 新接口与语义 -- `TrajectoryObservationLsPresentationModel.HorizontalAxisLabel` 为 `ReferenceS (m)`,`VerticalAxisLabel` 为 `l (m)`。 -- `DrawPose` 接受 `VehicleParameters`,按车辆长宽绘制四角旋转轮廓,并用 `endArrow: false` 绘制长度不超过半车长的细朝向线。 -- 世界路径使用同一 `MillimetersPerMeter` 转换 x/y;粗路径、Local G2、当前轨迹线宽收窄。 -- `DrawBoundaryMarkers` 绘制换向点 `换向点 N / s_end`(小菱形)与最终目标 `终点 / s_end`(小叉形),车辆标记为 `车辆`,计划起点为 `计划起点`。 -- ST Painter 增加 `t (s)` 与 `PathS (m)` 轴标签。 -- 未新增 Painter 图层、主题或生命周期;三个 Painter 窗口与清理路径保持原状。 - -## RED 证据 -先写入 Task 10 失败测试,再执行: -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -首次有效失败: -`observer painter removes the large terminal arrow was false` -位置:`TrajectoryObservationChecks.VerifiesPainterPoseGeometryReplacesLargeArrow`。 -确认 RED 是当前 Painter 仍使用旧的大终端箭头,而不是环境或测试自身错误。 - -期间测试新增的 `using System.Text` 编译缺失已修复,修复后重新取得上述行为 RED。 - -## GREEN / 回归证据 -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -git diff --check -``` - -- `trajectory-observation`:退出码 0,输出 `PASS trajectory-observation` -- 执行器审计:无匹配(命令无输出,退出码 1) -- `git diff --check`:退出码 0,仅仓库既有 LF/CRLF 提示,无新空白错误 - -## 执行器审计与清理 -MovementTest/observer 运行时源未出现任何指定执行器写调用。 -实现提交仅显式暂存上述 3 个白名单文件;提交后 `git diff --cached --name-only` 为空。 -未执行 `git reset --hard`、`git checkout --`、`git clean`、`git add .` 或 `git add -A`。 - -## 保留的脏工作区 -入口时已存在的 `.gitignore`、`ClumsyPilot.csproj`、`EmPlannerConfiguration.cs`、PathSmoothing/CoarsePath 修改或删除、未跟踪目录及报告等全部保留,本阶段未触碰。 - -## 已知警告与偏差 -- 构建输出保留两个既有 obsolete 警告:`Lidar2dDetect2LegTray.LegWidth` 与 `MultiWheelChassis.GetSteerWheels()`。 -- `git diff --check` 持续输出仓库既有 LF/CRLF 提示,不影响本阶段空白检查结论。 -- 本阶段未做真实 Painter 窗口截图或浏览器人工验证;自动化验证为源码/模型检查。 - -## 未运行项 -- 未进入 Phase 09 文档、全量自动化、本地 Web smoke 或实车验收。 -- 未运行 `em-core-all`、`em-all`、Node DOM 测试或可视化验证宿主。 -- 未重跑 Phase 07 Web 验证;本阶段未修改 Web 文件。 - -## Phase 09 事实 -- 下一阶段:Phase 09,对应主计划 Task 11、分阶段执行计划 Task 9。 -- 允许修改:MovementTest/EMPlanner/Web 三个 README;创建 `docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md`;实车验收通过后才创建 `phase-09-vehicle.md`。 -- Phase 09 自动验收命令包括 Node DOM 测试、可视化宿主、`trajectory-observation`、`em-all`、完整 build、执行器审计和 `git diff --check`。 -- 无监督安全环境时,Phase 09 自动窗口状态写 `自动验收完成但实车待验`,不得声称整体完成。 -- 本 handoff 提交的真实哈希由 Phase 09 调用方在最终回答中提供。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md deleted file mode 100644 index abb23f5..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md +++ /dev/null @@ -1,51 +0,0 @@ -# EM 完整方向段可视化修复 - Phase 09 实车续验记录 - -## 状态 - -未运行(实车待验,2026-08-07)。 - -本次续验窗口未找到可确认的有监督安全车辆环境,因此主计划 Task 11 Step 4 的八项实车清单全部记录为未运行;自动化、DOM 或合成 smoke 证据不作为实车验收。 - -## 入口与祖先检查 - -- 分支:`trajplanner` -- 入口 HEAD:`b76de20fafe1d54723d4d1271e0b14b5900c95bd` -- 文档实现提交:`88a66b8b2a1a4b548d5733d21e94f2fad73df1fe` -- `git merge-base --is-ancestor 88a66b8b2a1a4b548d5733d21e94f2fad73df1fe HEAD`:通过 -- `git merge-base --is-ancestor b76de20fafe1d54723d4d1271e0b14b5900c95bd HEAD`:通过 -- 工作区既有脏修改与未跟踪内容全部保留,未修改规划器、MovementTest、Web、Painter 或任何实现文件。 - -## 自动化复核证据 - -入口工作区存在既有相关代码修改,因此按 phase-09 记录重跑受影响自动化: - -| 命令 | 结果 | -| --- | --- | -| `npm ci --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom` | 39 个包安装成功 | -| `npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom` | 11/11 通过 | -| `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` | `PASS trajectory-planning-visualization` | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation` | `PASS trajectory-observation`,仅保留两个既有 obsolete 警告 | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all` | 20 个组件全部 `PASS` | -| `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true` | 0 警告、0 错误 | -| `rg -n "SendXYThSpeed\|SendMotion\|DriveStop\|PredefinedDriveStop\|AccumulateSpeed\|SetGear\|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest` | 无匹配(退出码 1) | -| `git diff --check` | 退出码 0,仅仓库既有 LF/CRLF 提示 | -| 本地 smoke `--smoke-seconds 60` | 退出码 0,输出 token URL;结束后 `2192` 与 `1403` 均无监听 | - -本地 smoke 仅使用合成快照,作为当前工作区回归证据,不作为实车验收。 - -## 实车清单 - -未提供可确认的有监督安全车辆环境:没有受监督安全区域确认,没有运行中的 MovementTest/底盘会话,没有实车日志或截图可引用,也未声明人工监督条件。按安全边界不启动实车观察,也不通过操作真实底盘促成观察条件。 - -1. Web 开启、Native Painter 默认关闭、UI/控制台/页面显示 `OBSERVE_ONLY`:未运行 -2. 当前完整方向段产生完整 LS/ST;静态起步加速、达到可行巡航速度、按包络开始制动并在真实边界停止:未运行 -3. ST `PathS` 递增;jerk 为 `N-1` 个区间且无终点后继或假 `JerkLimitExceeded`:未运行 -4. 换向展示 `DeltaPosition`、同方向 `DeltaReferenceS`、`DeltaV`、`DeltaA`,不做无关局部 `PathS` 直接比较:未运行 -5. 真换向时 N 保持高亮至连续停车保持和三个带符号速度样本通过,等待文案明确:未运行 -6. 粗路径、Local G2、当前轨迹、车辆位姿、`s_end`、换向点和最终目标视觉可区分:未运行 -7. 关闭或慢加载浏览器不影响观察:未运行 -8. 停止 MovementTest 释放 HTTP/SSE、端口和 Painter;源码/日志审计无硬件写:未运行 - -## 结论 - -Phase 09 整体验收不能视为完成。下一次续验窗口必须在可确认的有监督安全车辆环境中按八项清单执行;全部通过后才写入“完成”并提交 `docs: record EM visualization vehicle acceptance`,出现真实回归才写入“阻塞”。 diff --git a/docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md b/docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md deleted file mode 100644 index 0603989..0000000 --- a/docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md +++ /dev/null @@ -1,83 +0,0 @@ -# EM 完整方向段可视化修复 - Phase 09 交接 - -## 状态 - -自动验收完成但实车待验(2026-08-07)。本窗口完成了主计划 Task 11 和分阶段执行计划 Task 9 的文档、全量自动化与本地确定性 smoke;受监督实车清单未执行,`phase-09-vehicle.md` 未创建,整体验收不能视为完成。 - -## 目标与非目标 - -- 更新 MovementTest、EMPlanner、Web 三份 README,记录 full/rolling 开关、`s_end/T_end` 来源、速度与终端容差、图表语义、缩放、停止清理和 `OBSERVE_ONLY` 边界。 -- 提交三份 README 后运行 Node DOM、可视化宿主、`trajectory-observation`、`em-all`、完整 build、执行器审计、`git diff --check` 和 60 秒本地 smoke。 -- 未修改 Web/Painter 实现,未扩展规划功能,未进入 Phase 08 之前实现,未派生子智能体,未重新 brainstorming。 -- 未执行实车观察;未把自动化或合成 smoke 证据写成实车验收。 - -## 入口事实 - -- 分支:`trajplanner` -- 入口 HEAD:`26b6246360dd2ac7e3d54ba57207a753b9bc70a7` -- 上一阶段实现提交:`37c27bdb1b41502ad23783a041a42512b3a49648` -- 上一阶段交接提交:`26b6246360dd2ac7e3d54ba57207a753b9bc70a7` -- 祖先检查:`37c27bd` 与 `26b6246` 均为入口 HEAD 的可追溯祖先 -- 本阶段文档实现提交:`88a66b8b2a1a4b548d5733d21e94f2fad73df1fe`(`docs: explain full-direction EM observation`) - -## 修改文件 - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` -- `ClumsyPilot/TrajectoryPlanningVisualization/README.md` - -## 文档事实 - -- `UseFullDirectionSegmentPlanning=true` 是 MovementTest 默认值,映射到 `EmPlanningScope.FullDirectionSegment`;每个活动方向段只发起一次冻结优化,随后重复发布同一轨迹与新鲜观察状态。 -- `DistanceHorizonMeters` 与 `TimeHorizonSeconds` 只用于 `RollingHorizon` 截断;full 模式保留兼容字段但不使用它们截断。 -- `s_end` 来自实际 Local G2 `PathS`;`T_end` 由可行加速度、巡航、jerk 限速停止和零速 hold 推导。 -- 前进期望/硬上限为 `1.0 m/s`,倒车期望/硬上限为 `0.5 m/s`。 -- 真实边界成功需要终端停止、世界位置误差不超过 `0.03 m`、归一化 yaw 误差不超过 `5 deg`(`5 * PI / 180 rad`)。 -- Web 是主观察界面;Native Painter 仅作为可选正确性/审计输出。 -- README 记录了各图轴/单位、jerk `N-1` 规则、语义图层、边界标记和视口缩放;缩放只改变浏览器本地视口,不修改快照或向规划器发送参数。 -- 关闭或断开浏览器不会停止观察;MovementTest 停止会回收 HTTP/SSE、端口和可选 Painter。 -- MovementTest 不写底盘、转向、制动、电机或挡位命令,全程保持 `OBSERVE_ONLY`。 - -## 自动化证据 - -| 命令 | 结果 | -| --- | --- | -| `npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom` | 11/11 通过 | -| `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/...` | `PASS trajectory-planning-visualization` | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/... -- trajectory-observation` | `PASS trajectory-observation`,仅两个既有 obsolete 警告 | -| `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/... -- em-all` | 20 个组件全部 `PASS` | -| `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true` | 0 警告、0 错误 | -| `rg -n "SendXYThSpeed\|SendMotion\|DriveStop\|PredefinedDriveStop\|AccumulateSpeed\|SetGear\|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest` | 无匹配,退出码 1 | -| `git diff --check` | 退出码 0,无新空白错误;仅仓库既有 LF/CRLF 提示 | -| 本地 smoke `--smoke-seconds 60` | 退出码 0,输出带 token 的 `http://127.0.0.1:1403/...`;结束后 `1403` 无监听 | - -测试依赖 `jsdom` 使用 `npm ci` 按锁文件安装,运行后已删除生成的 `node_modules`;`package.json` 与 `package-lock.json` 未改动。 - -## 警告与已知偏差 - -- MovementTest UI 字段 `EnableNativePainterVisualization` 当前默认值为 `true`,而 `TrajectoryObservationSettings` 契约默认值为 `false`。文档已如实记录,并要求受监督车辆清单在实车会话前显式设为 `false`,除非另行审查。 -- MovementTest UI 字段 `TimeHorizonSeconds` 当前默认值为 `20.0`,而设置契约默认值为 `2.0`;full 模式推导 `T_end`,该值仅保留为滚动兼容输入。 -- 未进行人工浏览器截图或真实 Painter 窗口检查;自动化证据来自 DOM 测试、C# 契约宿主和确定性 smoke 宿主。 -- `trajectory-observation` 保留两个既有 obsolete 警告(`Lidar2dDetect2LegTray.LegWidth`、`MultiWheelChassis.GetSteerWheels()`),非本阶段引入。 - -## 保留的脏工作区与暂存内容 - -入口时已有的 `.gitignore`、`ClumsyPilot.csproj`、`EmPlannerConfiguration.cs`、PathSmoothing/CoarsePath 相关修改或删除、未跟踪目录及报告等全部保留,未回退或覆盖。本阶段未修改这些内容;提交 README 后暂存区为空。除本次 `phase-09.md` 外没有新增应跟踪文件。 - -## 未运行项与原因 - -- 受监督实车清单未运行:当前无可确认的受监督安全车辆环境。 -- `phase-09-vehicle.md` 未创建:设计明确禁止用自动化或合成证据替代实车验收。 -- 未进行人工浏览器/截图检查:smoke 仅作为确定性本机宿主证据。 - -## Phase 09 实车续验事实 - -下一窗口是 Phase 09 的实车续验窗口,不是新阶段: - -- 仓库根目录:`D:\Users\Desktop\项目\prakrobot\ParkingRobot` -- 期望分支:`trajplanner` -- 启动时核对 `88a66b8b2a1a4b548d5733d21e94f2fad73df1fe` 与 Phase 09 handoff 提交均为祖先。 -- 必读:本 handoff、主计划 Task 11 Step 4-6、分阶段执行计划 Task 9 Step 5-6;如相关代码在 handoff 后变化,先重跑受影响的自动化。 -- 只允许创建 `docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md`,不得修改规划、Web、Painter、MovementTest 或 README。 -- 按主计划八项清单执行;MovementTest 必须保持 `OBSERVE_ONLY`,不得通过操作真实底盘促成观察条件。 -- 全部通过时状态写“完成”,提交消息 `docs: record EM visualization vehicle acceptance`;出现真实回归时状态写“阻塞”,提交消息 `docs: record blocked EM visualization vehicle acceptance`,并只输出同阶段恢复提示词。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-01.md b/docs/superpowers/handoffs/em-observation-web/phase-01.md deleted file mode 100644 index 6b2cfe6..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-01.md +++ /dev/null @@ -1,58 +0,0 @@ -# EM 观察网页可视化阶段 01 交接 - -状态:完成 - -阶段目标:完成可复用 `netstandard2.0` 可视化类库的基础:不可变通用契约、防御性复制、紧凑占用 bitset、容量一帧交换、有界周期历史、已验证选项快照和稳定 JSON;不包含 HTTP/SSE、网页、EM Adapter 或 MovementTest 集成。 - -基线提交:`fa4938b3fc60620bee2c1694e232a5c2b35ca392`(`b0b79e5` 已确认是祖先)。 - -实现提交: - -- `55d8e1eebf592b03caff28dfee28745959e41e09` — `feat: add planning visualization contracts` -- `ab8e4010f8d42dd8ddcd6066b82eaba7c163fc28` — `feat: add bounded visualization snapshots` - -修改文件: - -- `ClumsyPilot/ClumsyPilot.csproj` -- `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` -- `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs` - -新增或确认的接口: - -- 不可变契约:`VisualizationPoint`、`VisualizationPose`、`VisualizationBounds`、`VisualizationValue`、`VisualizationConfigurationGroup`、`VisualizationPolyline`、`VisualizationMarker`、`VisualizationDirectionSegment`、`VisualizationOccupancyGrid`、`VisualizationSeries`、`VisualizationChart`、`PlanningVisualizationStaticSnapshot`、`PlanningVisualizationDynamicSnapshot` 和 `VisualizationCycleSummary`。所有携带集合的构造函数拒绝 null 元素并复制为 `ReadOnlyCollection`;数值字段拒绝 NaN/Infinity。 -- `VisualizationOccupancyGrid` 严格验证 `ceil(rows * columns / 8)` 字节长度,防御性复制 row-major、LSB-first bitset,并只暴露 `OccupancyBitsBase64`。 -- `PlanningVisualizationOptions.CreateValidatedSnapshot()` 返回不可变 `PlanningVisualizationOptionsSnapshot`;默认端口 0、刷新 10 Hz、历史 60、固定最多 2 客户端。 -- `LatestVisualizationFrameStore.Publish` 使用 `Interlocked.Exchange` 发布最新一帧;`TryReadAfter` 只返回较新 store version。 -- `BoundedCycleHistory` 有界、按 `CycleVersion` 去重,且 `Snapshot()` 返回新只读副本;`VisualizationJson.Serialize` 以不变文化、ISO 日期、紧凑 camelCase 输出。 - -验证命令与结果: - -1. `git merge-base --is-ancestor b0b79e5 HEAD` — 退出码 0。 -2. `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true`(实现前基线)— 退出码 0;2 条既有弃用警告。 -3. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`(Task 1 RED)— 退出码 1;首个有效错误为缺少 `TrajectoryPlanningVisualization` 命名空间/契约类型。 -4. 同一宿主(Task 1 GREEN)— 退出码 0,输出 `PASS trajectory-planning-visualization`;随后主项目构建退出码 0。 -5. 同一宿主(Task 2 RED)— 退出码 1;首个有效错误为缺少 `LatestVisualizationFrameStore`,其余运行时类型同样缺失。 -6. 同一宿主连续两次(Task 2 GREEN)— 两次退出码均为 0,均输出 `PASS trajectory-planning-visualization`。 -7. `dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` — 退出码 0。 -8. 阶段退出:`dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`、`dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj`、`dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true` — 全部退出码 0;宿主输出 PASS,类库构建 0 警告,主项目构建保留 2 条既有弃用警告。 -9. `git diff --check -- <阶段文件路径>` 和 `git diff --check 55d8e1e^..ab8e401 -- <阶段文件路径>` — 均退出码 0;已用 `git diff-tree --no-commit-id --name-status -r` 分别核对两个实现提交的实际文件树。 - -未运行的验证及原因:无。 - -已知警告或遗留问题:主项目构建持续报告两条实现前已存在的弃用警告:`MovementTests.TireFollowing.cs(70)` 的 `Lidar2dDetect2LegTray.LegWidth` 与 `TireFollowing.cs(243)` 的 `MultiWheelChassis.GetSteerWheels()`;与本阶段无关。 - -与原计划的偏差:为使 Task 1 的验证宿主能以“契约类型不存在”而非“被引用项目不存在”进入 RED,在 RED 前先创建了不含源码的最小类库项目文件;契约与项目依赖内容仍在 GREEN 阶段完成,未改变功能或文件范围。 - -下一阶段注意事项:阶段 2 只能实现原类库 Task 3 的 HTTP/SSE 和会话门面。先核对以上两项实现提交与本交接状态,并重新运行独立验证宿主;不得实现网页资源、EM/MovementTest 适配或任何执行器接口。`PlanningVisualizationOptionsSnapshot` 是应传入服务的冻结配置,动态帧完整轨迹只应从 `LatestVisualizationFrameStore` 获取,周期历史只能保存 `VisualizationCycleSummary`。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-02.md b/docs/superpowers/handoffs/em-observation-web/phase-02.md deleted file mode 100644 index c4c7221..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-02.md +++ /dev/null @@ -1,42 +0,0 @@ -# EM 观察网页可视化阶段 02 交接 - -状态:完成 - -阶段目标:完成独立 `netstandard2.0` 可视化类库的受限 Loopback HTTP/1.1 GET/SSE 传输层与幂等会话门面;不包含网页资源、EM Adapter 或 MovementTest 集成。 - -基线提交:`106201e87b6144fe7cbff80f2994a6539cf15b98`(阶段 01 交接;`b0b79e5` 已核对为祖先)。 - -实现提交:`fa1a266` — `feat: serve planning snapshots on loopback`。 - -修改文件: - -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs` - -新增或确认的接口: - -- `PlanningVisualizationSession.Start(PlanningVisualizationStaticSnapshot)` 返回含 `http://127.0.0.1:/?token=<64-lowercase-hex>` 的 `PlanningVisualizationSessionInfo`;`Publish`、`Stop`、`Dispose` 和重复 `Start`/`Stop` 均具备本阶段规定的幂等边界。 -- 服务器只以 `TcpListener(IPAddress.Loopback, port)` 监听;可用路由为带 token 的 `/api/bootstrap` 与 `/api/events`。请求仅接受 HTTP/1.1 `GET`,读取总时限为 2 秒、头部上限 16 KiB。 -- `/api/events` 最多两个客户端;每个客户端以容量一发送槽和 1 秒写超时隔离慢客户端。单 dispatcher 在无浏览器时仍收集有界周期摘要,且只在有 SSE 客户端时序列化帧。 -- `Stop` 最多提供 100 ms 发送 `event: end`(`会话已结束`),然后关闭 socket 和 listener;服务内部故障不从规划发布调用逸出。 - -验证命令与结果: - -1. `git merge-base --is-ancestor b0b79e5 HEAD` — 退出码 0。 -2. `git show --stat 55d8e1eebf592b03caff28dfee28745959e41e09` 与 `git show --stat ab8e4010f8d42dd8ddcd6066b82eaba7c163fc28` — 文件树与阶段 01 交接一致。 -3. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` — 退出码 0,输出 `PASS trajectory-planning-visualization`;提交后重新执行结果相同。 -4. `dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` — 退出码 0,0 警告、0 错误。 -5. 真实 `TcpClient` 检查已覆盖:403(缺 token)、404(授权未知路由)、405(POST)、400(HTTP/1.0 与超过 16 KiB 头)、503(第三 SSE 客户端)、UTF-8 `Content-Length`、256-bit token、慢 SSE 客户端下 10,000 次 `Publish` 在 1 秒内完成、无客户端时三条周期摘要、`event:end` 正常停止和端口重绑。 -6. `git diff --check fa1a266^ fa1a266 -- <阶段 02 transport 文件>` — 退出码 0;`git diff-tree --no-commit-id --name-status -r fa1a266` 仅列出上述六个 Task 3 文件。 - -未运行的验证及原因:无。 - -已知警告或遗留问题:工作区仍有 130 个阶段范围外脏项;启动时和提交过程中均未展开、暂存、重置或修改它们。本阶段库构建无警告。 - -与原计划的偏差:无。为使超限头测试在 Windows 上可靠收到 400,解析器在拒绝其 16 KiB 上限后,仍在同一 2 秒总读取预算内丢弃至 `\r\n\r\n`;超限请求不被解析或接受。 - -下一阶段注意事项:阶段 03 仅实现原类库 Task 4–5 的嵌入中文网页、独立验证、冒烟模式和 README。它应使用本阶段 `PlanningVisualizationSession` 的 `/api/bootstrap` 和 `/api/events`,并维持所有路由 token 鉴权;不得引入 EM、MDCS、Painter 或 MovementTest 类型。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-03.md b/docs/superpowers/handoffs/em-observation-web/phase-03.md deleted file mode 100644 index 0241396..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-03.md +++ /dev/null @@ -1,51 +0,0 @@ -# EM 观察网页可视化阶段 03 交接 - -状态:完成 - -阶段目标:完成独立 `netstandard2.0` 可视化类库的嵌入式中文科研风网页、程序集资源路由、确定性合成冒烟模式和独立类库文档;不引入 EM、MDCS、Painter 或 MovementTest 类型。 - -基线提交:`00501055eb924e8704def7bc7b419248a755ede9`(阶段 02 交接;`b0b79e5` 已核对为祖先)。 - -实现提交: - -- `1ad324ca64d1139d34b2f2848b71f4fd90d4dedb` — `feat: add scientific planning dashboard` -- `8181f0532fb60067287c4d21d25e43c90758a88a` — `docs: document planning visualization library` - -修改文件: - -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` -- `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` -- `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- `ClumsyPilot/TrajectoryPlanningVisualization/README.md` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` - -新增或确认的接口: - -- `EmbeddedWebAssets.ReadText(string)` 从程序集读取 `index.html`、`app.css`、`app.js`;静态资源不使用 CDN、Node.js 或网页运行时文件查找。 -- 受既有会话 token 保护的 `/?token=…`、`/app.css?token=…`、`/app.js?token=…`;`/api/bootstrap` 返回静态快照和有效刷新率,`/api/events` 保持既有 SSE 契约。 -- 页面固定页签 ID 为 `overview`、`ls-st`、`kinematics`、`history-config`,固定 chart ID 为 `ls`、`st`、`curvature-s`、`curvature-t`、`velocity-t`、`acceleration-t`、`jerk-t`、`yaw-rate-t`。 -- `SampleSnapshotFactory` 只使用通用可视化契约,生成两段方向、换向标记、冻结配置和八张图;`--smoke-seconds N` 以 10 Hz 发布 N 秒并打印一次完整 URI。 - -验证命令与结果: - -1. `git merge-base --is-ancestor b0b79e5 HEAD` — 退出码 0。 -2. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` — 退出码 0,输出 `PASS trajectory-planning-visualization`。 -3. `dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` — 退出码 0,0 警告,0 错误。 -4. `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true` — 退出码 0,0 错误;保留两个既有 obsolete 警告,位于 `MovementTests.TireFollowing.cs` 和 `TireFollowing.cs`,均不在本阶段文件范围。 -5. `Resolve-Path` 后从 `$env:TEMP` 执行验证宿主 — 退出码 0,输出 `PASS trajectory-planning-visualization`,证明网页资源来自程序集而非当前工作目录。 -6. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 1` — 退出码 0,恰输出一条 `http://127.0.0.1:/?token=<64-lowercase-hex>` URI 后退出。 -7. `git diff --check 0050105..HEAD -- <阶段 03 文件>` — 退出码 0,无空白错误;`git diff-tree --no-commit-id --name-status -r 1ad324c` 仅列出 Task 4 的九个文件,`git diff-tree --no-commit-id --name-status -r 8181f05` 仅列出 Task 5 的两个文件。 - -未运行的验证及原因:无。 - -已知警告或遗留问题:启动时工作区有 130 项阶段范围外脏项,暂存区为空;本阶段未展开、暂存、重置、覆盖或提交它们。主项目构建的两个 obsolete 警告同样不属于本阶段。 - -与原计划的偏差:`WebAssetChecks.cs` 在基线中不存在,虽在 Task 4 文件清单标为 Modify,实际按该任务的“Create WebAssetChecks”步骤新建;其余实现范围和两个独立提交均按 Task 4–5 执行。 - -下一阶段注意事项:阶段 04 只实施集成计划 Task 1–2 的设置同步与纯换向段状态机。可复用类库已经完成,但不得向其引入 EM、MDCS、Painter 或 MovementTest 类型;继续保持 `OBSERVE_ONLY`,并保护 MovementTest 中用户已经设置的 5.0 s、100000、2.0 s。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-04.md b/docs/superpowers/handoffs/em-observation-web/phase-04.md deleted file mode 100644 index 3b4300c..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-04.md +++ /dev/null @@ -1,44 +0,0 @@ -# EM 观察网页可视化阶段 04 交接 - -状态:完成 - -阶段目标:完成集成计划 Task 1–2 的观察参数同步、网页/Painter 冻结开关,以及纯顺序方向段换向确认状态机;不实现活动段控制器、图表适配、网页托管或插件发布。 - -基线提交:`0bc8df9d5ecabd259dbd4d5db8594e590fbd0012`(阶段 03 交接)。`b0b79e5` 已核对为祖先。 - -实现提交: - -- `c5ea4f6` `feat: configure observation visualization` -- `0a9c34d` `feat: track observed direction segments` - -修改文件: - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -新增或确认的接口: - -- `TrajectoryObservationSettings` 的默认求解器超时、OSQP 迭代和 ST 时域分别冻结为 `5.0 s`、`100000`、`2.0 s`;新增网页/Painter、端口、10 Hz、60 轮历史及换向确认参数,端口仅允许 `0` 或 `1024..65535`。 -- MovementTest 的公共字段与上述设置一对一复制,然后通过 `CreateValidatedSnapshot()` 冻结;网页与 Painter 默认关闭,网页自动打开默认开启。 -- `TrajectoryObservationSegmentTracker`、`TrajectoryObservationSegmentState`、`TrajectoryObservationSegmentUpdate` 和 `TrajectoryObservationSegmentPhase` 为纯状态机接口。它仅按 `N -> N+1` 推进,并要求匹配的已到绝对末点 GearSwitch 轨迹、双段连接点投影、连续真实停车保持和三个严格递增的同向速度样本。错误方向、零速、重复序列、投影超限、非 GearSwitch 与时间倒退均重置确认;末段进入 `Completed`。 - -验证命令与结果: - -1. `git merge-base --is-ancestor b0b79e5 HEAD`:退出码 `0`。 -2. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`:退出码 `0`,输出 `PASS trajectory-observation`。Task 2 的 GREEN 连续运行两次均通过;最终退出验证再次通过。 -3. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`:退出码 `0`,输出 `PASS trajectory-planning-visualization`。 -4. `rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest`:退出码 `1`,无匹配;观察运行源码没有这些接口调用。`TrajectoryObservationChecks.cs` 中存在同名字符串,仅用于读取运行时源文件并执行禁止调用审计,不是运行时调用。 -5. `git diff --check c5ea4f6^..0a9c34d -- <阶段 04 文件>`:退出码 `0`,无空白错误。`git diff-tree --no-commit-id --name-status -r c5ea4f6` 仅列出 Task 1 的五个文件;同命令对 `0a9c34d` 仅列出 Task 2 的三个文件。 - -未运行的验证及原因:无。 - -已知警告或遗留问题:主项目构建保留两个既有 obsolete 警告,位于 `MovementTests.TireFollowing.cs` 与 `TireFollowing.cs`,均不属于本阶段文件。启动时工作区有 130 项脏变更、暂存区为空;本阶段只精确暂存和提交了 Task 1–2 文件,交接前其余 129 项仍未展开、重置或提交。 - -与原计划的偏差:首次基线构建因本次命令遗留的 MSBuild 节点短暂锁定 `TrajectoryPlanningVisualization.dll` 而失败(CS2012)。确认 PID 的启动时间和命令行后仅终止该本次失败命令的孤立节点,随后原样重跑两个基线入口并通过;没有代码偏差。 - -下一阶段注意事项:阶段 05 仅实施集成计划 Task 3。必须使用本阶段 tracker 的不可变状态作为唯一活动方向段来源;同段保留当前协调器轨迹作为 seed,跨方向时更换协调器且新段第一次请求的 `PreviousTrajectory` 必须为 `null`。继续保持 `OBSERVE_ONLY`,不得引入执行器写调用。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-05.md b/docs/superpowers/handoffs/em-observation-web/phase-05.md deleted file mode 100644 index bb5d5de..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-05.md +++ /dev/null @@ -1,43 +0,0 @@ -# EM 观察网页可视化阶段 05 交接 - -状态:完成 - -阶段目标:完成集成计划 Task 3 的活动方向段滚动规划控制器和异步观察循环;保持 `OBSERVE_ONLY`,不实现静态/动态图表 Adapter、网页生命周期或插件发布。 - -基线提交:`e141a76fdbef33f20821d6b69cad684d438eef43`(阶段 04 交接)。`b0b79e5` 已核对为祖先。 - -实现提交: - -- `51f744b` `feat: observe EM planning across gear segments` - -修改文件: - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -新增或确认的接口: - -- `TrajectoryObservationController.ActiveSegment`、`SegmentState` 和 `PreviousTrajectoryForVisualization` 由 tracker 的不可变状态统一驱动;`TryAdvanceSegment(...)` 仅在 tracker 确认 `N -> N+1` 后替换 coordinator/executor。 -- 同段请求使用当前 coordinator 的 `PublishedTrajectory` 作为 previous seed;确认换向时旧轨迹只保留在 `PreviousTrajectoryForVisualization`,新 coordinator 首次请求的 `PreviousTrajectory` 为 `null`。 -- 会话 trajectory ID 在 coordinator 替换后继续单调增长;`CreateEffectiveConfigurationSnapshot()` 返回 `configuration.Copy()`。 -- 等待换向时 executor 使用当前确认方向、下一段期望方向和 `directionConfirmed=false`;没有伪造档位读数。 -- `TrajectoryObservationLoopTick` 暴露 `SegmentAdvanced` 与 tracker 的不可变 `SegmentState`。loop 仅在已消费完成任务且无飞行任务时调用 `TryAdvanceSegment`,不会为切段取消规划。 - -验证命令与结果: - -1. `git merge-base --is-ancestor b0b79e5 HEAD`:退出码 `0`。 -2. RED:`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`:退出码 `1`,首个断言为 `active observation segment is not hardcoded to zero was false`,准确捕获既有 `segmentIndex = 0`。 -3. GREEN 与最终回归:`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`:退出码 `0`,输出 `PASS trajectory-observation`。 -4. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator`:退出码 `0`,输出 `PASS coordinator`。 -5. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor`:退出码 `0`,输出 `PASS executor`。 -6. `rg -n "segmentIndex = 0|SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest`:退出码 `1`,无匹配;运行源码既无硬编码活动段,也无执行器写调用。 -7. `git diff --check 51f744b^..51f744b -- `:退出码 `0`;`git diff-tree --no-commit-id --name-status -r 51f744b` 只列出这三个 Task 3 文件。 - -未运行的验证及原因:无。 - -已知警告或遗留问题:退出验证中的主项目编译保留两条既有 obsolete 警告,位于 `MovementTests.TireFollowing.cs` 的 `Lidar2dDetect2LegTray.LegWidth` 和 `TireFollowing.cs` 的 `MultiWheelChassis.GetSteerWheels()`;均不属于本阶段文件。启动时 `trajectory-observation` 首次构建曾因 `TrajectoryPlanningVisualization.dll` 的一次性写锁报 CS2012(退出码 `1`);经进程与目标检查后单独重跑同一入口通过,未修改源码处理该环境锁。实现提交后仍有 129 项无关工作区脏项,暂存区为空,均未展开、重置或提交。 - -与原计划的偏差:无代码范围偏差;Task 3 的三文件实现提交保持独立。阶段文件按阶段协议单独提交。 - -下一阶段注意事项:阶段 06 只实现集成计划 Task 4–5 的静态/动态快照、图表和 handoff 指标。它必须消费 `ActiveSegment`、`SegmentState`、`PublishedTrajectory` 与 `PreviousTrajectoryForVisualization`,不能恢复跨方向 previous seed,也不能改变 `OBSERVE_ONLY` 或引入网页生命周期。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-06.md b/docs/superpowers/handoffs/em-observation-web/phase-06.md deleted file mode 100644 index 01d9b91..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-06.md +++ /dev/null @@ -1,46 +0,0 @@ -# EM 观察网页可视化阶段 06 交接 - -状态:完成 - -阶段目标:完成集成计划 Task 4–5 的 EM 静态/动态数据可视化适配,在不启动网页会话、不改变 Painter 生命周期或插件发布的前提下,为冻结配置、地图/路径、运动学图表和周期交接提供可验证的不可变快照。 - -基线提交:`a7dde0f5e4c37438f775b706e3886ace00fe0e04`;启动检查确认 `b0b79e5` 是祖先,分支为 `trajplanner`。 - -实现提交: - -- `0a1cbe9` `feat: export EM observation configuration` -- `a7dde0f` `feat: visualize EM rolling kinematics` -- `cc6b0f1` `fix: complete EM observation visualization contracts` - -修改文件: - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -新增或确认的接口: - -- `TrajectoryObservationStaticSnapshotBuilder.Build(...)` 按固定十组顺序导出冻结生效配置、车辆/地图和网页/换向确认字段;占据图为 row-major、LSB-first 紧凑 bitset,静态数据包含粗路径、完整 Local G2、方向段与换向标记。 -- `TrajectoryObservationKinematicChartBuilder.Build(...)` 固定导出 `ls`、`st`、`curvature-s`、`curvature-t`、`velocity-t`、`acceleration-t`、`jerk-t`、`yaw-rate-t`;jerk 仅有 `N-1` 个真实区间,LS 使用完整活动段的共享 `ReferenceS`,ST 保留局部 `PathS`。 -- `TrajectoryObservationHandoffAnalyzer.Analyze(...)` 使用 `EffectiveAtUtc + TimeFromStart` 采样,保留世界位置、速度、加速度差;仅在任一投影失败时使 `DeltaReferenceSMeters` 不可用,不比较独立局部 `PathS`。 -- `TrajectoryObservationDynamicSnapshotBuilder.Build(...)` 产生当前/上一轨迹、活动段/当前视界、八图、滚动或精确停车中文语义、真实终端 `v/a`、换向确认和交接状态;`VisualizationCycleSummary` 不含完整点数组。 - -验证命令与结果: - -1. RED:`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`,退出码 `1`;首个有效断言为 `static configuration group count expected 10 but was 4`,证明原验证仅检查类型存在且静态导出未满足契约。 -2. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`,退出码 `0`,输出 `PASS trajectory-observation`。 -3. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all`,退出码 `0`,输出 `PASS longitudinal-model`、`PASS longitudinal-integration`、`PASS trajectory`、`PASS em-planning-service`。 -4. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`,退出码 `0`,输出 `PASS trajectory-planning-visualization`。 -5. `rg -n "rolling publishes the two-second ST knot count|cycle 1 has nonzero terminal speed|jerk has N-1 intervals" ...` 确认 rolling service 的 21 knot、非零末速断言和本阶段 jerk 20 区间断言均已纳入通过的入口。 -6. `git diff --check 0a1cbe9^..cc6b0f1 -- `,退出码 `0`;已核对 `0a1cbe9`、`a7dde0f`、`cc6b0f1` 的精确文件树。 - -未运行的验证及原因:无。 - -已知警告或遗留问题:主项目构建仍报告两条既有 obsolete 警告,位于 `MovementTests.TireFollowing.cs` 的 `Lidar2dDetect2LegTray.LegWidth` 和 `TireFollowing.cs` 的 `MultiWheelChassis.GetSteerWheels()`,均不属于本阶段文件。一次并发启动三个验证宿主触发共享 DLL 写锁(CS2012/MSB3026);根因是并发构建,随后按串行方式重跑三条入口均通过,未修改代码规避该环境锁。 - -与原计划的偏差:原 Task 4–5 实现提交已存在但验证仅检查适配器类型可构造。本阶段以 TDD 增补真实行为断言并用 `cc6b0f1` 精确修正既有阶段文件;未扩大到 Task 6–7,也未启动网页会话、修改 Painter 生命周期或插件发布。 - -下一阶段注意事项:阶段 07 只执行集成计划 Task 6–7。应消费本阶段静态/动态快照构建器,添加网页发布熔断与可选 Painter 生命周期、插件 DLL 打包和操作文档;继续保持 `OBSERVE_ONLY`,不得把观察循环变为控制输出。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-07.md b/docs/superpowers/handoffs/em-observation-web/phase-07.md deleted file mode 100644 index 47ce6cc..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-07.md +++ /dev/null @@ -1,52 +0,0 @@ -# EM 观察网页可视化阶段 07 交接 - -状态:完成 - -阶段目标:完成集成计划 Task 6–7:MovementTest 的回环网页会话生命周期、一次性故障隔离、可选原生 Painter、插件 DLL 原子打包和操作文档;保持 `OBSERVE_ONLY`,不进行实车验收。 - -基线提交:`819ff7324b4242ba9fad158618b49a49d76e93f2`(启动时分支 `trajplanner`,批准设计 `b0b79e5` 与 phase-06 实现提交均为祖先)。 - -实现提交: - -- `fbd3ba6` `feat: host EM observation dashboard` -- `6034568` `docs: package EM observation dashboard` - -修改文件: - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- `ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1` -- `ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs` -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` - -新增或确认的接口: - -- 内部 `ITrajectoryObservationVisualizationSink` 提供 `Start`、`Publish`、`Stop`;生产 sink 包装一个 `PlanningVisualizationSession`,验证宿主注入记录/抛错 sink,不模拟网页类库。 -- `TrajectoryObservationVisualizationPublisher` 仅在启用且到达 `WebRefreshRateHz` 门控点时构建动态快照;第一次网页快照、服务或适配器异常会停止 sink、仅记录一次中文原因并永久熔断本会话网页输出,观察循环继续。 -- `EnableWebVisualization=false` 不创建网页静态/动态快照或历史,也不启动服务;成功 bootstrap 后才建立静态快照、记录完整 token URI,并把浏览器启动失败与服务生命周期隔离。 -- 原生 Painter 不再静态预创建;只在 `EnableNativePainterVisualization=true` 的会话内创建。`TestStop()`、会话替换和当前会话故障会幂等停止网页、释放端口并清理既有 Painter。 -- 实际 tick 使用 `controller.ActiveSegment`、当前/上一轨迹和当前循环信息创建网页快照;不在观察循环等待网页序列化或客户端。 -- 发布脚本只从显式 `ManagedDll` 同级目录解析 `TrajectoryPlanningVisualization.dll`,先将其复制进 staging,再保留既有 root 防护、OSQP hash、原子交换与恢复逻辑。插件精确树现为 `ClumsyPilot.dll`、`TrajectoryPlanningVisualization.dll`、`osqp.dll` 和三份 OSQP 许可证。 - -验证命令与结果: - -1. RED:`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`,退出 `1`;首个有效错误为缺少 `ITrajectoryObservationVisualizationSink`,确认新 seam 尚未实现。 -2. GREEN:同一 `trajectory-observation` 命令,退出 `0`,输出 `PASS trajectory-observation`;验证 throwing sink 单次熔断、20 Hz tick 下 10 Hz 最多 11 帧、禁用网页不建快照、Painter 源码惰性创建。 -3. Task 7 RED:`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package`,退出 `1`;精确包树缺少 `plugins/TrajectoryPlanningVisualization.dll`。 -4. 最终 `trajectory-observation`,退出 `0`,输出 `PASS trajectory-observation`。 -5. 最终 `plugin-package` 连续两次均退出 `0`,均输出 `PASS plugin-package`;验证精确包树、程序集名 `TrajectoryPlanningVisualization` 和无 staging/backup 残留。 -6. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`,退出 `0`,输出 `PASS trajectory-planning-visualization`。 -7. `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true`,退出 `0`,输出 `0 个警告 / 0 个错误`。 -8. `rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest` 无匹配,确认运行观察源码没有执行器调用。 -9. `git diff --check fbd3ba6^..6034568 -- `,退出 `0`;并以 `git diff-tree` 核对两个实现提交的精确文件树和插件输出契约。 - -未运行的验证及原因:未执行 Task 8 的 30 秒网页烟雾会话或任何实车观察/验收;本阶段授权范围严格限于 Task 6–7。 - -已知警告或遗留问题:`trajectory-observation` 编译时仍可见 phase-06 已记录的两条非本阶段 obsolete 警告:`MovementTests.TireFollowing.cs` 的 `Lidar2dDetect2LegTray.LegWidth` 与 `TireFollowing.cs` 的 `MultiWheelChassis.GetSteerWheels()`。Task 7 首次 GREEN 尝试曾在 staging `Directory.Move` 处遇到瞬时 Windows 拒绝访问;随后以同一脚本在独立临时目录连续两次原子交换成功,且最终 `plugin-package` 连续两次通过,未修改既有交换/恢复机制。 - -与原计划的偏差:无功能偏差。计划文件列出的 `TrajectoryObservationPresentation.cs` 无需修改:其 Painter 获取仍在实例字段初始化中,而实例本身已被 MovementTest 的会话级开关完全隔离,避免无关重写。 - -下一阶段注意事项:仅可在新窗口执行 Task 8 自动化与受监督实车观察;必须保持 `OBSERVE_ONLY`,不得把网页或轨迹输出接入任何车辆写接口。先运行完整自动化入口,再按清单记录网页烟雾和实车结果;若实车不可用,必须明确标为待验收。 diff --git a/docs/superpowers/handoffs/em-observation-web/phase-08.md b/docs/superpowers/handoffs/em-observation-web/phase-08.md deleted file mode 100644 index ab81bfe..0000000 --- a/docs/superpowers/handoffs/em-observation-web/phase-08.md +++ /dev/null @@ -1,41 +0,0 @@ -# EM 观察网页可视化阶段 08 交接 - -状态:自动验收完成但实车待验 - -阶段目标:完成集成计划 Task 8 的全量自动化、确定性本地网页烟雾和实车观察验收边界记录;不扩展功能,不修改运行时代码,并持续保持 `OBSERVE_ONLY`。 - -基线提交:`b45ec357b641ad3e855d76b1f675db2aeb155647`(分支 `trajplanner`;`b0b79e5` 已确认是祖先)。 - -实现提交:无。本阶段未发现回归,未作源码或测试修改。 - -修改文件: - -- `docs/superpowers/handoffs/em-observation-web/phase-08.md` - -新增或确认的接口:无新运行时接口。确认两个验证宿主、`em-all`、插件包验证、主项目构建和确定性网页烟雾入口仍可用。 - -验证命令与结果: - -1. 启动检查:phase-01.md 至 phase-07.md 全部存在且状态为“完成”;抽查并确认 `55d8e1e`、`ab8e401`、`fa1a266`、`1ad324c`、`8181f05`、`c5ea4f6`、`0a9c34d`、`51f744b`、`0a1cbe9`、`a7dde0f`、`cc6b0f1`、`fbd3ba6`、`6034568` 都可由 HEAD 到达。 -2. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`:退出 `0`,输出 `PASS trajectory-planning-visualization`。 -3. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`:退出 `0`,输出 `PASS trajectory-observation`。 -4. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all`:退出 `0`;所有组件均输出 `PASS`,包括 `plugin-package`、`trajectory-observation`、`rolling-end-to-end` 和 `rolling-execution-tail`。 -5. `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package`:退出 `0`,输出 `PASS plugin-package`。 -6. `dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true`:退出 `0`,输出 `0 个警告 / 0 个错误`。 -7. `git diff --check 55d8e1e^..6034568`:退出 `0`,本功能自 phase-01 起至 phase-07 实现收口范围内无空白错误。 -8. `dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 30`:确定性合成快照烟雾宿主退出 `0`;第二次会话输出 token URI `http://127.0.0.1:5465/?token=67b53e5a28508927b766e78923720a8a855a96803be41ff23c7b68d8be42e452`,自然退出后检查确认端口 `5465` 不再处于监听状态。 - -未运行的验证及原因: - -- 当前工具没有可用的浏览器控制连接(浏览器枚举为空)。已在 30 秒会话窗口中向操作者提供 token URI,但没有取得人工页面观察反馈;因此中文标题/状态、英文或数学坐标、科研细线白底、四页签、八图表、Canvas 栅格、SVG 路径覆盖、活动段/horizon 高亮、生效配置和过期状态的肉眼核对未记录为通过。独立验证宿主已通过嵌入资源、中文内容、Canvas/SVG、页签和图表契约检查,但这不替代本次肉眼结果。 -- 未执行任何实车观察。当前窗口没有可确认的“有人监督且环境安全”的实车条件,故未开启 MovementTest,未连接车辆写接口,也未尝试操作底盘。 -- 因此以下实车项待续验:网页开启且 Painter 关闭时的 `OBSERVE_ONLY`;rolling、approach、exact-stop 语义;jerk 的 `N-1` 区间与无伪造终点后区间;`DeltaPosition`、`DeltaReferenceS`、`DeltaV`、`DeltaA`;真实换向停车保持加三个带符号速度样本后的 `N -> N+1`;关闭浏览器后规划继续;停止 MovementTest 后网页端口/Painter 回收和全过程无执行器输出。 - -已知警告或遗留问题: - -- `trajectory-observation` 的编译输出仍出现两个既有且与本功能无关的 obsolete 警告:`MovementTests.TireFollowing.cs(70)` 的 `Lidar2dDetect2LegTray.LegWidth`,以及 `TireFollowing.cs(243)` 的 `MultiWheelChassis.GetSteerWheels()`。同一轮 `dotnet build` 最终输出为 `0 个警告 / 0 个错误`;未将前述既有警告归因于本功能。 -- 启动检查统计工作区有 129 项既有未暂存改动,暂存区为空;本阶段未展开、修改、暂存或提交它们。 - -与原计划的偏差:无功能或源码偏差。浏览器控制能力不可用时,按 Task 8 要求明确请求操作者协助,不伪造肉眼页面结果;该项保留给续验窗口。 - -下一阶段注意事项:仅在有人监督且环境安全时使用执行手册的“Stage 8 Vehicle Continuation Prompt”继续实车观察。先核对本交接的自动化证据;若 phase-08 后有相关代码变动,先重新运行受影响自动验证。不得通过操作真实底盘帮助观察条件成立,MovementTest 始终保持 `OBSERVE_ONLY`。 diff --git a/docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md b/docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md deleted file mode 100644 index a2781ee..0000000 --- a/docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md +++ /dev/null @@ -1,266 +0,0 @@ -# TrapMap Image Export and Console Logging Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add optional 300 DPI full-map PNG export with hard pixel/file limits and an independent switch for TrapMap terminal diagnostics. - -**Architecture:** A new `TrapMapImageExporter` renders the completed in-memory grid without depending on CycleGUI/Painter state. `TrapMapTest` owns the two manual switches and calls the exporter only after successful map construction. A shared `TrapMapLog` always writes `DLog` and conditionally mirrors the same message to `Console.WriteLine`. - -**Tech Stack:** C# 10, .NET Standard 2.0, internal pure-C# RGBA rasterizer, exact `StbImageWriteSharp` 1.16.7 managed PNG encoder, BCL-only PowerShell PNG parser, existing TrapMap tests. - -## Global Constraints - -- Do not inspect or modify `TrajPlanner`. -- Do not commit or stage any file. -- Keep `UI.GetPainter("TrapMapTest")` as the world-coordinate Painter; do not reintroduce `false`. -- Keep `UI.GetPainter("MultiWheelTwoLegDetect.Filter", false)` as the car-coordinate ROI Painter. -- Defaults: `_saveFullMapImage = true`, `_enableTerminalDebugLog = true`. -- PNG: 300 DPI, 4 pixels per cell, maximum edge 4000 pixels, maximum final size `50 * 1024 * 1024` bytes. -- Output: `\TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png`. -- Export failure never changes `TrapMapBuilder.Succeeded` or clears `GridMap`. -- Do not capture the Clumsy viewport or add point-cloud/motion behavior. - ---- - -### Task 1: Export contract, dependency, and pre-allocation limits - -**Files:** -- Modify: `ClumsyPilot/ClumsyPilot.csproj` -- Create: `ClumsyPilot/TrapMapImageExporter.cs` -- Create: `ClumsyPilot/tests/verify_trapmap_image.ps1` - -**Interfaces:** -- Consumes: `GridMapData`, `TrapMapVehiclePose`, workstation `Vector2`, tire metadata, output root. -- Produces: `TrapMapImageExportRequest`, `TrapMapImageExportResult`, and `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)`. - -- [ ] **Step 1: Add a failing image-export reflection test** - -Create `verify_trapmap_image.ps1`. Load `ClumsyPilot/bin/Debug/netstandard2.0/ClumsyPilot.dll`; require types `MultiWheelC.TrapMapImageExporter`, `TrapMapImageExportRequest`, and `TrapMapImageExportResult`, and assert the assembly/output no longer contains a platform drawing dependency. Create a temporary directory under `$env:TEMP`, invoke the disabled path with an output directory that does not exist, and assert `Saved=false`, `Skipped=true`, and that no directory was created. Invoke an oversized request using a `1000×1` grid at 50mm so the four-pixels-per-cell canvas plus padding exceeds 4000, and assert rejection before any PNG/temp file exists. - -Use these assertion helpers and cleanup guard: - -```powershell -function Assert-Equal($expected, $actual, [string]$message) { - if ($expected -ne $actual) { throw "$message Expected=$expected Actual=$actual" } -} -$testRoot = Join-Path $env:TEMP ("trapmap-image-test-" + [guid]::NewGuid().ToString('N')) -try { - # reflection setup and assertions -} finally { - if (Test-Path -LiteralPath $testRoot) { - Remove-Item -LiteralPath $testRoot -Recurse -Force - } -} -``` - -- [ ] **Step 2: Run RED verification** - -```powershell -dotnet restore ClumsyPilot\ClumsyPilot.csproj -dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 -``` - -Expected: compilation succeeds and the image script fails because `TrapMapImageExporter` is absent. - -- [ ] **Step 3: Add the managed PNG dependency** - -Add the exact managed encoder package inside the package `ItemGroup` in `ClumsyPilot.csproj`. Keep the target framework unchanged, expose the package path, and use one explicit build target that copies only its single managed `netstandard2.0` runtime asset. No drawing-runtime or native asset is required: - -```xml - - - - - - -``` - -Do not change the target framework. - -- [ ] **Step 4: Implement request/result types and dimension validation** - -Create `TrapMapImageExporter.cs` in namespace `MultiWheelC`. Use exact constants: - -```csharp -public const int PixelsPerCell = 4; -public const int MaximumImageEdgePixels = 4000; -public const long MaximumFileSizeBytes = 50L * 1024L * 1024L; -public const float OutputDpi = 300f; -public const int OuterPaddingPixels = 24; -public const int HeaderHeightPixels = 140; -``` - -Request properties must include `GridMap`, `VehiclePose`, `WorkstationWorld`, `TireLayerStatus`, `TireLayerMessage`, `InputSource`, and `OutputRootDirectory`. Result properties must include `Saved`, `Skipped`, `FilePath`, `Message`, `FileSizeBytes`, `PixelWidth`, and `PixelHeight`. - -`ExportIfEnabled(false, request)` returns a skipped result before validating the request or touching the filesystem. Enabled export validates non-null map/pose, finite workstation, then computes with `long`: - -```csharp -long pixelWidth = 2L * OuterPaddingPixels + (long)grid.Cols * PixelsPerCell; -long pixelHeight = HeaderHeightPixels + 2L * OuterPaddingPixels - + (long)grid.Rows * PixelsPerCell; -``` - -Reject non-positive or over-4000 dimensions before allocating the RGBA surface. Add `public static bool IsFileSizeAllowed(long byteCount)` returning `byteCount >= 0 && byteCount <= MaximumFileSizeBytes`; the actual save path must call this same function. - -- [ ] **Step 5: Run GREEN contract checks** - -Run the Task 1 commands. Expected: disabled/oversized assertions pass, no output directory exists for disabled export, and no RGBA buffer is allocated for oversized export. - -### Task 2: Complete 300 DPI PNG rendering and atomic 50MB save - -**Files:** -- Modify: `ClumsyPilot/TrapMapImageExporter.cs` -- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1` - -**Interfaces:** -- Consumes: validated Task 1 request. -- Produces: a complete PNG or a failure result with no final/temporary file. - -- [ ] **Step 1: Add failing PNG behavior assertions** - -Extend the test with a `2×2` 50mm grid, mark one known occupied cell, set a finite vehicle/workstation, and export twice. Assert: - -```powershell -Assert-Equal $true $result.Saved 'Small map must save.' -Assert-Equal $false $result.Skipped 'Enabled successful export is not skipped.' -if (-not (Test-Path -LiteralPath $result.FilePath)) { throw 'PNG file missing.' } -if ((Get-Item -LiteralPath $result.FilePath).Length -gt 50MB) { throw 'PNG exceeds 50MB.' } -if ([IO.Path]::GetExtension($result.FilePath) -ne '.png') { throw 'Output is not PNG.' } -if ((Split-Path $result.FilePath -Leaf) -notmatch '^TrapMap_\d{8}_\d{6}_\d{3}(_\d+)?\.png$') { - throw 'Timestamp filename is invalid.' -} -$image = Read-PngRgba $result.FilePath -Assert-Equal $result.PixelWidth $image.Width 'PNG width mismatch.' -Assert-Equal $result.PixelHeight $image.Height 'PNG height mismatch.' -Assert-Equal 11811 $image.PixelsPerMetreX 'PNG horizontal pHYs mismatch.' -Assert-Equal 11811 $image.PixelsPerMetreY 'PNG vertical pHYs mismatch.' -``` - -Assert the two file paths differ. Assert `IsFileSizeAllowed(50MB)` is true and `IsFileSizeAllowed(50MB + 1)` is false. Assert no `*.tmp` remains. - -- [ ] **Step 2: Run RED behavior test** - -Run the image test. Expected: it fails because enabled rendering/save is not implemented. - -- [ ] **Step 3: Render the full grid** - -After validation, create an internal RGBA8 surface and draw cells, grid, overlays, and 5×7 bitmap text with clipped integer primitives. Encode the buffer with `StbImageWriteSharp`, then insert a CRC-protected `pHYs` chunk containing `11811,11811,1` immediately after `IHDR`. - -Exact layout and colors: - -```text -Canvas background: White -Map top-left: (OuterPaddingPixels, HeaderHeightPixels + OuterPaddingPixels) -Unmarked cell interior: White -Grid lines: LightGray, 1px -Occupied cell interior: Red -Map border: Black, 2px -Vehicle outline/center/heading: Blue -Workstation circle/cross/text: LimeGreen -Header text: Black -``` - -For cell `(col,row)`, invert Y: - -```csharp -int imageCol = col; -int imageRow = grid.Rows - 1 - row; -int x = mapLeft + imageCol * PixelsPerCell; -int y = mapTop + imageRow * PixelsPerCell; -``` - -Fill occupied interiors before drawing all vertical/horizontal grid lines. Convert vehicle rectangle corners and workstation through a shared world-to-pixel helper using `(worldX - XMin) / ResolutionMm` for X and `((YMin + Rows * ResolutionMm) - worldY) / ResolutionMm` for Y. The Y expression uses the discrete raster's actual upper edge, so overlays stay aligned when the requested world bounds are not an exact multiple of the resolution. Draw title strings for bounds, resolution, rows/cols, occupancy, obstacle count, tire status/message, and input source. - -- [ ] **Step 4: Implement collision-safe atomic save and cleanup** - -Create `\TrapMapExports` only after all request/dimension validation. Select the millisecond timestamp name; if it exists, append `_1`, `_2`, etc. Encode into the exclusively reserved `finalPath + ".tmp"` stream, read `FileInfo.Length`, call `IsFileSizeAllowed`, delete the temp on rejection, then `File.Move(tempPath, finalPath)`. - -Wrap rendering/saving in `try/catch/finally`; `finally` deletes only the current temp path if present. Never delete an existing final PNG. Return failure messages instead of throwing into the test runner. - -- [ ] **Step 5: Run image and existing behavior tests** - -```powershell -dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1 -``` - -Expected: PNG assertions pass and existing behavior scripts retain their passing messages. - -### Task 3: Terminal switch and successful-map export integration - -**Files:** -- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs` -- Modify: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` -- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1` - -**Interfaces:** -- Consumes: Task 2 exporter. -- Produces: `_saveFullMapImage`, `_enableTerminalDebugLog`, shared dual-channel logging, and post-success export. - -- [ ] **Step 1: Add failing source/integration contracts** - -Require exact defaults, `EnableTerminalDebugLog` builder wiring, `TrapMapLog.Write`, and `TrapMapImageExporter.ExportIfEnabled`. Assert source still contains both world-Painter calls without `false`, retains the filter Painter with `false`, and export invocation occurs only after `_builder.Succeeded` is checked. - -Add source assertions: - -```powershell -if ($source -notmatch '_saveFullMapImage\s*=\s*true') { $failures.Add('Image switch default missing.') } -if ($source -notmatch '_enableTerminalDebugLog\s*=\s*true') { $failures.Add('Terminal switch default missing.') } -if ($source -notmatch 'TrapMapLog\.Write\(') { $failures.Add('Shared TrapMap logger missing.') } -if ($source -notmatch 'Console\.WriteLine\(') { $failures.Add('Terminal mirror missing.') } -if ($source -match 'GetPainter\("TrapMapTest",\s*false\)') { $failures.Add('TrapMap Painter regressed to local coordinates.') } -if ($source -notmatch 'GetPainter\("MultiWheelTwoLegDetect\.Filter",\s*false\)') { $failures.Add('Filter Painter lost local coordinates.') } -``` - -- [ ] **Step 2: Run RED contract test** - -Run `verify_trapmap_inputs.ps1`. Expected: new switch/logger/export assertions fail. - -- [ ] **Step 3: Implement the shared logger and switches** - -Add a `TrapMapLog` static class with: - -```csharp -public static void Write(string message, bool enableTerminal) -{ - DLog.Log(message, "TrapMapTest"); - if (enableTerminal) - Console.WriteLine($"[TrapMapTest] {message}"); -} -``` - -Add `public bool EnableTerminalDebugLog = true` to the builder. Replace each TrapMap-owned two-argument `DLog.Log` call whose category is exactly `"TrapMapTest"` with `TrapMapLog.Write(message, EnableTerminalDebugLog)` in the builder and with the const switch in `TrapMapTest`. Do not replace unrelated log categories or `Hedingben.ToastText`. - -Add the exact two constants to the test manual-edit section and pass terminal configuration into the builder. - -- [ ] **Step 4: Invoke image export after successful map construction** - -After the `_builder.Succeeded` failure return and after retrieving `grid`, construct the request from `_builder.GridMap`, `VehiclePose`, `WorkstationWorld`, tire status/message/input source, and `Environment.CurrentDirectory`. Call: - -```csharp -var export = TrapMapImageExporter.ExportIfEnabled(_saveFullMapImage, request); -TrapMapLog.Write(export.Message, _enableTerminalDebugLog); -if (export.Saved) - Hedingben.ToastText($"栅格图片已保存: {export.FilePath}", "TrapMapTest"); -``` - -Do not alter builder success when export fails/skips. - -- [ ] **Step 5: Run full fresh verification** - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1 -dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 -git diff --check -$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" } -``` - -Expected: source, compile, grid, lifecycle, and image tests pass; no whitespace errors; no staged files. Confirm an exporter-generated test PNG reports 300 DPI and never exceeds 50MB before test cleanup. diff --git a/docs/superpowers/plans/2026-07-22-workstation-bounded-trap-map.md b/docs/superpowers/plans/2026-07-22-workstation-bounded-trap-map.md deleted file mode 100644 index f16992a..0000000 --- a/docs/superpowers/plans/2026-07-22-workstation-bounded-trap-map.md +++ /dev/null @@ -1,279 +0,0 @@ -# Workstation-Bounded Layered Trap Map Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a task-level world-coordinate grid bounded by the current vehicle, an adjustable workstation, and independent X/Y margins; tire detection remains an optional layer. - -**Architecture:** `TrapMapBuilder` validates required inputs, reads the Detour pose, calculates the vehicle/workstation rectangle, creates the base grid, and marks the self layer before attempting tire detection. Tire acquisition records layer health and optionally adds occupancy without controlling map success. `GridMapData` remains the merged output so a future point-cloud layer can rasterize into the same map through a separate step. - -**Tech Stack:** C# 10, .NET Standard 2.0, ClumsyCore/ClumsyDance, PowerShell reflection/contract tests, `dotnet msbuild`. - -## Global Constraints - -- Do not inspect or modify `TrajPlanner`. -- Workstation coordinates use the Detour world frame and millimetres. -- Defaults: `WorkstationX=10000f`, `WorkstationY=0f`, `MapMarginX=3000f`, `MapMarginY=3000f`. -- `Free` means “not marked by an integrated source,” not sensor-confirmed obstacle-free. -- Do not invent a point-cloud API or send chassis commands. -- Reject maps above `4_000_000` cells before allocating `byte[,]`. - ---- - -### Task 1: Executable task-bound calculation - -**Files:** -- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs` -- Modify: `ClumsyPilot/tests/verify_trapmap_grid.ps1` - -**Interfaces:** -- Consumes: vehicle/workstation coordinates, X/Y margins, resolution, cell limit. -- Produces: `TrapMapBounds.TryCreate(float carX, float carY, float workstationX, float workstationY, float marginX, float marginY, float resolutionMm, int maxCellCount, out TrapMapBounds bounds, out string failureReason)` plus `XMin`, `XMax`, `YMin`, `YMax`, `Rows`, `Cols`, `CellCount`. - -- [ ] **Step 1: Write failing reflection tests** - -Append to `verify_trapmap_grid.ps1`: - -```powershell -$boundsType = $assembly.GetType('MultiWheelC.TrapMapBounds', $true) -$tryCreate = $boundsType.GetMethod('TryCreate') -function Invoke-Bounds([single]$carX, [single]$carY, [single]$stationX, [single]$stationY, - [single]$marginX, [single]$marginY, [single]$resolution, [int]$maxCells) { - $args = @($carX, $carY, $stationX, $stationY, $marginX, $marginY, - $resolution, $maxCells, $null, $null) - $ok = $tryCreate.Invoke($null, $args) - [pscustomobject]@{ Ok=$ok; Bounds=$args[8]; Reason=$args[9] } -} -$result = Invoke-Bounds 2000 -1000 10000 0 3000 4000 50 4000000 -Assert-Equal $true $result.Ok 'Valid bounds must succeed.' -Assert-Equal ([single]-1000) $result.Bounds.XMin 'Wrong XMin.' -Assert-Equal ([single]13000) $result.Bounds.XMax 'Wrong XMax.' -Assert-Equal ([single]-5000) $result.Bounds.YMin 'Wrong YMin.' -Assert-Equal ([single]4000) $result.Bounds.YMax 'Wrong YMax.' -Assert-Equal 280 $result.Bounds.Cols 'Wrong column count.' -Assert-Equal 180 $result.Bounds.Rows 'Wrong row count.' -Assert-Equal $false (Invoke-Bounds 0 0 100000 100000 0 0 20 4000000).Ok 'Oversized map must fail.' -Assert-Equal $false (Invoke-Bounds 0 0 10000 0 -1 3000 50 4000000).Ok 'Negative margin must fail.' -``` - -- [ ] **Step 2: Prove the new test fails** - -Run: - -```powershell -dotnet msbuild ClumsyPilot\ClumsyPilot.csproj /t:Compile /p:RestoreIgnoreFailedSources=true /v:minimal -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 -``` - -Expected: compile succeeds; the script fails because `MultiWheelC.TrapMapBounds` is absent. - -- [ ] **Step 3: Implement the bounds object** - -Add `TrapMapBounds` beside the input models. `TryCreate` must validate every float with `TrapMapValue.IsFinite`, require margins `>=0`, resolution and limit `>0`, calculate: - -```csharp -float xMin = Math.Min(carX, workstationX) - marginX; -float xMax = Math.Max(carX, workstationX) + marginX; -float yMin = Math.Min(carY, workstationY) - marginY; -float yMax = Math.Max(carY, workstationY) + marginY; -int cols = (int)Math.Ceiling((xMax - xMin) / resolutionMm); -int rows = (int)Math.Ceiling((yMax - yMin) / resolutionMm); -long cellCount = (long)rows * cols; -``` - -Reject non-finite/degenerate boundaries and `cellCount > maxCellCount` before returning a populated immutable-result object. Return a concrete Chinese reason for every rejection. - -- [ ] **Step 4: Recompile and run behavior tests** - -Run the Step 2 commands. Expected final line: `TrapMap GridMapData behavior checks passed.` - -- [ ] **Step 5: Commit** - -```powershell -git add ClumsyPilot/MovementTest.Trapmaptest.cs ClumsyPilot/tests/verify_trapmap_grid.ps1 -git commit -m "feat: calculate workstation-bounded trap map" -``` - -### Task 2: Base-map-first lifecycle and optional tire layer - -**Files:** -- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs` -- Modify: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` - -**Interfaces:** -- Consumes: Task 1 `TrapMapBounds.TryCreate` with its declared ten parameters, Detour pose, car dimensions, optional `TwoLegDetect` output. -- Produces: `WorkstationX`, `WorkstationY`, `MapMarginX`, `MapMarginY`, `WorkstationWorld`, `TrapMapTireLayerStatus TireLayerStatus`, and `string TireLayerMessage`. - -- [ ] **Step 1: Add failing lifecycle contracts** - -Add to `verify_trapmap_inputs.ps1`: - -```powershell -if ($source -match '\bMapHalfSizeMm\b') { $failures.Add('Legacy MapHalfSizeMm remains.') } -foreach ($name in 'WorkstationX','WorkstationY','MapMarginX','MapMarginY','TrapMapBounds','TireLayerStatus','TireLayerMessage') { - if ($source -notmatch "\b$name\b") { $failures.Add("Missing layered-map member: $name") } -} -if ($source -notmatch 'WorkstationX\s*=\s*10000f' -or $source -notmatch 'WorkstationY\s*=\s*0f' -or - $source -notmatch 'MapMarginX\s*=\s*3000f' -or $source -notmatch 'MapMarginY\s*=\s*3000f') { - $failures.Add('Required task-map defaults are missing.') -} -$createIndex = $source.IndexOf('TryCreateGridMap(') -$detectIndex = $source.IndexOf('ReadTireLayer(') -if ($createIndex -lt 0 -or $detectIndex -lt 0 -or $createIndex -gt $detectIndex) { - $failures.Add('Base map must precede optional tire acquisition.') -} -if ($source -notmatch 'TRAPMAP_FREE_SEMANTICS') { $failures.Add('Free semantics are undocumented.') } -``` - -- [ ] **Step 2: Run the contract and confirm failure** - -Run `powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1`. - -Expected: legacy half-size, missing members, and ordering assertions fail. - -- [ ] **Step 3: Add task-map configuration and status types** - -Replace `MapHalfSizeMm` with: - -```csharp -public float WorkstationX { get; set; } = 10000f; -public float WorkstationY { get; set; } = 0f; -public float MapMarginX { get; set; } = 3000f; -public float MapMarginY { get; set; } = 3000f; -public const int MaxCellCount = 4_000_000; -public Vector2 WorkstationWorld { get; private set; } -public TrapMapTireLayerStatus TireLayerStatus { get; private set; } -public string TireLayerMessage { get; private set; } -``` - -Define enum values `NotAttempted`, `Populated`, `NoDetection`, `Unavailable`, `Simulated`. Validate finite workstation coordinates and non-negative finite margins. - -- [ ] **Step 4: Reorder `Get()` and split population** - -Implement this required sequence: - -```csharp -VehiclePose = vehiclePose; -WorkstationWorld = new Vector2(WorkstationX, WorkstationY); -if (!TryCreateGridMap(out var gridMap, out failureReason) || - !TryPopulateSelfLayer(gridMap, out failureReason)) { - Fail(failureReason); - yield break; -} -ReadTireLayer(self); -TryPopulateTireLayer(gridMap); -``` - -`TryCreateGridMap` calls Task 1's ten-parameter `TrapMapBounds.TryCreate` and constructs `GridMapData` from the returned limits. The self method marks only the vehicle. Tire population may log a rasterization problem but must not clear or suppress the valid base map. - -- [ ] **Step 5: Make tire acquisition non-blocking** - -Rename the required-input method to `ReadTireLayer`. Every exit assigns an empty or populated list plus status/message. Use: - -```csharp -private void SetEmptyTireLayer(TrapMapTireLayerStatus status, string message) -{ - DetectedObstacles = new List(); - TireLayerStatus = status; - TireLayerMessage = message; - InputSource = "无轮胎障碍输入"; - DLog.Log($"轮胎层为空: status={status}, reason={message}", "TrapMapTest"); -} -``` - -Real-mode empty lidar/config errors/exceptions/illegal coordinates become `Unavailable`; `detected == null` becomes `NoDetection`; valid endpoints become `Populated`. Ghost simulation remains `Simulated` and is never a real-mode fallback. - -Add beside base-map creation: - -```csharp -// TRAPMAP_FREE_SEMANTICS: 当前 Free 仅表示尚未被已接入层标记, -// 不表示传感器确认现实中无障碍。未来实时点云应作为独立障碍层; -// 引入 Unknown 后,未观测栅格不应默认允许规划通行。 -``` - -- [ ] **Step 6: Verify and commit** - -Run `powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1`, `dotnet msbuild ClumsyPilot\ClumsyPilot.csproj /t:Compile /p:RestoreIgnoreFailedSources=true /v:minimal`, and `powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1`. Expected: both scripts exit 0 and compile has no TrapMap errors. Then: - -```powershell -git add ClumsyPilot/MovementTest.Trapmaptest.cs ClumsyPilot/tests/verify_trapmap_inputs.ps1 -git commit -m "fix: build trap map before optional tire layer" -``` - -### Task 3: Adjustable test entry and workstation visualization - -**Files:** -- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs` -- Modify: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` - -**Interfaces:** -- Consumes: Task 2 builder configuration/status. -- Produces: editable test defaults, non-occupying target marker, exact bounds/layer logs. - -- [ ] **Step 1: Add failing UI/source contracts** - -```powershell -if ($source -notmatch '_workstationX\s*=\s*10000f' -or $source -notmatch '_workstationY\s*=\s*0f' -or - $source -notmatch '_mapMarginX\s*=\s*3000f' -or $source -notmatch '_mapMarginY\s*=\s*3000f') { - $failures.Add('MovementTest adjustable defaults are missing.') -} -if ($source -notmatch 'DrawWorkstationMarker\(') { $failures.Add('Target visualization is missing.') } -if ($source -match 'MarkObstacle\(Workstation|MarkOccupied\([^\r\n]*Workstation') { - $failures.Add('Workstation must not become occupancy.') -} -``` - -- [ ] **Step 2: Prove the contract fails** - -Run the input script. Expected: missing test defaults and target visualization are reported. - -- [ ] **Step 3: Replace manual-edit fields and pass them to builder** - -```csharp -private const float _gridResolutionMm = 50f; -private const float _workstationX = 10000f; -private const float _workstationY = 0f; -private const float _mapMarginX = 3000f; -private const float _mapMarginY = 3000f; -private const float _safetyMarginMm = 300f; -``` - -Pass all values into `TrapMapBuilder`. Log vehicle, workstation, limits, dimensions, cells, `TireLayerStatus`, and `TireLayerMessage`. Remove all half-size/20m-window documentation. - -- [ ] **Step 4: Draw the workstation without marking occupancy** - -```csharp -private void DrawWorkstationMarker(Painter painter) -{ - const float radius = 250f; - painter.DrawCircle(Color.Lime, WorkstationWorld.X, WorkstationWorld.Y, radius); - painter.DrawLine(Color.Lime, WorkstationWorld.X - radius, WorkstationWorld.Y, - WorkstationWorld.X + radius, WorkstationWorld.Y, width: 3); - painter.DrawLine(Color.Lime, WorkstationWorld.X, WorkstationWorld.Y - radius, - WorkstationWorld.X, WorkstationWorld.Y + radius, width: 3); - painter.DrawText(Color.Lime, "Workstation", - WorkstationWorld.X + radius, WorkstationWorld.Y + radius); -} -``` - -Call it from visualization; never pass `WorkstationWorld` to occupancy methods. - -- [ ] **Step 5: Update comments and run final verification** - -Document formulas, defaults, task-level “global” meaning, layer order, optional tire behavior, and future independent point-cloud layer. Run: - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1 -dotnet msbuild ClumsyPilot\ClumsyPilot.csproj /t:Compile /p:RestoreIgnoreFailedSources=true /v:minimal -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 -rg -n "MapHalfSizeMm|GetSensor\(|GetPointCloud\(|PredefinedDriveStop\(|SendMotion\(|SendRotateMotion\(" ClumsyPilot\MovementTest.Trapmaptest.cs -git diff --check -``` - -Expected: scripts pass, compile has no TrapMap errors, forbidden search has no matches, and diff check is clean. - -- [ ] **Step 6: Commit** - -```powershell -git add ClumsyPilot/MovementTest.Trapmaptest.cs ClumsyPilot/tests/verify_trapmap_inputs.ps1 -git commit -m "feat: expose workstation trap map inputs" -``` diff --git a/docs/superpowers/plans/2026-07-23-trap-map-pure-png.md b/docs/superpowers/plans/2026-07-23-trap-map-pure-png.md deleted file mode 100644 index 4df0c26..0000000 --- a/docs/superpowers/plans/2026-07-23-trap-map-pure-png.md +++ /dev/null @@ -1,125 +0,0 @@ -# TrapMap Managed PNG Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Remove TrapMap's platform-specific drawing dependency and produce the same bounded 300 DPI PNG with a managed encoder that runs inside Clumsy. - -**Architecture:** Keep `TrapMapImageExporter.ExportIfEnabled` and its file-reservation/publication behavior. Replace only the renderer with an internal RGBA raster surface, primitive drawing functions, a compact embedded bitmap font, and `StbImageWriteSharp` for PNG encoding; insert the 300 DPI `pHYs` chunk after encoding. - -**Tech Stack:** C# 10, .NET Standard 2.0, `StbImageWriteSharp` 1.16.7, PowerShell contract/PNG parsing tests. - -## Global Constraints - -- Do not inspect or modify `TrajPlanner`. -- Do not commit or stage any file. -- Do not change grid construction, Painter behavior, movement behavior, switches, output location, naming, 300 DPI, 4 pixels per cell, 4000-pixel edge limit, or 50 MiB limit. -- The only new image package is `StbImageWriteSharp` version 1.16.7; do not add native assets or another graphics package. -- `TrapMapImageExporter` must have no runtime reference to `System.Drawing.Common` or `System.Drawing`. -- Preserve collision-safe temporary-file reservation, encoded-size validation, atomic publication, and contained export failures. - ---- - -### Task 1: Replace System.Drawing rendering with a managed Stb PNG encoder - -**Files:** -- Modify: `ClumsyPilot/ClumsyPilot.csproj` -- Modify: `ClumsyPilot/TrapMapImageExporter.cs` -- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1` -- Modify: `docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md` -- Modify: `docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md` - -**Interfaces:** -- Preserve: `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)` and all public request/result properties and constants. -- Add only private implementation units: `RgbaSurface`, integer drawing helpers, bitmap-font helpers, and `PngWriter`. -- `RenderToTemporaryPng` continues to consume the existing request/dimensions and write to the already exclusively reserved stream. - -- [ ] **Step 1: Add dependency-removal and PNG-structure assertions** - -Update `verify_trapmap_image.ps1` before production code. Require that: - -```powershell -if ($project.PackageReference.Include -contains 'System.Drawing.Common') { - throw 'TrapMap must not depend on System.Drawing.Common.' -} -if ($project.Target.Name -contains 'DeployFrameworkDrawingRuntime') { - throw 'Legacy drawing-runtime deployment target remains.' -} -if (-not ($project.PackageReference | Where-Object { - $_.Include -eq 'StbImageWriteSharp' -and $_.Version -eq '1.16.7' -})) { throw 'Exact managed PNG package is missing.' } -if ($exporterSource -match 'System\.Drawing|\bBitmap\b|\bGraphics\b|ImageFormat') { - throw 'Exporter still uses the external drawing API.' -} -``` - -Parse the generated PNG without loading a drawing assembly. Verify signature, one `IHDR`, one `pHYs`, one or more `IDAT`, and `IEND`; verify every chunk CRC. Assert `IHDR` width/height and RGBA8 fields and `pHYs` values `11811,11811,1`. Decode representative pixels with a test-only PNG decoder or the Stb package and reuse the existing color/Y-inversion assertions. - -Add a clean-output assertion after build: - -```powershell -$drawingDll = Join-Path (Split-Path -Parent $AssemblyPath) 'System.Drawing.Common.dll' -if (Test-Path -LiteralPath $drawingDll) { - throw 'System.Drawing.Common.dll must not be deployed for TrapMap.' -} -``` - -- [ ] **Step 2: Run RED verification** - -```powershell -dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -v:minimal -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 -``` - -Expected: the image test fails because the package, deployment target, `using System.Drawing`, and renderer still exist. - -- [ ] **Step 3: Remove the drawing package and deployment target** - -Delete the `System.Drawing.Common` `PackageReference` and the entire `DeployFrameworkDrawingRuntime` target. Add ``. Do not change the target framework or other references. Ensure a clean build cannot retain the old DLL: the verification command must remove only `ClumsyPilot/bin/Debug/netstandard2.0/System.Drawing.Common.dll` before rebuilding, after resolving and validating that exact path is under the project output directory. - -- [ ] **Step 4: Implement the RGBA raster surface** - -Replace drawing types with a private surface backed by `byte[]` in RGBA order. Required primitives and semantics: - -```csharp -SetPixel(int x, int y, byte r, byte g, byte b, byte a = 255); -FillRectangle(int x, int y, int width, int height, Color32 color); -DrawLine(int x0, int y0, int x1, int y1, Color32 color, int thickness); -DrawRectangle(int x, int y, int width, int height, Color32 color, int thickness); -DrawCircle(int centerX, int centerY, int radius, Color32 color, int thickness); -FillCircle(int centerX, int centerY, int radius, Color32 color); -FillPolygon(PointD[] points, Color32 color); -``` - -Clip every primitive to the surface. Use pre-clipped Bresenham lines, scale-normalized scanline polygon filling, and a canvas-clipped bounded circle scan whose work is proportional to visible rows/columns rather than radius. Preserve exact white, red, LightGray `(211,211,211)`, black, blue, and LimeGreen `(0,255,0)` colors. Convert vehicle/workstation world positions using the existing discrete-grid-aligned transform. - -- [ ] **Step 5: Implement deterministic bitmap text** - -Embed a private 5×7 ASCII glyph table for code points 32–126. Draw scaled glyphs using integer pixels; unsupported characters render as `?`. Use a 2× scale for header text and 1× scale for the workstation label. Keep all five header baselines inside `HeaderHeightPixels=140` with fixed non-overlapping line boxes. Continue building the same five metadata lines, including occupancy rate; sanitize only the exported header text, not logs. - -- [ ] **Step 6: Encode PNG and add 300 DPI metadata** - -Use `StbImageWriteSharp.ImageWriter.WritePng` to encode the RGBA buffer. Then insert: - -```text -pHYs: X=11811, Y=11811, unit=1 -``` - -Encode into a temporary `MemoryStream`, validate the PNG signature and first `IHDR` chunk, then copy the signature+IHDR, append the 13-byte `pHYs` chunk, and copy the remaining encoded chunks. Use big-endian integers and standard CRC-32 over `pHYs`+data. Do not close the caller-owned reserved stream before the existing file-size/atomic-move flow finishes. - -- [ ] **Step 7: Update documentation and run GREEN verification** - -Remove all claims that TrapMap deploys or requires `System.Drawing.Common`; document the pure C# encoder and BCL-only runtime. - -Run from a clean output state: - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1 -dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -v:minimal -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 -git diff --check -$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" } -``` - -Expected: build succeeds without a drawing DLL in output; all tests pass; PNG parser reports correct dimensions, CRCs, 300 DPI metadata, RGBA pixels, unique filenames, file size at or below 50 MiB, and no temporary files. diff --git a/docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md b/docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md deleted file mode 100644 index dfd5439..0000000 --- a/docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md +++ /dev/null @@ -1,427 +0,0 @@ -# Hybrid A* P0 规划核心 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。 - -**Goal:** 在既有 `PlanningGridMap` 之上提供可复用、可验证且不依赖 UI 的 Hybrid A* 粗路径规划服务。 - -**Architecture:** P0 先固定 m/rad/1/m 数据契约和连续车辆碰撞边界,再在该边界上实现恒曲率原语、二维启发式、确定性 Hybrid A*、路径重建和最终复核。`CoarsePathPlanningService` 是业务的唯一组合入口;`HybridAStarPlanner` 是只消费已建地图的下层门面。 - -**Tech Stack:** C# 10、.NET Standard 2.0、现有 `PlanningGridMap`、PowerShell 反射契约测试、`CancellationToken`。 - -## Global Constraints - -- 所有运行时代码位于 `ClumsyPilot/ParkrobTrajplanner/CoarsePath/`,命名空间为 `MultiWheelC.TrajectoryPlanning.CoarsePath` 或其子命名空间。 -- Map 只保存外部障碍物;安全余量只在连续车辆碰撞检查时扩张车辆矩形,绝不写入 Map。 -- 地图输入和障碍几何使用 mm;CoarsePath 的位置使用 m、航向使用 rad、曲率使用 1/m。 -- 规划器只能接受 `PlanningGridMap`,不得引用 TwoLeg、定位、Painter、UI 或系统时间。 -- 所有公开类型、构造函数、属性和方法使用中文 XML 文档,明确参数单位、边界以及返回或失败语义;内部几何/搜索不变量使用简短中文注释。 -- `netstandard2.0` 禁止直接使用 `PriorityQueue`、`Math.Clamp`、`double.IsFinite`、`record`、`init`。 -- 固定约束:原语最大长度 0.50 m;积分最大步长 0.05 m;碰撞中心步长不超过 `min(0.025 m, Map.ResolutionMeters / 2)`;默认终点容差为 0.15 m、5°。 -- 终点候选必须进入 Open List,只有作为最佳有效条目出队时才能成功;地图外始终按占据处理。 -- 本计划不实现 P1 的 Clumsy `MovementTest`、Painter 绘制、Release 性能基准或旧 TrapMap 入口退役。 -- 按用户现有约束,不执行 Git 自检、暂存、提交或推送。 - ---- - -## 文件结构 - -```text -ClumsyPilot/ParkrobTrajplanner/ -├── CoarsePath/ -│ ├── Contracts/ # 请求、结果、枚举和值对象 -│ ├── Vehicle/ # 扩大车辆几何和连续碰撞 -│ ├── Search/ # 原语、堆、启发式和 Hybrid A* 搜索 -│ ├── Output/ # 回溯、稠密路径装配和最终验证 -│ ├── Facade/ # 一次调用编排和调试旁路契约 -│ ├── HybridAStarPlanner.cs -│ └── README.md # 粗规划调用方文档;链接至 ../Map/README.md -└── tests/ - ├── verify_coarse_path_collision.ps1 - ├── verify_coarse_path_search.ps1 - └── verify_coarse_path_integration.ps1 -``` - -## Task 1: 固定公共契约、状态与默认配置 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs` -- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1` - -**Produces:** - -```csharp -public sealed class Pose2D -{ - public Pose2D(double xMeters, double yMeters, double headingRadians); - public double X { get; } - public double Y { get; } - public double Heading { get; } -} - -public sealed class PlanningRequest -{ - public PlanningGridMap Map { get; set; } - public Pose2D Start { get; set; } - public Pose2D Goal { get; set; } - public VehicleParameters Vehicle { get; set; } - public HybridAStarConfiguration Configuration { get; set; } - public double StartVehicleCurvature { get; set; } - public TravelDirection? StartDirection { get; set; } - public GoalDirectionConstraint GoalDirection { get; set; } -} -``` - -- [ ] **Step 1: 写失败的公共契约测试。** 在 `verify_planning_utils.ps1` 载入程序集后添加反射断言,检查 `Pose2D` 构造函数、三个枚举、`PlanningRequest` 属性和每个默认值。默认配置断言如下: - -```powershell -$config = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.HybridAStarConfiguration -Assert-Equal 0.50 $config.PrimitiveLengthMeters '原语最大长度' -Assert-Equal 0.05 $config.IntegrationStepMeters '积分步长' -Assert-Equal 0.025 $config.MaximumCollisionCheckStepMeters '碰撞步长' -Assert-Equal 5 $config.CurvatureLevelCount '曲率等级数' -Assert-Equal 200000 $config.MaximumExpandedNodes '节点上限' -``` - -- [ ] **Step 2: 运行测试并确认 RED。** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -``` - -预期:脚本因 `CoarsePath` 类型尚不存在而以非零退出。 - -- [ ] **Step 3: 实现最小契约。** `TravelDirection` 仅含 `Forward`、`Reverse`;`GoalDirectionConstraint` 仅含 `Any`、`Forward`、`Reverse`;`CoarsePathPointSource` 仅含 `Start`、`MotionPrimitive`、`GoalTruncation`。`PlanningStatus` 必须包含 `Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed`、`InternalError`。 - -`HybridAStarConfiguration` 的构造默认值必须是:`Math.PI / 36d` 航向/终点航向容差、5 秒超时、`HeuristicWeight=1d`、`ReverseCostMultiplier=1.5d`、`GearSwitchPenaltyMeters=1d`、`CurvatureMagnitudeWeight=0.10d`、`CurvatureChangePenaltyMetersPerLevel=0.05d`、`ClearanceCostWeight=0.20d`、`ClearanceCostDistanceMeters=0.50d`。 - -`PlanningResult` 只允许成功结果携带非空路径与分段;所有失败工厂方法返回空只读集合并保留诊断。`PlanningDiagnostics` 固定记录扩展、生成、重开、陈旧堆条目、Open List 峰值、路径长度、最小保守净空、耗时和终止原因。 - -- [ ] **Step 4: 重新运行工具契约测试并确认 GREEN。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -``` - -预期:退出码为 0,既有 Utils/Map 契约仍可加载。 - -## Task 2: 实现扩大车辆足迹与连续碰撞检查 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs` -- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1` - -**Consumes:** `PlanningGridMap`、`Pose2D`、`VehicleParameters`。 - -**Produces:** - -```csharp -public sealed class FootprintCollisionChecker -{ - public bool IsPoseCollisionFree( - Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, - double additionalMarginMeters, out double bodyClearanceMeters); - - public bool IsSweptMotionCollisionFree( - Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, - double maximumCenterStepMeters, out double minimumBodyClearanceMeters); -} -``` - -- [ ] **Step 1: 写失败的连续碰撞测试。** 脚本通过 `PlanningMapFactory` 创建 50 mm 地图和单个薄矩形障碍,验证下面三种行为:车辆与障碍格擦边返回碰撞、距离场净空严格大于外接圆半径时返回安全、两个端点安全但中间穿过障碍时扫掠检查返回碰撞。 - -```powershell -$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker -$clearance = 0.0 -$safe = $checker.IsPoseCollisionFree($pose, $map, $vehicle, 0.0, [ref]$clearance) -Assert-False $safe '矩形擦边必须视为碰撞' -``` - -- [ ] **Step 2: 运行碰撞脚本并确认 RED。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -``` - -预期:因车辆命名空间和碰撞检查器不存在而失败。 - -- [ ] **Step 3: 实现最小连续几何。** `VehicleKinematics` 在最大曲率与最小转弯半径都存在时取 `Math.Min(maximumCurvature, 1d / minimumRadius)`。`VehicleFootprint` 用 `LengthMeters + 2 * SafetyMarginMeters` 和 `WidthMeters + 2 * SafetyMarginMeters` 构造以 `Pose2D` 为几何中心的旋转矩形、AABB 与外接圆。 - -`OrientedRectangleCellIntersection` 使用 SAT:矩形的两个单位轴和格子的世界 X/Y 轴都作为投影轴;任一轴存在严格分离才是不相交,投影接触算相交。`FootprintCollisionChecker` 依次验证四角均在地图内、用严格 `distance > radius + additionalMargin` 快速放行、遍历 AABB 内占据格并执行 SAT。扫掠检查将中心位移切分到 `min(maximumCenterStepMeters, map.ResolutionMeters / 2d)`,每一段的临时边距为 `0.5d * (centerDisplacement + circumscribedRadius * Math.Abs(headingDelta))`。 - -- [ ] **Step 4: 扩展碰撞测试并运行 GREEN。** 加入 0°、45°、任意航向、栅格中心/亚栅格中心、薄障碍、边界外和扫掠场景;随后运行: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -``` - -预期:构建与脚本退出码均为 0。 - -## Task 3: 实现原语积分、目标容差与内部截断 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs` -- Create: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**Consumes:** `Pose2D`、方向、车辆最大曲率、`HybridAStarConfiguration`、`FootprintCollisionChecker`。 - -**Produces:** 含方向、曲率、实际长度和内部积分点的不可变 `MotionPrimitive`;目标检查器只判定位置、航向和目标进入方向。 - -- [ ] **Step 1: 写失败的原语测试。** 验证直行、圆弧、倒车、0.50 m 上限、积分点间距上限,以及目标在 0.30 m 处时原语恰好截断到第一个满足条件的内部点。 - -```powershell -$primitive = $generator.Generate($start, $curvature, $direction, $config, $map) -Assert-True ($primitive.Points.Count -ge 1) '原语必须产生内部积分点' -Assert-Equal 0.30 $truncated.ActualLengthMeters '0.30m 目标必须在原语内部截断' -``` - -- [ ] **Step 2: 运行搜索脚本并确认 RED。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -``` - -预期:因原语类型尚不存在而失败。 - -- [ ] **Step 3: 实现解析积分和检查顺序。** 单步积分使用: - -```csharp -double signedDistance = direction == TravelDirection.Forward ? step : -step; -double nextHeading = AngleMath.NormalizeRadians(heading + curvature * signedDistance); -if (Math.Abs(curvature) < 1e-12) -{ - nextX = x + signedDistance * Math.Cos(heading); - nextY = y + signedDistance * Math.Sin(heading); -} -else -{ - nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature; - nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature; -} -``` - -原语点步长不得超过 `min(IntegrationStepMeters, MaximumCollisionCheckStepMeters, Map.ResolutionMeters / 2d)`。每个内部点严格按“有限数值 → 从前一点的扫掠碰撞 → 终点容差”执行;命中目标即截断,并标记 `GoalTruncation`。起点已满足目标时生成零长度终点候选,不生成运动原语。 - -- [ ] **Step 4: 运行原语测试并确认 GREEN。** 加入五个曲率等级、曲率相邻变化最多一级和 `±π` 航向容差的断言;再运行 `verify_coarse_path_search.ps1`,预期退出码为 0。 - -## Task 4: 实现确定性 Open List、代价与二维启发式 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**Produces:** 内部二叉最小堆、等效米代价计算器和目标反向八邻域距离启发式。 - -- [ ] **Step 1: 写失败的堆、代价和启发式测试。** 验证堆的排序优先级为 `F`、`H`、较大 `G`、插入序号;验证八邻域斜向代价和禁止切过两个正交障碍的对角夹角;验证倒车、换向、曲率和净空代价项。 - -```powershell -Assert-Equal 'node-b' $heap.Pop().Id '相同 F 时应先选较小 H' -Assert-Throws { $calculator.Calculate($invalidInput) } '负权重必须拒绝' -Assert-True ([double]::IsPositiveInfinity($heuristic.GetCost($blockedRow, $blockedCol))) '二维不可达应为无穷' -``` - -- [ ] **Step 2: 运行搜索测试并确认 RED。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -``` - -预期:因堆、代价或启发式类型不存在而失败。 - -- [ ] **Step 3: 实现最小支持结构。** `BinaryMinHeap` 使用 `List`,比较器严格按 `F`、`H`、反向 `G`、插入序号。代价必须实现: - -```text -length * directionMultiplier * - (1 + curvatureMagnitudeWeight * abs(curvature / maximumCurvature) - + clearanceCostWeight * max(0, 1 - clearance / clearanceCostDistance)) -+ gearSwitchPenalty -+ curvatureChangePenalty * abs(curvatureLevelDelta) -``` - -`GridDijkstraHeuristic` 从目标格反向传播四邻域 1 倍格长和对角 `sqrt(2)` 倍格长;对角移动前确认两个正交邻格均未占据。 - -- [ ] **Step 4: 运行搜索测试并确认 GREEN。** 重复构造同一输入两次,断言出队顺序相同;运行 `verify_coarse_path_search.ps1`,预期退出码为 0。 - -## Task 5: 实现 Hybrid A* 节点、重开与终点候选管理 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - -**Consumes:** Task 2–4 的碰撞、原语、堆、代价与启发式。 - -**Produces:** 接收已验证请求并返回成功节点索引或明确搜索失败状态的内部搜索器。 - -- [ ] **Step 1: 写失败的搜索测试。** 覆盖空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标方向、无解、取消、超时、节点上限、较小 `G` 重开和终点候选出队顺序。 - -```powershell -$result = $search.Search($request, [Threading.CancellationToken]::None) -Assert-Equal 'Success' $result.Status '空图应规划成功' -Assert-True $result.ReopenedNodeCount -gt 0 '更小 G 到达同键时必须允许重开' -``` - -- [ ] **Step 2: 运行搜索脚本并确认 RED。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -``` - -预期:因 `HybridAStarSearch` 不存在而失败。 - -- [ ] **Step 3: 实现离散键与搜索循环。** `HybridAStarNodeKey` 固定包含位置行列、航向索引、方向和曲率等级。普通状态的最佳 `G` 保存在 `Dictionary`;发现严格更小的 `G` 时压入新条目,旧条目在弹出时丢弃。循环在扩展前检查 `CancellationToken`、配置超时和最大扩展数。 - -终点候选压入同一 Open List,但不放入普通键的去重表;它只能在作为当前最佳有效条目弹出、重新验证终点条件与末段碰撞后成功。Open List 耗尽返回 `NoFeasiblePath`。 - -- [ ] **Step 4: 运行全量搜索场景并确认 GREEN。** 对每个固定场景重复运行两次并断言状态、路径代价和节点扩展顺序一致;运行 `verify_coarse_path_search.ps1`,预期退出码为 0。 - -## Task 6: 回溯、路径装配、最终验证与下层门面 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` -- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Produces:** - -```csharp -public sealed class HybridAStarPlanner -{ - public PlanningResult Plan( - PlanningRequest request, - CancellationToken cancellationToken = default(CancellationToken)); -} -``` - -- [ ] **Step 1: 写失败的路径输出测试。** 验证首点弧长为 0、弧长不递减、展开航向连续、终点截断来源、相邻重复点只允许作为换向对、分段的包含式索引覆盖全部路径。 - -```powershell -Assert-Equal 0.0 $result.Path[0].ArcLength '起点弧长必须为零' -Assert-True ($result.Segments[-1].EndIndex -eq ($result.Path.Count - 1)) '分段必须覆盖尾点' -Assert-Equal 'FinalValidationFailed' $invalid.Status '最终复核失败不能发布部分路径' -``` - -- [ ] **Step 2: 运行集成脚本并确认 RED。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -预期:因 `HybridAStarPlanner` 与输出类型不存在而失败。 - -- [ ] **Step 3: 实现回溯和最终复核。** 搜索节点只保留父索引和原语描述;`PathBacktracker` 在成功后使用同一解析积分公式重建内部点。`CoarsePathAssembler` 累计弧长,保持换向处两个相同位姿/弧长而方向不同的点,并让新方向点设置 `IsGearSwitchPoint=true`。 - -`CoarsePathValidator` 使用与搜索相同的 `FootprintCollisionChecker` 和扫掠规则,检查有限数、曲率上限、起终点容差/方向、弧长单调性、换向对与分段覆盖。验证失败返回 `FinalValidationFailed`,路径和分段均为空。 - -`HybridAStarPlanner` 在调用搜索前映射空请求、Map 未就绪、车辆无效、曲率配置无效、起终点越界及起终点碰撞;其余异常收敛为 `InternalError` 并记录诊断。 - -- [ ] **Step 4: 运行碰撞、搜索和集成脚本并确认 GREEN。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -预期:三个脚本均退出 0。 - -## Task 7: 一次调用服务、调试旁路契约与 README - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Produces:** - -```csharp -public sealed class CoarsePathPlanningService -{ - public CoarsePathPlanningJobResult Plan( - CoarsePathPlanningJob job, - CancellationToken cancellationToken = default(CancellationToken)); -} -``` - -- [ ] **Step 1: 写失败的一次调用测试。** 断言服务先建图、地图失败时不搜索、成功时同时返回 `PlanningMapBuildResult` 和 `PlanningResult`;同一服务实例两次使用相同地图请求时第二次是 `Input` 缓存命中。 - -```powershell -$service = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Facade.CoarsePathPlanningService -$first = $service.Plan($job, [Threading.CancellationToken]::None) -$second = $service.Plan($job, [Threading.CancellationToken]::None) -Assert-Equal 'Input' $second.MapResult.CacheHit.ToString() '服务必须长期持有地图工厂' -``` - -- [ ] **Step 2: 运行集成脚本并确认 RED。** - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -预期:因门面类型与 README 不存在而失败。 - -- [ ] **Step 3: 实现服务和文档。** `CoarsePathPlanningService` 构造时创建一个长期 `PlanningMapFactory` 与一个 `HybridAStarPlanner`,计划调用顺序固定为: - -```text -PlanningMapFactory.Create(job.MapRequest) - -> 地图失败:包装 MapResult,返回空 PlanningResult - -> 地图成功:HybridAStarPlanner.Plan(job 转换的 PlanningRequest) - -> 仅依 Debug 选项向 IPlanningDebugSink 发布旁路数据 -``` - -默认 sink 为空实现。任何 sink 异常只追加调试诊断,绝不改变地图哈希、规划状态、路径或分段。 - -README 必须包含以下小节:模块范围;Map 与 CoarsePath 的职责表;`CoarsePathPlanningService.Plan` 的可编译调用示例;mm/m/rad/1/m 单位表;`SourceVersion` 与缓存规则;`PlanningStatus` 处理示例;路径点和方向分段含义;第一版不支持的平滑、速度规划、控制和横移能力;到 `../Map/README.md` 的链接。 - -- [ ] **Step 4: 运行完整 P0 验收。** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -预期:构建和全部七个脚本退出码均为 0;不执行 P1 UI 或性能工作。 - -## Plan Self-Review - -- 覆盖性:Task 1 覆盖公共契约;Task 2 覆盖连续车辆碰撞;Task 3–5 覆盖原语、代价、启发式、确定性搜索与终点候选;Task 6 覆盖输出与最终复核;Task 7 覆盖一次调用门面、文档和全量验收。 -- 类型一致性:所有搜索和门面输入均以 `PlanningGridMap`、`PlanningRequest`、`CoarsePathPlanningJob` 为唯一跨层契约;Map 构建只存在于 Task 7 的服务门面。 -- 范围:没有包含 Clumsy UI、Painter、性能基准或旧 TrapMap 迁移,这些均为 P1。 diff --git a/docs/superpowers/plans/2026-07-27-map-documentation.md b/docs/superpowers/plans/2026-07-27-map-documentation.md deleted file mode 100644 index a09435e..0000000 --- a/docs/superpowers/plans/2026-07-27-map-documentation.md +++ /dev/null @@ -1,242 +0,0 @@ -# Map 模块文档与注释实施计划 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 为 Map 模块提供根目录结构说明,并为所有公共 API 提供中文、Python docstring 风格的调用说明。 - -**Architecture:** `Map/README.md` 只说明模块结构、数据流、单位与入口;`.cs` 中的 `/// ` 是参数、返回和约束的唯一 API 文档来源。注释不得改变方法签名、建图算法、缓存键或任何运行时行为。 - -**Tech Stack:** C# 10、netstandard2.0、PowerShell 验证脚本、Markdown。 - -## Global Constraints - -- 所有新增说明使用中文。 -- 公共 API 文档使用可被 C# IDE 识别的 `///`,内容顺序为“功能、参数、返回、注意”。 -- 参数说明必须给出单位、坐标系、可空性或输入约束中的适用项。 -- 返回说明必须给出结果数据的业务意义;`bool` 说明其 true/false 语义。 -- README 不复制逐个属性的完整参数表。 -- 不修改运行逻辑,不执行 Git 自检、暂存、提交或重置。 - ---- - -### Task 1: 建立 Map README 与文档存在性检查 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/Map/README.md` -- Create: `ClumsyPilot/tests/verify_planning_map_documentation.ps1` - -**Interfaces:** -- Consumes: `PlanningMapFactory.Create(PlanningMapRequest request)`、Map 现有目录结构。 -- Produces: Map 模块入口说明和可重复运行的文档检查。 - -- [ ] **Step 1: 写入失败检查** - -创建 PowerShell 脚本,读取 `Map/README.md`,断言它不存在时抛出异常;创建后继续断言包含以下固定标题:`# Map 模块说明`、`## 文件结构`、`## 建图数据流`、`## 坐标与单位`、`## 最小调用示例`、`## 缓存与版本`、`## 测试与调试`。 - -- [ ] **Step 2: 运行检查确认失败** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: 因 `Map/README.md` 不存在而失败。 - -- [ ] **Step 3: 创建 README** - -写入当前 `Core`、`Obstacles`、`Sources`、`Planning`、`Test`、`Test/Visualization` 的目录树;每个 `.cs` 文件后写一句职责。说明数据流为 `PlanningMapRequest → IMapObstacleSource → EnvironmentMapBuilder/MapObstacleRasterizer → EnvironmentGridMap → PlanningMapAdapter/ObstacleDistanceField → PlanningGridMap`。说明环境图使用世界 mm、规划查询使用 m、范围采用左闭右开;示例只经长期持有的 `PlanningMapFactory.Create` 调用;说明 `SourceVersion` 变化与两级缓存的关系;明确 PNG 为可选调试、旧 TrapMap 不属于新运行时入口。 - -- [ ] **Step 4: 运行检查确认通过** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: `Planning map documentation checks passed.` - -### Task 2: 注释公共建图入口与结果契约 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs` - -**Interfaces:** -- Consumes: 外部调用者提供的地图范围、分辨率、障碍物来源。 -- Produces: 建图请求、构建结果、缓存命中状态的中文 API 契约。 - -- [ ] **Step 1: 扩展失败检查** - -在 `verify_planning_map_documentation.ps1` 中对上述文件断言:`PlanningMapFactory.Create`、`PlanningMapRequest.Bounds`、`PlanningMapBuildResult.Map` 前方紧邻中文 `///` 注释,且包含 `参数:`、`返回:`、`单位:` 或 `注意:` 中适用的说明。 - -- [ ] **Step 2: 运行检查确认失败** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: 失败并指出缺失的入口契约说明。 - -- [ ] **Step 3: 添加入口契约注释** - -为 `PlanningMapFactory`、构造和 `Create` 写明长期复用要求、请求输入、结果与三种缓存命中语义。为请求与结果的每个公共属性写明单位、可空性和失败/空图语义。为 `PlanningMapCacheHit` 的每个枚举值写明 `None`、`Input`、`Occupancy` 的实际含义。为内部 Map 构建请求和结果的 public 成员补充相同层级说明。 - -- [ ] **Step 4: 运行入口检查与编译** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore` - -Expected: 文档检查通过;编译 0 error。 - -### Task 3: 注释地图边界、环境栅格与障碍物契约 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs` - -**Interfaces:** -- Consumes: 世界坐标毫米几何、有效栅格范围。 -- Produces: 环境占据图以及几何到栅格的公开行为说明。 - -- [ ] **Step 1: 扩展失败检查** - -对 `MapBoundsMm` 构造函数、`Contains`、`GetDimensions`,`EnvironmentGridMap` 构造函数和世界/栅格查询方法,以及两种障碍物构造函数与属性,断言有中文 `///`。脚本还断言 `MapObstacleRasterizer.Rasterize` 注释包含其是唯一写栅格入口的约束。 - -- [ ] **Step 2: 运行检查确认失败** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: 失败并显示尚未文档化的公共几何/栅格 API。 - -- [ ] **Step 3: 添加边界与几何注释** - -为范围、行列、世界 mm 坐标、左闭右开边界、越界 `false`/占据行为、`out row/col` 的失败值写明说明。为圆和矩形的坐标、半径与 `IsValid` 写明单位和 true/false 条件。为环境构建器 `Build` 写明必需来源失败会整体失败、可选来源只记录状态的规则。 - -- [ ] **Step 4: 运行检查与 Map 适配器脚本** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1` - -Expected: 两个脚本通过。 - -### Task 4: 注释障碍物来源与 TwoLeg 投影契约 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs` - -**Interfaces:** -- Consumes: 纯检测快照和外部障碍物几何。 -- Produces: 世界 mm 几何、来源状态和诊断信息。 - -- [ ] **Step 1: 扩展失败检查** - -对来源接口的 ID、版本、必需性和 `ProjectToWorld`,TwoLeg 输入构造函数/属性,以及投影结果工厂方法和状态枚举值断言中文 API 说明。 - -- [ ] **Step 2: 运行检查确认失败** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: 失败并指出缺失的来源或 TwoLeg 契约说明。 - -- [ ] **Step 3: 添加来源注释** - -明确 `ProjectToWorld` 不得读传感器、定位、UI、时钟;`SourceVersion` 必须在快照内容变化时递增;`IsRequired` 的失败语义;TwoLeg 检测时世界位姿和两腿局部 mm 坐标、航向弧度、半径单位;`Applied/Empty/Unavailable/Invalid` 的规划含义。 - -- [ ] **Step 4: 运行来源工厂验证** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1` - -Expected: `Planning map factory checks passed.` - -### Task 5: 注释规划快照、距离场与缓存契约 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs` - -**Interfaces:** -- Consumes: 环境占据栅格和建图输入/占据哈希。 -- Produces: 不可变规划快照、保守距离和缓存复用行为说明。 - -- [ ] **Step 1: 扩展失败检查** - -断言 `PlanningGridMap` 的公共属性与查询方法、适配器/距离场/EDT 的公共静态方法、缓存公共方法均有中文 `///`;检查 `PlanningGridMap` 注释含 m 与 mm 的单位区分。 - -- [ ] **Step 2: 运行检查确认失败** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: 失败并报告缺失的规划或缓存说明。 - -- [ ] **Step 3: 添加规划与缓存注释** - -说明 `PlanningGridMap` 不可变、规划世界查询使用 m、越界视为占据/零净距、距离是保守下界;说明适配器从 mm 环境图转为 m 规划图;说明 EDT 输出平方距离;说明缓存容量为四、输入命中返回同一快照、占据命中共享数组但颁发新快照元数据。 - -- [ ] **Step 4: 运行适配器和工厂验证** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1` - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1` - -Expected: 两个脚本通过。 - -### Task 6: 注释测试、PNG 调试公共 API并完成总验证 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs` - -**Interfaces:** -- Consumes: 只读 `PlanningGridMap` 和可选 PNG 输出目录。 -- Produces: 清晰的 MapTest 配置/日志语义和 PNG 导出结果说明。 - -- [ ] **Step 1: 扩展失败检查** - -对 `PlanningMapTest.Test/TestStop`、PNG 请求/结果的每个属性、导出器常量与 `ExportIfEnabled`、渲染器和 PNG 写入器公共方法断言中文 `///`。 - -- [ ] **Step 2: 运行检查确认失败** - -Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` - -Expected: 失败并列出测试或可视化公共成员。 - -- [ ] **Step 3: 添加测试与 PNG 注释** - -说明 MapTest 是手工 Clumsy 入口,日志/PNG 开关仅影响调试;说明 PNG 不参与建图和缓存;说明输出目录、像素尺寸、字节大小、Saved/Skipped 的语义;说明 `ValidatedPngWriter.Write` 输入是 RGBA 行主序字节及其宽高。 - -- [ ] **Step 4: 完整验证** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 -``` - -Expected: 所有脚本通过;编译 0 error。现有过时 API 警告若仍来自 `MovementTests.TireFollowing.cs` 或 `TireFollowing.cs`,记录为非本任务引入。 - -## Plan Self-Review - -- Spec coverage: Task 1 覆盖 README 和目录结构;Task 2 至 Task 6 覆盖全部 public API 分层;Task 6 覆盖完整验证。 -- Placeholder scan: 本计划没有 TODO、TBD 或未指定的验证命令。 -- Type consistency: 文中使用的类型和方法名均来自当前 Map 源码;不引入新运行时接口。 diff --git a/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md b/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md deleted file mode 100644 index 3b5a31a..0000000 --- a/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md +++ /dev/null @@ -1,534 +0,0 @@ -# P1 粗路径 Clumsy UI 集成 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 交付可在 Clumsy 中后台运行的七个粗路径测试入口,显示真实规划栅格快照和完整路径信息,并支持传入 AMR 世界位姿与手动目标位姿。 - -**Architecture:** `CoarsePathScenarioFactory` 保持无 UI 的纯输入构造职责,提供六个可重复的回归场景及一个显式标注为空图演示的“AMR 位姿 + 手动终点”请求创建入口。`MovementTest.CoarsePathTest.cs` 只作为 UI 适配层:把 AMR/目标 mm+deg 转为核心所需的 m+rad,使用一个共享门面在 `Task.Run` 后台运行,并从不可变 `CoarsePathPlanningJobResult` 绘制地图快照和路径。 - -**Tech Stack:** C# / `netstandard2.0`、现有 Clumsy `MovementTest`/`Painter`、`CoarsePathPlanningService`、PowerShell 反射验证脚本。 - -## Global Constraints - -- `PlanningMapRequest` 的地图、障碍物、AMR 输入和手动目标 X/Y 均为世界 mm;`Pose2D` 和路径 X/Y 为世界 m;核心航向为 rad。 -- 项目上游的 AMR `th` 输入按 deg 适配为 `th * Math.PI / 180d`;不得沿用直接将该值传给 `Math.Cos/Sin` 的旧写法。 -- AMR 起点必须是车辆几何中心;传感器安装点必须由上游先按外参转换。 -- UI 和测试只能调用 `CoarsePathPlanningService.Plan(job, token)`;不得直接实例化 `PlanningMapFactory`、`HybridAStarPlanner`、栅格化器、碰撞器、原语生成器或搜索节点。 -- 所有七个 MovementTest 都不得引用 `BasicPilotBase.Chassis`、`SendMotion`、`DriveTask` 或任何底盘控制 API。 -- `Test` 不得等待后台任务或读取 `Task.Result`;`TestStop` 先取消令牌,再使运行编号失效、解绑任务并清空 Painter。 -- 只在 `PlanningStatus.Success` 绘制路径、方向箭头、换向点和扩大车体检查框;失败、取消和超时只显示地图、起点、终点和状态。 -- 代码兼容 `netstandard2.0`,不引入新 NuGet 包;公开类型/成员写中文 XML 文档,复杂单位与并发逻辑写简短中文行注释。 -- 不改动 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。 - ---- - -## 文件结构 - -| 文件 | 变更职责 | -| --- | --- | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新建纯场景工厂、六场景枚举、AMR/目标位姿 mm+deg 到核心 `Pose2D` 的转换,以及空图演示手动目标请求。 | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 新建七个 UI 入口、共享会话执行器、任务取消、Painter 地图/路径/图例绘制与手动输入解析。 | -| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 为工厂行为、单位转换、缓存/换向/无解、UI 源码边界与 README 内容新增真实程序集和文本断言。 | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 记录 P1 测试入口、输入单位、空图演示限制、图例、停止语义及无底盘命令边界。 | - -### 固定接口 - -```csharp -namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test; - -public enum CoarsePathTestScenario -{ - ExplicitEmpty, - RectangleDetour, - ManualAndTwoLeg, - CacheHit, - ReverseGearSwitch, - NoFeasiblePath, -} - -public static class CoarsePathScenarioFactory -{ - public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario); - - public static CoarsePathPlanningJob CreateManualGoalDemo( - double startXMillimeters, double startYMillimeters, double startHeadingDegrees, - double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees); -} -``` - -`CreateManualGoalDemo` 只构造带 2,000 mm 边缘留白的显式空图演示请求,并在 README/测试名称中明确其不代表真实环境安全。未来现场入口必须提供真实 `IMapObstacleSource` 快照,而不是修改此方法的语义。 - -### Task 1: 工厂契约与失败测试 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` -- Create later in Task 2: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` - -**Consumes:** 已有 `CoarsePathPlanningService.Plan(CoarsePathPlanningJob, CancellationToken)`、`Find-Method`、`Assert-True`、`Assert-Equal` 与程序集加载逻辑。 - -**Produces:** 对 `CoarsePathTestScenario` 和 `CoarsePathScenarioFactory` 的反射行为约束;Task 2 的最小实现必须使这些断言通过。 - -- [ ] **Step 1: 在集成脚本加入工厂反射测试** - -在现有 facade 检查后、最终输出前插入以下 PowerShell。它要求类型和两个公开方法都存在,因此在工厂未创建时失败。 - -```powershell -$testNamespace = $coarsePath + 'Test.' -$scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false) -$scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false) -Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.' -Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.' - -$factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType) -$factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @( - [double], [double], [double], [double], [double], [double]) -Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).' -Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.' - -$scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath') -foreach ($scenarioName in $scenarioNames) { - $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) - $jobA = $factoryCreate.Invoke($null, @($scenario)) - $jobB = $factoryCreate.Invoke($null, @($scenario)) - Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job." - Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call." -} - -$manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0)) -Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.' -Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.' -Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.' -Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.' -Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.' -Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.' -``` - -- [ ] **Step 2: 运行脚本确认失败** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建成功;脚本因 `P1 scenario enum must exist.` 失败。 - -### Task 2: 实现纯场景工厂并通过行为测试 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` -- Test: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Task 1 的枚举和两个公开工厂方法;`MapBoundsMm`、`ManualObstacleSource`、`TwoLegObstacleSource`、`Pose2D`、`VehicleParameters`、`HybridAStarConfiguration`。 - -**Produces:** 六个可重复 job 与一个空图演示手动 job,供 UI 入口和后续脚本行为断言共同使用。 - -- [ ] **Step 1: 先建立最小的公共类型和转换辅助函数** - -创建工厂文件并定义以下枚举、转换函数和公共入口。所有输入先做有限值检查;非有限输入抛出 `ArgumentOutOfRangeException`,避免伪造核心请求。 - -```csharp -public enum CoarsePathTestScenario -{ - ExplicitEmpty, - RectangleDetour, - ManualAndTwoLeg, - CacheHit, - ReverseGearSwitch, - NoFeasiblePath, -} - -public static class CoarsePathScenarioFactory -{ - private const double MillimetersPerMeter = 1000d; - private const double DegreesToRadians = Math.PI / 180d; - private const float ResolutionMillimeters = 50f; - private const double ManualMapPaddingMillimeters = 2000d; - - public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) - { - switch (scenario) - { - case CoarsePathTestScenario.ExplicitEmpty: return CreateExplicitEmpty(); - case CoarsePathTestScenario.RectangleDetour: return CreateRectangleDetour(); - case CoarsePathTestScenario.ManualAndTwoLeg: return CreateManualAndTwoLeg(); - case CoarsePathTestScenario.CacheHit: return CreateRectangleDetour(); - case CoarsePathTestScenario.ReverseGearSwitch: return CreateReverseGearSwitch(); - case CoarsePathTestScenario.NoFeasiblePath: return CreateNoFeasiblePath(); - default: throw new ArgumentOutOfRangeException(nameof(scenario)); - } - } - - public static CoarsePathPlanningJob CreateManualGoalDemo( - double startXMillimeters, double startYMillimeters, double startHeadingDegrees, - double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees) - { - EnsureFinite(startXMillimeters, nameof(startXMillimeters)); - EnsureFinite(startYMillimeters, nameof(startYMillimeters)); - EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees)); - EnsureFinite(goalXMillimeters, nameof(goalXMillimeters)); - EnsureFinite(goalYMillimeters, nameof(goalYMillimeters)); - EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees)); - return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters), - ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), - ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); - } - - private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees) - => new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter, - headingDegrees * DegreesToRadians); -} -``` - -- [ ] **Step 2: 实现统一请求模板和六个固定场景** - -使用统一的车辆和配置,避免场景间无意改变安全或搜索语义。模板必须是新对象: - -```csharp -private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal, - TravelDirection? startDirection, GoalDirectionConstraint goalDirection) -{ - return new CoarsePathPlanningJob - { - MapRequest = mapRequest, - Start = start, - Goal = goal, - Vehicle = new VehicleParameters - { - LengthMeters = 0.80d, - WidthMeters = 0.60d, - SafetyMarginMeters = 0.05d, - MaximumCurvaturePerMeter = 1d / 1.20d, - }, - Configuration = new HybridAStarConfiguration(), - StartDirection = startDirection, - GoalDirection = goalDirection, - }; -} -``` - -固定地图均使用 `new MapBoundsMm(0f, 6000f, 0f, 4000f)`、50 mm 分辨率。以下代码固定各场景的障碍来源和世界位姿,所有 `ManualObstacleSource` 版本为 `1L`、所有必需来源为 `true`: - -```csharp -private static CoarsePathPlanningJob CreateExplicitEmpty() - => CreateJob(CreateMap(true, Array.Empty()), - new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); - -private static CoarsePathPlanningJob CreateRectangleDetour() - => CreateJob(CreateMap(false, new IMapObstacleSource[] - { - new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] - { new AxisAlignedRectangleObstacle(2700f, 3300f, 1200f, 2800f) }), - }), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); - -private static CoarsePathPlanningJob CreateManualAndTwoLeg() - => CreateJob(CreateMap(false, new IMapObstacleSource[] - { - new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] - { - new CircleObstacle(2400f, 1300f, 220f), - new AxisAlignedRectangleObstacle(3000f, 3600f, 2000f, 2600f), - }), - new TwoLegObstacleSource("two-leg", 1L, true, - new TwoLegProjectionInput(true, 3900f, 2500f, 0d, - -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")), - }), new Pose2D(1d, 1d, 0d), new Pose2D(5d, 3d, 0d), null, GoalDirectionConstraint.Forward); - -private static CoarsePathPlanningJob CreateNoFeasiblePath() - => CreateJob(CreateMap(false, new IMapObstacleSource[] - { - new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] - { new AxisAlignedRectangleObstacle(2900f, 3100f, 0f, 4000f) }), - }), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); -``` - -`CreateMap` 返回新的 `PlanningMapRequest`,固定写入地图边界、分辨率、给定来源和 `AllowExplicitEmptyMap`。倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)`、`StartDirection=Forward`、`GoalDirection=Reverse`,使路径必须以至少一次换向结束。 - -倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)`、`StartDirection=Forward`、`GoalDirection=Reverse`,使路径必须以至少一次换向结束。若 P0 的离散搜索在此几何下无法稳定得到成功,只允许调整此场景的目标距离或障碍布局,且测试必须继续要求 `IsGearSwitchPoint=true`。 - -`CreateManualDemoMap` 用起终点 X/Y 的最小/最大值各扩展 `ManualMapPaddingMillimeters`,按 50 mm 向外取整,并明确设置 `AllowExplicitEmptyMap=true` 和空的 `ObstacleSources`。 - -- [ ] **Step 3: 扩展行为断言以覆盖所有场景的实际状态** - -在 Task 1 的反射代码之后增加服务执行测试。它不引用任何 Painter 或 MovementTest: - -```powershell -$scenarioService = [Activator]::CreateInstance($serviceType) -foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { - $job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName))) - $result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None)) - Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed." -} - -$reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch'))) -$reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None)) -Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.' - -$noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath'))) -$noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None)) -Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.' -Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.' - -$cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) -$cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) -$cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None)) -$cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None)) -Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.' -Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.' -``` - -- [ ] **Step 4: 运行测试并固定数值场景** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 工厂、单位转换、六个固定场景、缓存与无解断言通过;此时尚未加入 MovementTest 源码检查,因此脚本整体通过。 - -### Task 3: MovementTest 后台会话与完整 Painter 绘制 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` -- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` - -**Consumes:** Task 2 的 `CoarsePathScenarioFactory.Create` 和 `CreateManualGoalDemo`;`CoarsePathPlanningService`、`CoarsePathPlanningJobResult`、`PlanningGridMap`、`Painter`。 - -**Produces:** 六个固定场景入口与一个“AMR 位姿 + 手动终点(空图演示)”入口;所有入口通过同一个后台执行器运行和绘制。 - -- [ ] **Step 1: 写入失败的 UI 源码结构断言** - -在 PowerShell 脚本中加入下列纯文本检查,避免在自动化测试中实例化外部 UI: - -```powershell -$movementTestPath = Join-Path $plannerRoot 'CoarsePath\Test\MovementTest.CoarsePathTest.cs' -if (-not (Test-Path -LiteralPath $movementTestPath -PathType Leaf)) { - throw 'P1 coarse-path MovementTest source file must exist.' -} -$movementTestContent = Get-Content -LiteralPath $movementTestPath -Raw -foreach ($required in @( - '[MovementTest(name = "粗路径-显式空图")]', - '[MovementTest(name = "粗路径-矩形绕行")]', - '[MovementTest(name = "粗路径-多来源障碍")]', - '[MovementTest(name = "粗路径-缓存命中")]', - '[MovementTest(name = "粗路径-倒车换向")]', - '[MovementTest(name = "粗路径-无解")]', - '[MovementTest(name = "粗路径-AMR起点手动终点(空图演示)")]', - 'Task.Run', 'CancellationTokenSource', 'CoarsePathPlanningService', - 'PlanningGridMap', 'IsOccupied', 'ResolutionMm', 'SnapshotId', '图例', 'IsGearSwitchPoint')) { - Assert-True $movementTestContent.Contains($required) "MovementTest must contain: $required" -} -foreach ($forbidden in @('PlanningMapFactory', 'HybridAStarPlanner', 'MapObstacleRasterizer', - 'FootprintCollisionChecker', 'MotionPrimitiveGenerator', 'HybridAStarSearch', - 'BasicPilotBase.Chassis', 'SendMotion', 'DriveTask', '.Wait()', '.Result')) { - Assert-False $movementTestContent.Contains($forbidden) "MovementTest must not depend on: $forbidden" -} -``` - -- [ ] **Step 2: 运行脚本确认 UI 结构检查失败** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 在工厂断言通过后,因 `P1 coarse-path MovementTest source file must exist.` 失败。 - -- [ ] **Step 3: 实现共享会话执行器与七个薄入口** - -在新文件中使用 `namespace MultiWheelC;`,引用 `System.Threading`、`System.Threading.Tasks`、`System.Drawing`、`System.Numerics`、`ClumsyCore`、`MDCSToolBox.Clumsy.Movements` 和 Map/CoarsePath 命名空间。定义一个内部静态执行器,核心形状如下: - -```csharp -internal static class CoarsePathMovementTestRunner -{ - private static readonly object SyncRoot = new object(); - private static readonly CoarsePathPlanningService Service = new CoarsePathPlanningService(); - private static readonly Painter Painter = UI.GetPainter("CoarsePathPlanningV1", true); - private static long _nextRunId; - private static long _activeRunId; - private static CancellationTokenSource _activeCancellation; - private static Task _activeTask; - - internal static void Start(string displayName, CoarsePathPlanningJob job) - { - CancellationTokenSource previous; - long runId; - var cancellation = new CancellationTokenSource(); - lock (SyncRoot) - { - previous = _activeCancellation; - runId = ++_nextRunId; - _activeRunId = runId; - _activeCancellation = cancellation; - Painter.Clear(); - _activeTask = Task.Run(() => Service.Plan(job, cancellation.Token)); - _activeTask.ContinueWith(task => Complete(runId, displayName, job, cancellation, task), - CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); - } - previous?.Cancel(); - } - - internal static void Stop() - { - CancellationTokenSource cancellation; - lock (SyncRoot) - { - cancellation = _activeCancellation; - } - cancellation?.Cancel(); - lock (SyncRoot) - { - if (!ReferenceEquals(_activeCancellation, cancellation)) return; - _activeCancellation = null; - _activeTask = null; - _activeRunId = ++_nextRunId; - } - Painter.Clear(); - } -} -``` - -`Complete` 必须捕获 `task.Exception`,但正常情况下只接受 `CoarsePathPlanningJobResult`。在锁内确认 `runId == _activeRunId`、任务未取消并且 `task.Status == TaskStatus.RanToCompletion` 后才绘制;无论绘制与否都在 finally 中释放该任务专用 `CancellationTokenSource`。不可在锁内等待任务。 - -添加一个抽象 `CoarsePathScenarioMovementTest`,其 `Test` 调用 `CoarsePathMovementTestRunner.Start(DisplayName, CoarsePathScenarioFactory.Create(Scenario))`,其 `TestStop` 调用 `Stop()`。实现六个带固定属性名称的密封子类。第七个类在 `Test` 中只读取一次六个 UI 输入:AMR 起点 X/Y/航向和目标 X/Y/航向(分别为 mm/mm/deg),调用 `CreateManualGoalDemo` 后启动;输入解析失败时仅记录错误并不启动任务。 - -- [ ] **Step 4: 实现确定的地图和结果绘制辅助方法** - -在同一执行器内只消费 `job` 与 `CoarsePathPlanningJobResult`,按固定顺序调用以下辅助方法: - -```csharp -private static void DrawMap(PlanningGridMap map); -private static void DrawPose(Color color, string label, Pose2D pose); -private static void DrawGoalTolerance(Pose2D goal, HybridAStarConfiguration configuration); -private static void DrawSuccessfulPath(PlanningResult result, VehicleParameters vehicle); -private static void DrawLegendAndStatus(string displayName, CoarsePathPlanningJobResult result, int gridStride); -``` - -`DrawMap` 画 `[Bounds.XMin, Bounds.XMax) × [Bounds.YMin, Bounds.YMax)` 的粗边界和 X/Y 参考。`gridStride = Max(1, Ceiling(Max(Rows, Cols) / 100d))`;每 `gridStride` 个真实栅格画一条线,状态文字写入 `分辨率=...mm,显示每...格`。遍历 `row/col`,仅对 `map.IsOccupied(row,col)` 为 true 的单元以四条边线画深色格框,确保显示的是最终快照而非原始几何。 - -`DrawPose` 将 m 转 mm,以圆、朝向短线和标签分别绘制绿色起点、橙色终点。`DrawGoalTolerance` 将位置容差 m 转 mm,绘制橙色容差圆。`DrawSuccessfulPath` 仅在 `result.Status == PlanningStatus.Success` 时运行:相邻路径点按当前点 `Direction` 使用青色(前进)或蓝色(倒车)连线;每隔 10 点画短箭头;`IsGearSwitchPoint` 画紫色圆与“换向”;首、末、换向和每 20 点调用旋转矩形绘制,半长/半宽严格按车辆长宽加安全余量。`DrawLegendAndStatus` 在边界左上方显示边界、占据格、起点、终点、前进、倒车、换向和扩大车体颜色说明,另显示 `SnapshotId`、`MapResult.Status`、`CacheHit`、`PlanningResult.Status`、`Elapsed` 和终止原因。 - -- [ ] **Step 5: 运行构建与集成脚本** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建成功;工厂行为、七入口结构、后台取消约束和 Painter 数据来源检查全部通过。 - -### Task 4: README 与文档断言 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Task 2 的 `CreateManualGoalDemo` 单位契约和 Task 3 的七个入口名称、图例颜色和停止行为。 - -**Produces:** 可独立使用的 P1 UI 说明,以及对其关键安全声明的自动化保护。 - -- [ ] **Step 1: 为 README 写失败断言** - -在现有 README 检查后加入: - -```powershell -foreach ($requiredReadmeText in @( - '## P1:Clumsy 手动测试与可视化', - '粗路径-AMR起点手动终点(空图演示)', - 'AMR 位姿输入:X/Y 使用世界 mm,th 使用 deg', - 'Pose2D:X/Y 使用 m,航向使用 rad', - '显式空图只能用于演示', - '不会发送底盘运动命令', - 'TestStop', - '栅格边界', - '占据格', - '换向')) { - Assert-True $coarsePathReadmeContent.Contains($requiredReadmeText) "CoarsePath README must document: $requiredReadmeText" -} -``` - -- [ ] **Step 2: 运行脚本确认 README 检查失败** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 所有代码检查通过;脚本因 `CoarsePath README must document: ## P1:Clumsy 手动测试与可视化` 失败。 - -- [ ] **Step 3: 在 README 增加 P1 专节** - -在“第一版限制”之前增加 `## P1:Clumsy 手动测试与可视化`,逐项写明: - -1. 七个 MovementTest 名称及对应场景;缓存测试连续运行两次,第二次展示 `Input` 命中。 -2. AMR/手动目标输入契约:世界 `X/Y(mm)`、`th(deg)`,转换成 `Pose2D` 的 m/rad;起点是车辆几何中心。 -3. 空图手动目标入口只能演示坐标、路径和取消流程;现场必须提供真实障碍物快照。 -4. 可视化图例:边界、抽稀格线、占据格、起点、终点及容差、前进、倒车、换向和扩大车体检查框;失败不显示部分路径。 -5. `Test` 在后台规划,`TestStop` 取消令牌并清空图层;测试只显示结果,绝不发送底盘运动命令或执行路径跟踪。 - -- [ ] **Step 4: 运行 README 与集成检查** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建成功;README 和全部 P1 集成检查通过。 - -### Task 5: 全量回归与手动核验说明 - -**Files:** - -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Tasks 1–4 的代码、文档和脚本。 - -**Produces:** 通过 Debug 回归的 P1 UI 集成首个交付;不进入 Release 性能基准。 - -- [ ] **Step 1: 执行 Debug 构建和所有现存 P0/P1 功能脚本** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建 0 errors;每个存在的脚本返回 0 并输出其 `passed` 消息。 - -- [ ] **Step 2: 手动 Clumsy 验收** - -在 Clumsy 的 MovementTest 列表依次运行“粗路径-矩形绕行”和“粗路径-AMR起点手动终点(空图演示)”。检查:测试启动后界面仍可操作;图层拥有边界、格线、占据格、图例、起终点与状态;成功案例有方向区分路径和扩大车体框;点击停止后图层清空且没有任何底盘运动命令。 - -- [ ] **Step 3: 记录交付边界** - -在完成报告中明确:P1 UI 集成已完成;下一 P1 子项目是 Release 性能、资源和确定性基准;TrapMap 迁移/清理继续排除;没有执行 Git 操作。 - -## 自检 - -- 覆盖性:Task 2 交付纯场景与单位转换;Task 3 交付后台七入口和完整视觉要素;Task 4 交付 README;Task 5 交付自动化与手动验收。规格中的空图限制、实际占据快照、停止语义、无底盘命令和不显示部分路径均有对应任务。 -- 占位符:已检查任务不含未决占位、延后实现或泛化错误处理类措辞;每个实现任务均给出文件、接口、测试、命令和具体代码形状。 -- 类型一致性:所有任务统一使用 `CoarsePathTestScenario`、`CoarsePathScenarioFactory.Create`、`CreateManualGoalDemo`、`CoarsePathPlanningJob`、`CoarsePathPlanningJobResult` 和 `PlanningGridMap`;AMR 输入始终是 mm+deg,核心位姿始终是 m+rad。 diff --git a/docs/superpowers/plans/2026-07-27-planning-operation-budget.md b/docs/superpowers/plans/2026-07-27-planning-operation-budget.md deleted file mode 100644 index f58ccc6..0000000 --- a/docs/superpowers/plans/2026-07-27-planning-operation-budget.md +++ /dev/null @@ -1,369 +0,0 @@ -# 规划操作预算与诊断收尾实施计划 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 让一次粗规划调用在建图、距离场、Dijkstra 和 Hybrid A* 阶段共用可取消的总超时预算,并发布真实的 Open List 诊断计数。 - -**Architecture:** 在 `Utils` 新增内部 `PlanningOperationBudget`,它不依赖 Map 或 CoarsePath,只报告继续、取消、超时。Map 与搜索分别将该中立结果映射为自己的结果;`CoarsePathPlanningService` 创建唯一预算并传递给下层。原有公开的无预算入口保持兼容,门面使用内部带预算入口。 - -**Tech Stack:** C# 10、.NET Standard 2.0、PowerShell 反射回归脚本、`Stopwatch`、`CancellationToken`。 - -## Global Constraints - -- 位置使用 m、地图输入使用 mm、航向使用 rad、曲率使用 1/m;不得改变现有单位边界。 -- 取消或超时必须返回空路径和空方向分段,绝不发布部分路径或部分 `PlanningGridMap`。 -- 地图不得依赖 CoarsePath;共享预算只能放在 `ParkrobTrajplanner/Utils`。 -- 每 256 个或更少循环工作单元检查一次预算;外部 `IMapObstacleSource.ProjectToWorld()` 是调用方提供的同步快照接口,只能在调用前后检查,不能强制抢占其内部执行。 -- 不改变碰撞保守性、运动原语、代价公式、目标候选保护或 Open List 排序。 -- 保留 `PlanningMapFactory.Create(request)`、`HybridAStarPlanner.Plan(request, token)`、`HybridAStarSearch.Search(request, token)` 和 `GridDijkstraHeuristic(map,row,col)` 的兼容入口。 -- 不执行 Git 添加、提交、重置或工作区清理。 - ---- - -## 文件结构 - -| 文件 | 职责 | -| --- | --- | -| `Utils/PlanningOperationBudget.cs` | 内部单调计时、取消检查和统一停止原因。 | -| `Map/PlanningMapBuildResult.cs` | 地图构建状态:成功、失败、取消、超时。 | -| `Map/Core/EnvironmentMapBuilder.cs`、`EnvironmentMapBuildResult.cs` | 将预算传入来源处理与栅格化,并保留停止原因。 | -| `Map/Obstacles/MapObstacleRasterizer.cs` | 在圆形/矩形逐格写入期间定期停止。 | -| `Map/Planning/{PlanningMapAdapter,ObstacleDistanceField,EuclideanDistanceTransform}.cs` | 在占据复制、EDT 和距离换算期间定期停止且不产出快照。 | -| `Map/PlanningMapFactory.cs` | 可取消地等待创建锁、检查缓存、建图、哈希和写缓存。 | -| `CoarsePath/Search/{GridDijkstraHeuristic,HybridAStarSearch}.cs` | 为 Dijkstra 和 Open List 使用共享预算,记录陈旧条目和峰值。 | -| `CoarsePath/{HybridAStarPlanner,Contracts/PlanningDiagnostics}.cs` | 将搜索统计传给最终结果,并让公开 Planner 包装兼容预算。 | -| `CoarsePath/Facade/CoarsePathPlanningService.cs` | 创建一次调用唯一预算,映射地图阶段停止状态。 | -| `Map/README.md`、`CoarsePath/README.md` | 分别说明地图构建状态,以及粗规划总超时和取消状态。 | -| `tests/verify_planning_map_factory.ps1`、`verify_planning_map_adapter.ps1`、`verify_coarse_path_search.ps1`、`verify_coarse_path_integration.ps1` | 预算与诊断的反射回归覆盖。 | - -### Task 1: 建立独立的操作预算与地图终止契约 - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs` -- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` - -**Consumes:** `System.Diagnostics.Stopwatch`、`System.Threading.CancellationToken`。 - -**Produces:** `PlanningOperationStopReason`、`PlanningOperationBudget`、`PlanningMapBuildStatus`;下游 Map/CoarsePath 均只通过这些类型传递停止信息。 - -- [ ] **Step 1: 写失败测试,锁定新的地图状态与预算公开反射形状。** - - 在现存的 `verify_planning_map_factory.ps1` 断言存在内部 `MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget` 与三值 `PlanningOperationStopReason`,并断言 `PlanningMapBuildResult.Status` 存在;已取消、已超时结果的 `Succeeded` 必须为 `false`、`Map` 为 `$null`、`CacheHit` 为 `None`。 - - ```powershell - $statusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) - Assert-True ($statusType.GetEnumNames() -contains 'Cancelled') 'Map build status must expose cancellation.' - Assert-True ($statusType.GetEnumNames() -contains 'TimedOut') 'Map build status must expose timeout.' - Assert-True ($resultType.GetProperty('Status') -ne $null) 'Map result must expose an explicit status.' - ``` - -- [ ] **Step 2: 运行两个脚本,确认因类型或属性不存在而失败。** - - Run: - - ```powershell - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 - ``` - - Expected: 断言报告 `PlanningOperationBudget` 或 `PlanningMapBuildStatus` 缺失。 - -- [ ] **Step 3: 实现最小共享预算和显式地图状态。** - - `PlanningOperationBudget` 的核心接口固定如下;超时使用构造时启动的单调 `Stopwatch`,取消优先于超时: - - ```csharp - internal enum PlanningOperationStopReason { None, Cancelled, TimedOut } - - internal sealed class PlanningOperationBudget - { - internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout); - internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken); - internal TimeSpan Elapsed { get; } - internal PlanningOperationStopReason GetStopReason(); - internal PlanningOperationStopReason CheckEvery(ref int workItemCount); - } - ``` - - `CheckEvery` 在第一次工作单元以及每 256 个工作单元检查;`Unlimited` 不启用时间限制但仍响应取消。将地图结果从布尔构造改为状态构造: - - ```csharp - public enum PlanningMapBuildStatus { Success, Failed, Cancelled, TimedOut } - - public PlanningMapBuildStatus Status { get; } - public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } } - - internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason reason, - IReadOnlyList sourceResults) - { - return new PlanningMapBuildResult( - reason == PlanningOperationStopReason.Cancelled - ? PlanningMapBuildStatus.Cancelled - : PlanningMapBuildStatus.TimedOut, - reason == PlanningOperationStopReason.Cancelled ? "地图创建已取消。" : "地图创建已超时。", - sourceResults, PlanningMapCacheHit.None, null); - } - ``` - -- [ ] **Step 4: 重跑两个脚本,确认新契约通过且旧地图缓存断言未回归。** - - Run: 与 Step 2 相同。 - - Expected: 地图工厂脚本输出 `Planning map factory checks passed.`。 - -### Task 2: 让地图创建、栅格化和距离场遵守预算 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs` -- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` -- Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` - -**Consumes:** Task 1 的 `PlanningOperationBudget` 与 `PlanningOperationStopReason`。 - -**Produces:** `PlanningMapFactory` 的内部 `Create(PlanningMapRequest, PlanningOperationBudget)`;它在取消/超时时返回 `PlanningMapBuildResult.Stopped`,不会写入 LRU 缓存。 - -- [ ] **Step 1: 写失败测试,覆盖预先取消、EDT 中超时与缓存不污染。** - - 在工厂脚本创建一个已取消的 `CancellationTokenSource`,通过反射调用新的内部带预算 `Create`,断言: - - ```powershell - Assert-Equal 'Cancelled' $cancelledMapResult.Status.ToString() 'Cancelled map construction must report cancellation.' - Assert-False $cancelledMapResult.Succeeded 'Cancelled map construction must not succeed.' - Assert-Null $cancelledMapResult.Map 'Cancelled map construction must not publish a map.' - Assert-Equal 'None' $cancelledMapResult.CacheHit.ToString() 'Stopped construction must not publish a cache hit.' - ``` - - 在适配器脚本为含障碍的大栅格创建 `PlanningOperationBudget`,使用零超时调用内部 `TryCreate`,断言返回 `TimedOut` 且输出 `PlanningGridMap` 为 `$null`。随后用无预算入口再次创建相同地图,断言距离场仍可用,以证明停止时没有污染输入或缓存。 - -- [ ] **Step 2: 运行地图工厂和适配器脚本,确认新增反射入口缺失而失败。** - - Run: - - ```powershell - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 - ``` - - Expected: 新带预算 `Create` 或 `TryCreate` 反射查找失败。 - -- [ ] **Step 3: 在地图管线的全部长循环中传递并检查预算。** - - 实现以下内部接口,所有 `Try*` 在停止时返回 `false` 并把 `stopReason` 设为非 `None`;普通参数错误仍按现有失败原因或异常处理。 - - ```csharp - internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget); - internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle, - PlanningOperationBudget budget, out PlanningOperationStopReason stopReason); - internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget, - out PlanningGridMap map, out PlanningOperationStopReason stopReason); - internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters, - PlanningOperationBudget budget, out ObstacleDistanceField field, - out PlanningOperationStopReason stopReason); - internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols, - PlanningOperationBudget budget, out double[] squared, - out PlanningOperationStopReason stopReason); - internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget); - ``` - - 保留现有公开 `Build`、`Rasterize`、`PlanningMapAdapter.Create`、`ObstacleDistanceField.Create` 与 `ComputeSquaredDistances`,让它们以 `PlanningOperationBudget.Unlimited(CancellationToken.None)` 包装新入口。对圆形/矩形逐格循环、EDT 两遍扫描、`Transform1D` 两个 `q` 循环、距离场扫描均调用 `budget.CheckEvery(ref workItemCount)`。 - - 工厂以 `Monitor.TryEnter(_createGate, 16)` 循环等待创建锁;每次失败后检查预算。拿到锁后立刻再检查预算,随后在缓存读、来源处理、适配、占据哈希和每次缓存写入前检查。将 SHA-256 改为每 4096 字节调用 `TransformBlock` 的增量哈希,并在块间检查预算。任一非 `None` 停止原因直接返回 `PlanningMapBuildResult.Stopped`,且不会执行 `_cache.AddOccupancy` 或 `_cache.AddInput`。 - -- [ ] **Step 4: 重跑地图脚本,确认普通建图、两级缓存和新停止结果同时通过。** - - Run: 与 Step 2 相同。 - - Expected: `Planning map factory checks passed.` 与 `Planning map adapter checks passed.`。 - -### Task 3: 为 Dijkstra 与 Hybrid A* 使用同一预算并记录 Open List 统计 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Task 1 的预算和 Task 2 不可变地图;现有 `BinaryMinHeap`。 - -**Produces:** `GridDijkstraHeuristic.TryCreate`、带共享预算的内部搜索/规划入口,以及 `HybridAStarSearchResult.StaleOpenListEntryCount` 和 `PeakOpenListCount`。 - -- [ ] **Step 1: 写失败测试,覆盖 Dijkstra 中取消、总超时和统计透传。** - - 在搜索脚本上创建至少 500×500 格的已就绪空地图,在地图创建完成后启动 `CancellationTokenSource.CancelAfter(1)` 并调用搜索。断言返回 `Cancelled`、`SuccessNodeIndex` 为 `$null`、运行时间小于 2 秒。再用同一地图和 `SearchTimeout = TimeSpan.Zero` 断言 `SearchTimeout`,以证明启发式创建前即尊重总预算。 - - 反射断言搜索结果的新属性存在,并让直接路径搜索至少满足: - - ```powershell - Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entries.' - Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak size.' - Assert-True ($searchResult.PeakOpenListCount -ge 1) 'A successful search must retain at least one Open List entry.' - Assert-True ($searchResult.StaleOpenListEntryCount -ge 0) 'Stale Open List entry count must never be negative.' - ``` - - 在集成脚本断言 `PlanningResult.Diagnostics` 中的两项值与搜索结果一致;对产生重开/失效条目的固定障碍场景断言陈旧条目数大于零。 - -- [ ] **Step 2: 运行搜索和集成脚本,确认新增属性、带预算入口或中途取消断言失败。** - - Run: - - ```powershell - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 - ``` - - Expected: 新属性或 Dijkstra 中止行为缺失导致失败;现有“搜索前取消”检查不应被视为通过中途取消测试。 - -- [ ] **Step 3: 实现预算感知的 Dijkstra、搜索与真实诊断。** - - `GridDijkstraHeuristic` 保留当前公开构造函数,并增加内部可失败构建: - - ```csharp - internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol, - PlanningOperationBudget budget, out GridDijkstraHeuristic heuristic, - out PlanningOperationStopReason stopReason); - ``` - - `Build` 的每次出堆与每 256 个邻居检查预算;停止时不返回部分启发式。`HybridAStarSearch.Search(request, token)` 从 `request.Configuration.SearchTimeout` 创建预算作为兼容包装,新增内部: - - ```csharp - internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget); - ``` - - 删除搜索器内部新建的 `Stopwatch` 与 `IsTimedOut`,所有原有取消/超时位置改为读取 `budget.GetStopReason()` 并精确映射为 `PlanningStatus.Cancelled` 或 `PlanningStatus.SearchTimeout`。Dijkstra 返回停止原因时立即返回对应搜索状态;节点上限仍只在预算检查之后、真正扩展之前检查。 - - 扩展搜索结果构造函数与只读属性: - - ```csharp - public int StaleOpenListEntryCount { get; } - public int PeakOpenListCount { get; } - ``` - - 每次 `openList.Push` 后执行 `peakOpenListCount = Math.Max(peakOpenListCount, openList.Count)`。普通节点出堆后因 best-G 已更新、节点索引不匹配或已关闭而跳过时递增 `staleOpenListEntryCount`;目标候选的出队复核失败不算陈旧条目。所有 `CreateResult` 调用传递两项计数。 - - `HybridAStarPlanner` 的公开 `Plan` 保持签名并建立自己的预算;新增内部 `Plan(request, budget)` 供门面调用。诊断使用 `budget.Elapsed`,并把两个搜索统计填入原本为零的构造参数: - - ```csharp - searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, - searchResult == null ? 0 : searchResult.PeakOpenListCount, - ``` - -- [ ] **Step 4: 重跑搜索和集成脚本,确认取消、超时、目标候选与统计均通过。** - - Run: 与 Step 2 相同。 - - Expected: `Coarse path search primitive checks passed.`、`Coarse path Hybrid A star search checks passed.`、`Coarse path integration checks passed.` 和 `Coarse path facade checks passed.`。 - -### Task 4: 让业务门面映射地图阶段终止状态并更新调用文档 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/README.md` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Task 1 的地图状态、Task 2 的带预算工厂入口、Task 3 的内部 Planner 入口。 - -**Produces:** 一次 `CoarsePathPlanningService.Plan` 的统一预算和对调用方稳定的 `PlanningStatus` 映射。 - -- [ ] **Step 1: 写失败测试,锁定门面状态映射和空结果。** - - 在集成脚本使用已取消 Token 调用门面,断言: - - ```powershell - Assert-Equal 'Cancelled' $facadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.' - Assert-Equal 'Cancelled' $facadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.' - Assert-Equal 0 $facadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.' - Assert-Equal 0 $facadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.' - ``` - - 对 `SearchTimeout = TimeSpan.Zero` 的有效 job,断言地图结果和规划结果分别为 `TimedOut`、`SearchTimeout`,且调试 sink 不会把已停止操作改写为成功。 - -- [ ] **Step 2: 运行集成脚本,确认当前门面把地图阶段停止误报为 `InvalidMap` 或继续建图。** - - Run: - - ```powershell - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 - ``` - - Expected: 地图状态属性或正确的 `Cancelled`/`SearchTimeout` 映射不存在。 - -- [ ] **Step 3: 让门面创建并传递唯一预算,随后更新 README。** - - 门面从有效 `job.Configuration.SearchTimeout` 创建 `PlanningOperationBudget`;配置为空或时间值非法时使用无超时预算,让既有 Planner 预检继续返回原有无效配置状态。依次调用: - - ```csharp - PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget); - if (!mapResult.Succeeded) - { - PlanningStatus status = mapResult.Status == PlanningMapBuildStatus.Cancelled - ? PlanningStatus.Cancelled - : mapResult.Status == PlanningMapBuildStatus.TimedOut - ? PlanningStatus.SearchTimeout - : PlanningStatus.InvalidMap; - return PublishDebug(job, mapResult, PlanningResult.Failure(status, diagnostics)); - } - PlanningResult planningResult = _planner.Plan(request, budget); - ``` - - `Map/README.md` 在 `PlanningMapBuildResult` 的说明处增加 `Status`:`Success`、`Failed`、`Cancelled`、`TimedOut`;后两种不提供地图也不会进入缓存。`CoarsePath/README.md` 增加“总预算与取消”小节:`SearchTimeout` 是从门面开始的总预算,覆盖建图、距离场、Dijkstra 与 Hybrid A*;`Cancelled`/`SearchTimeout` 一律无路径;不应以 `InvalidMap` 重试用户主动取消。 - -- [ ] **Step 4: 重跑集成脚本,确认门面状态映射、缓存复用和 debug 旁路隔离均通过。** - - Run: 与 Step 2 相同。 - - Expected: `Coarse path integration checks passed.` 和 `Coarse path facade checks passed.`。 - -### Task 5: 全量回归与验收记录 - -**Files:** - -- Modify only if a command reveals a concrete regression: the exact responsible source or test file from Tasks 1–4. - -- [ ] **Step 1: 执行 Debug 构建。** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - ``` - - Expected: `0 个警告`、`0 个错误`。 - -- [ ] **Step 2: 执行全部现行 P0 地图与粗规划回归。** - - Run: - - ```powershell - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 - powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 - ``` - - Expected: 每个脚本退出码为 0 并输出 `passed`。 - -- [ ] **Step 3: 对照设计完成验收。** - - 逐项检查:预先取消和中途 Dijkstra 取消均返回 `Cancelled`;零总超时返回 `SearchTimeout`;地图停止不创建快照或缓存条目;正常输入的路径与缓存行为不变;诊断两项不再硬编码为零;README 已说明总预算语义。 - -## 自检 - -- 规格覆盖:Task 1 定义共享预算和显式地图状态;Task 2 覆盖地图、EDT、锁和缓存;Task 3 覆盖 Dijkstra、Hybrid A*、统计和 Planner;Task 4 覆盖门面映射与文档;Task 5 覆盖完整回归。 -- 类型一致性:所有耗时组件仅接收 `PlanningOperationBudget` 并输出 `PlanningOperationStopReason`;Map 使用 `PlanningMapBuildStatus`,粗规划使用既有 `PlanningStatus`。 -- 范围:不触及 UI、Painter、场景工厂、Release 基准、运动模型或旧 TrapMap 脚本。 diff --git a/docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md b/docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md deleted file mode 100644 index 92e539d..0000000 --- a/docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md +++ /dev/null @@ -1,436 +0,0 @@ -# 固定粗路径案例使用实时 AMR 位姿 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 让全部 `粗路径规划-*` 固定 MovementTest 从一次冻结的当前 AMR 世界坐标和航向开始规划,并把案例地图、目标和障碍平移到 AMR 附近。 - -**Architecture:** `CoarsePathScenarioFactory` 保留确定性的基准案例入口,并新增接受 AMR mm/deg 位姿的入口。一个仅属于固定场景工厂的平移对象将基准案例的起点映射为 AMR 起点,平移地图和障碍,并以起终点基准航向差计算新的终点航向。MovementTest 运行器在前台读取一次 `DetourInterface.getCartLocation()`,校验并冻结快照后才提交已有后台规划流程。 - -**Tech Stack:** C# / .NET Standard 2.0、Clumsy `MovementTest` 与 Painter、现有 `CoarsePathPlanningService`、PowerShell 反射验证脚本。 - -## Global Constraints - -- 保留 `CoarsePathScenarioFactory.Create(CoarsePathTestScenario)` 的现有确定性行为,自动化离线测试继续使用它。 -- 新实时入口的 X/Y 单位为世界 mm、航向单位为 deg;核心 `Pose2D` 仍为 m/rad。 -- 固定场景仅做位置平移;TwoLeg 的 `DetectionHeadingRadians` 不得因 AMR 航向改变。 -- 起点航向必须等于冻结 AMR 航向;终点航向必须保持基准案例的起终点航向差,并规范化到 `[-pi, pi]`。 -- 不改变 Hybrid A*、车辆参数、碰撞模型、地图缓存实现、手动 `粗路径规划` 的终点/障碍/超时输入流程,或任何底盘控制行为。 -- 实时位姿为空、读取抛异常或 X/Y/航向不是有限数时,不得创建后台任务;必须向用户报告以 `AMR 位姿不可用` 开头的原因。 -- 不暂存工作区中的无关改动;每次提交均明确列出文件路径。 - -## File Structure - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` - - 固定场景的基准坐标定义、实时 AMR 工厂重载、平移与航向转换的唯一实现位置。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` - - 固定场景的 AMR 快照读取、输入失败提示,以及冻结位姿的 Painter 状态显示。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - - 对新工厂重载、平移几何、航向规则、缓存和基准入口不回归的程序集级验证。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` - - 对运行器只读一次实时位姿、传入实时工厂入口及 AMR 失败/状态文案的源级验证。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - - 说明六个固定案例的实时锚定规则、缓存命中条件和定位异常行为。 - ---- - -### Task 1: 为固定案例建立可验证的实时 AMR 平移工厂 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:303-399` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs:115-284` - -**Interfaces:** - -- Consumes: `CoarsePathTestScenario`、`PlanningMapRequest`、`MapBoundsMm`、`ManualObstacleSource`、`TwoLegObstacleSource` 和 `TwoLegProjectionInput`。 -- Produces: `public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。 -- Produces: 每个固定案例以 `FixedScenarioTransform` 统一转换的起点、终点、地图边界和来源快照;旧 `Create(scenario)` 仍创建原始基准几何。 - -- [ ] **Step 1: 在程序集级验证脚本中写出失败测试** - - 在 `$factoryCreate` 定义之后加入实时重载查找;在基准案例循环之后加入以下断言。测试锚点 `(12000, -3000, 90)` 对单矩形场景的偏移应为 `(+11000, -5000)` mm;多来源锚点 `(7000, 8000, 45)` 对该基准场景的偏移应为 `(+6000, +7000)` mm。 - - ```powershell - $factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @( - $scenarioEnumType, [double], [double], [double]) - Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).' - - foreach ($scenarioName in $scenarioNames) { - $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) - $liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0)) - Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X." - Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y." - Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading." - } - - $rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour') - $liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0)) - Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.' - Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.' - Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.' - Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.' - Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.' - Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.' - Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.' - Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.' - $rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld() - $rectangleObstacle = $rectangleProjection.Obstacles[0] - Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.' - Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.' - - $multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg') - $baselineMulti = $factoryCreate.Invoke($null, @($multiScenario)) - $liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0)) - Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.' - Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.' - Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.' - $baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles - $liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles - Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.' - Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.' - - $noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath') - $liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0)) - Assert-Near 9000.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.' - Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.' - Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.' - - $invalidLivePoseRejected = $false - try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) } - catch [Reflection.TargetInvocationException] { - $invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] - } - Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.' - ``` - -- [ ] **Step 2: 运行该测试,确认它因缺少实时工厂重载而失败** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 - ``` - - Expected: build succeeds; the script fails at `Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).` - -- [ ] **Step 3: 以单一平移对象实现实时工厂重载** - - 在 `CoarsePathScenarioFactory` 中保留一参 `Create`,并用一个私有锚点和变换对象驱动同一个 switch。使用以下接口形状;`CreateCore` 的每个 case 都先以该案例的基准起点创建 `FixedScenarioTransform`,再传给对应的场景构造函数。 - - ```csharp - public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) - { - return CreateCore(scenario, null); - } - - public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, - double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees) - { - return CreateCore(scenario, - new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees)); - } - - private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, - FixedScenarioAnchor anchor) - { - switch (scenario) - { - case CoarsePathTestScenario.ExplicitEmpty: - return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - case CoarsePathTestScenario.RectangleDetour: - case CoarsePathTestScenario.CacheHit: - return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - case CoarsePathTestScenario.ManualAndTwoLeg: - return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor)); - case CoarsePathTestScenario.ReverseGearSwitch: - return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - case CoarsePathTestScenario.NoFeasiblePath: - return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); - default: - throw new ArgumentOutOfRangeException(nameof(scenario)); - } - } - ``` - - Implement the two private classes in the same file. They must validate all public live inputs with existing `EnsureFinite`, keep the identity transform when `anchor` is null, convert fixed mm to metres only when creating `Pose2D`, and normalize headings without changing a position. - - ```csharp - private sealed class FixedScenarioAnchor - { - public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees) - { - EnsureFinite(xMillimeters, nameof(xMillimeters)); - EnsureFinite(yMillimeters, nameof(yMillimeters)); - EnsureFinite(headingDegrees, nameof(headingDegrees)); - XMillimeters = xMillimeters; - YMillimeters = yMillimeters; - HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians); - } - - public double XMillimeters { get; } - public double YMillimeters { get; } - public double HeadingRadians { get; } - } - - private sealed class FixedScenarioTransform - { - private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians) - { - DeltaXMillimeters = deltaXMillimeters; - DeltaYMillimeters = deltaYMillimeters; - HeadingDeltaRadians = headingDeltaRadians; - } - - public double DeltaXMillimeters { get; } - public double DeltaYMillimeters { get; } - public double HeadingDeltaRadians { get; } - - public static FixedScenarioTransform From(double baselineStartXMillimeters, - double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor) - { - if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d); - return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters, - anchor.YMillimeters - baselineStartYMillimeters, - NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians)); - } - - public float X(float value) { return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); } - public float Y(float value) { return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); } - public Pose2D Pose(double xMeters, double yMeters, double headingRadians) - { - return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter, - (yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter, - NormalizeRadians(headingRadians + HeadingDeltaRadians)); - } - } - - private static double NormalizeRadians(double angle) - { - double normalized = angle % (2d * Math.PI); - if (normalized <= -Math.PI) return normalized + 2d * Math.PI; - return normalized > Math.PI ? normalized - 2d * Math.PI : normalized; - } - ``` - - Update every fixed builder to accept `FixedScenarioTransform transform` and use `transform.Pose`, `transform.X` and `transform.Y` for all coordinates: - - ```csharp - new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f), - transform.Y(1200f), transform.Y(2800f)); - new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f); - new TwoLegProjectionInput(true, transform.X(3900f), transform.Y(2500f), 0d, - -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot."); - ``` - - Change `CreateMap` to accept the transform and translate all four `MapBoundsMm` limits with the correct axis. It must not change resolution, source IDs, source versions, required flags, empty-map flags, vehicle parameters, direction constraints or existing timeout assignments. - -- [ ] **Step 4: 运行工厂和回归验证,确认新旧入口都通过** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 - ``` - - Expected: build has zero errors; script ends with `Coarse path P1 scenario checks passed.` Existing assertions for the one-argument factory must continue to pass. - -- [ ] **Step 5: 提交仅包含工厂与程序集级测试的可审查变更** - - ```powershell - git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1 - git commit -m "feat: anchor coarse path scenarios to AMR pose" - ``` - -### Task 2: 让固定 MovementTest 冻结 AMR 快照并显示诊断 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-52` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:221-371,512-540` - -**Interfaces:** - -- Consumes: 新的 `CoarsePathScenarioFactory.Create(scenario, xMm, yMm, headingDeg)`、`DetourInterface.getCartLocation()` 和现有 `Run(string, CoarsePathPlanningJob)`。 -- Produces: `RunScenario` 只读取一次位姿,创建已冻结的实时 job;手动入口继续调用现有两参 `Run`。 -- Produces: 私有 `AmrPoseSnapshot`,仅保存有限 X/Y/deg 和格式化后的状态文本,不向规划后台读取定位。 - -- [ ] **Step 1: 在 UI 源级验证中加入失败断言** - - 在现有 `$source` 断言后追加以下代码。使用 ASCII 的方法名和 `AMR` 文本,避免 Windows PowerShell 5 对中文脚本字符串的编码歧义。 - - ```powershell - $runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner') - Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.' - $runnerSource = $source.Substring($runnerStart) - Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.' - Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.' - Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.' - Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.' - Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.' - Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.' - ``` - -- [ ] **Step 2: 运行 UI 脚本,确认新增实时位姿约束尚未满足** - - Run: - - ```powershell - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 - ``` - - Expected: script fails at `Fixed scenarios must read the current AMR pose.` or `Fixed scenarios must use the AMR-aware factory overload.` - -- [ ] **Step 3: 实现单次 AMR 读取、校验、冻结与绘制传递** - - 在 `CoarsePathPlanningTestRunner` 内加入以下私有快照类型和工厂调用路径。任何读取或校验异常都在前台转换为 `AMR 位姿不可用:...`,因此不会进入 `Task.Run`。 - - ```csharp - private sealed class AmrPoseSnapshot - { - public AmrPoseSnapshot(double xMillimeters, double yMillimeters, double headingDegrees) - { - EnsureFiniteAmrValue(xMillimeters, "X"); - EnsureFiniteAmrValue(yMillimeters, "Y"); - EnsureFiniteAmrValue(headingDegrees, "航向"); - XMillimeters = xMillimeters; - YMillimeters = yMillimeters; - HeadingDegrees = headingDegrees; - } - - public double XMillimeters { get; } - public double YMillimeters { get; } - public double HeadingDegrees { get; } - - public string DisplayText - { - get - { - return "AMR 起点:X=" + XMillimeters.ToString("F0", CultureInfo.InvariantCulture) + - " mm,Y=" + YMillimeters.ToString("F0", CultureInfo.InvariantCulture) + - " mm,航向=" + HeadingDegrees.ToString("F1", CultureInfo.InvariantCulture) + " deg"; - } - } - } - - private static void EnsureFiniteAmrValue(double value, string name) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - throw new ArgumentException(name + " 必须是有限数。"); - } - - internal static void RunScenario(CoarsePathTestScenario scenario, string scenarioName) - { - try - { - var pose = DetourInterface.getCartLocation(); - var snapshot = new AmrPoseSnapshot(pose.x, pose.y, pose.th); - CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenario, - snapshot.XMillimeters, snapshot.YMillimeters, snapshot.HeadingDegrees); - Run(scenarioName, job, snapshot); - } - catch (Exception exception) - { - ShowInputFailure(new ArgumentException("AMR 位姿不可用:" + exception.Message, exception)); - } - } - ``` - - Keep the existing `internal static void Run(string scenarioName, CoarsePathPlanningJob job)` as a thin compatibility overload that calls a new private `Run(string, CoarsePathPlanningJob, AmrPoseSnapshot)`. Thread the snapshot through `DrawPending`, the `ContinueWith` lambda, `Finish`, `DrawResult`, and `DrawStatus`. In `DrawStatus`, render `snapshot.DisplayText` after the scene line only when the snapshot is non-null; increase subsequent Y offsets consistently so vehicle and termination-reason text do not overlap. Manual `CoarsePathPlanningTest.Test()` must keep its present `Run(..., job)` call and user-entered timeout behavior. - -- [ ] **Step 4: 编译并运行 UI 验证,确认后台边界未回归** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 - ``` - - Expected: build has zero errors; script ends with `Coarse path UI source checks passed.` Existing assertions still confirm one shared service, background `Task.Run`, cancellation and no blocking `Task.Result`/`Wait`. - -- [ ] **Step 5: 提交仅包含运行器与 UI 验证的可审查变更** - - ```powershell - git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/tests/verify_coarse_path_ui.ps1 - git commit -m "feat: use live AMR pose in coarse path tests" - ``` - -### Task 3: 记录实时固定场景语义并执行完整回归 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:289-347` -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:54-100` - -**Interfaces:** - -- Consumes: 已实现的实时工厂入口和 MovementTest 状态文本。 -- Produces: 可供现场使用者理解的固定案例锚定、缓存和定位异常说明;文档验证继续只检查稳定 ASCII 标识符。 - -- [ ] **Step 1: 在 README 源级验证中加入失败断言** - - 在 `$readmeStructure` 数组中加入以下 ASCII 项,并在数组后加入 `Contains` 断言: - - ```powershell - foreach ($requiredLiveAmrText in @( - 'Create(CoarsePathTestScenario scenario, double amrXMillimeters', - 'AMR', - 'Input', - 'TwoLeg')) { - Assert-True ($readme.Contains($requiredLiveAmrText)) "P1 README must document live fixed scenarios: $requiredLiveAmrText." - } - ``` - -- [ ] **Step 2: 运行 UI 文档验证,确认文档尚未描述实时固定案例** - - Run: - - ```powershell - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 - ``` - - Expected: script fails because README does not yet contain the new `Create(CoarsePathTestScenario scenario, double amrXMillimeters` signature. - -- [ ] **Step 3: 更新 P1 手动测试与可视化章节** - - 在固定案例表之前新增“固定案例的实时 AMR 锚点”小节,逐条说明:六个 `粗路径规划-*` 入口读取一次 `getCartLocation`;起点等于这份冻结的世界 mm/deg 位姿;地图边界、目标、圆形/轴对齐矩形和 TwoLeg 检测原点统一平移;终点航向保持和基准起点的航向差;TwoLeg 朝向不旋转;手动 `粗路径规划` 的输入流程不变。 - - 在同一小节以单行 inline code 明确列出公开签名:`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。 - - 加入公开签名示例: - - ```csharp - CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create( - CoarsePathTestScenario.RectangleDetour, - amrXMillimeters, amrYMillimeters, amrHeadingDegrees); - ``` - - 将缓存命中表项改为“相同 AMR X/Y 下的重复完整地图输入”;明确 AMR 位置已移动时未命中是正常的 `None`/非 `Input` 缓存结果,且仅航向变化不改变地图输入、仍可命中。补充运行器在定位读取、空值访问或有限数校验失败时显示 `AMR 位姿不可用` 且不启动后台规划;状态区会显示该次冻结的 AMR 起点。保留关于 `getCartLocation` 可能阻塞和必须在定位准备完成后运行的既有警告。 - -- [ ] **Step 4: 运行完整粗路径与地图回归集** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 - ``` - - Expected: build reports zero warnings and zero errors. Every verification script exits 0; integration ends with `Coarse path P1 scenario checks passed.` and UI ends with `Coarse path UI source checks passed.` - -- [ ] **Step 5: 提交文档与最后验证脚本变更** - - ```powershell - git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1 - git commit -m "docs: explain live AMR coarse path scenarios" - ``` diff --git a/docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md b/docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md deleted file mode 100644 index 929f053..0000000 --- a/docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md +++ /dev/null @@ -1,256 +0,0 @@ -# Coarse Path Search Elapsed Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 在保留总耗时和总超时语义的前提下,记录并显示地图就绪后生成最终粗路径的独立耗时。 - -**Architecture:** `PlanningDiagnostics` 增加兼容的 `PathSearchElapsed` 只读字段。`HybridAStarPlanner` 在调用 `HybridAStarSearch.Search` 前启动本地秒表,并把搜索、回溯、装配和最终复核的耗时传入诊断对象;门面总预算和 `Elapsed` 不改变。MovementTest 图层和 Toast 同时显示总耗时与路径搜索耗时。 - -**Tech Stack:** C# / .NET Standard 2.0、`System.Diagnostics.Stopwatch`、PowerShell 回归脚本、Clumsy `MovementTest` Painter。 - -## Global Constraints - -- `PlanningDiagnostics.Elapsed` 继续表示从 `CoarsePathPlanningService.Plan` 入口开始的总耗时。 -- `PathSearchElapsed` 不包括地图来源读取、缓存、栅格化和距离场构建。 -- `PathSearchElapsed` 包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核。 -- 搜索开始前失败时 `PathSearchElapsed` 必须为 `TimeSpan.Zero`;搜索阶段失败时保留已消耗时间。 -- 新构造函数参数必须放在现有参数之后并提供默认值,保持现有位置参数调用的兼容性。 -- 不修改 `PlanningOperationBudget`、取消机制、超时预算或地图缓存行为。 -- 不执行 Git 操作。 - -## 文件结构 - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` — 公开独立路径搜索耗时。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` — 在地图就绪后的路径产出阶段计时,并写入诊断对象。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` — 在状态图层和 Toast 显示两种耗时。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` — 说明两个耗时的计时边界。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` — 验证真实路径搜索耗时和地图阶段失败的零值。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` — 验证 UI/README 使用新字段。 - ---- - -### Task 1: 路径搜索耗时诊断契约 - -**Files:** -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:100-108,199-204` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs:10-60` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs:1-105,185-210` - -**Interfaces:** -- Consumes: `PlanningResult.Diagnostics.Elapsed`、`HybridAStarPlanner.Plan(PlanningRequest, PlanningOperationBudget)`。 -- Produces: `PlanningDiagnostics.PathSearchElapsed : TimeSpan`。 -- Contract: 成功结果满足 `TimeSpan.Zero <= PathSearchElapsed <= Elapsed`;地图阶段取消的结果为 `TimeSpan.Zero`。 - -- [ ] **Step 1: 在集成脚本写入失败断言** - -在空地图成功规划的现有诊断断言之后加入: - -```powershell -Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) ` - 'Planner diagnostics must retain a non-negative path-search elapsed time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) ` - 'Path-search elapsed time must not exceed total planning elapsed time.' -``` - -在 `$cancelledFacadeResult` 的现有断言之后加入: - -```powershell -Assert-Equal ([TimeSpan]::Zero) $cancelledFacadeResult.PlanningResult.Diagnostics.PathSearchElapsed ` - 'Map-stage cancellation must not report path-search time.' -``` - -- [ ] **Step 2: 运行集成脚本并确认红灯** - -Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1` - -Expected: FAIL,提示 `PathSearchElapsed` 不存在或无法通过新增的路径搜索耗时断言;此前的地图和门面检查仍先通过。 - -- [ ] **Step 3: 以兼容形式扩展诊断对象** - -在 `PlanningDiagnostics` 构造函数的最后一个参数之后增加: - -```csharp -TimeSpan pathSearchElapsed = default(TimeSpan) -``` - -并在构造函数中加入: - -```csharp -PathSearchElapsed = pathSearchElapsed; -``` - -在 `Elapsed` 属性之后加入: - -```csharp -/// -/// 地图就绪后搜索、回溯、装配和最终复核得到最终粗路径的耗时;不含建图。搜索开始前失败时为零。 -/// -public TimeSpan PathSearchElapsed { get; } -``` - -同时更新构造函数 XML 注释,明确 `elapsed` 是总耗时而 `pathSearchElapsed` 是不含建图的路径产出耗时。 - -- [ ] **Step 4: 在规划器的正确边界计时** - -在 `HybridAStarPlanner.cs` 顶部加入: - -```csharp -using System.Diagnostics; -``` - -在 `Plan(PlanningRequest request, PlanningOperationBudget budget)` 的 `try` 外部声明: - -```csharp -Stopwatch pathSearchStopwatch = null; -``` - -在第二次 `budget.GetStopReason()` 通过、且紧接 `_search.Search(request, budget)` 前写入: - -```csharp -pathSearchStopwatch = Stopwatch.StartNew(); -HybridAStarSearchResult searchResult = _search.Search(request, budget); -``` - -将 `Failure` 签名扩展为: - -```csharp -private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, - HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) -``` - -并在内部取得: - -```csharp -TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed; -return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, reason, - pathSearchElapsed)); -``` - -将 `_search.Search` 之后的每个失败返回和 `catch` 都传入 `pathSearchStopwatch`。成功结果调用改为: - -```csharp -return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters, - minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); -``` - -最后将 `CreateDiagnostics` 扩展为接收最后一个 `TimeSpan pathSearchElapsed` 参数,并将其作为 `PlanningDiagnostics` 的最后一个实参传入。 - -- [ ] **Step 5: 运行集成脚本并确认绿灯** - -Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore` - -Expected: exit code 0;只允许项目既有的过时 API 警告。 - -Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1` - -Expected: `Coarse path integration checks passed.`、`Coarse path facade checks passed.`、`Coarse path P1 scenario checks passed.`。 - -### Task 2: 图层、Toast 与 README 展示 - -**Files:** -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-70` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:494-513` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:285-337` - -**Interfaces:** -- Consumes: `result.PlanningResult.Diagnostics.Elapsed` 和 Task 1 提供的 `PathSearchElapsed`。 -- Produces: 状态图层与 Toast 中的“总耗时”和“路径搜索”文本,以及对应 README 说明。 -- Contract: 展示值均使用毫秒、`InvariantCulture` 和 `F0` 格式;不改变后台任务、取消或 Painter 图层名称。 - -- [ ] **Step 1: 为 UI 和 README 写入失败检查** - -在 UI 源码断言区域加入: - -```powershell -Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) ` - 'The status layer and Toast must both show path-search elapsed time.' -Assert-Match $source '路径搜索' 'The UI must label the independent path-search elapsed time.' -``` - -在 `$readmeStructure` 数组加入: - -```powershell -'PathSearchElapsed', -'路径搜索耗时', -``` - -- [ ] **Step 2: 运行 UI 脚本并确认红灯** - -Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1` - -Expected: FAIL,提示状态图层与 Toast 尚未显示 `PathSearchElapsed`,或 README 尚未说明该字段。 - -- [ ] **Step 3: 同时更新状态图层和 Toast** - -在 `DrawStatus` 中将单一耗时文本替换为: - -```csharp -Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" + result.PlanningResult.Status + ",总耗时:" + - result.PlanningResult.Diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - " ms,路径搜索:" + result.PlanningResult.Diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - " ms", x, y + 240f); -``` - -在 `BuildToastMessage` 中将返回字符串替换为: - -```csharp -return "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status + ",规划=" + - result.PlanningResult.Status + ",总耗时=" + - result.PlanningResult.Diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - "ms,路径搜索=" + result.PlanningResult.Diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - "ms。"; -``` - -- [ ] **Step 4: 补充 README 的耗时定义** - -在 “P1 手动测试与可视化” 的 “后台执行、停止与图层” 小节,在当前 `PlanningGridMap` 说明之后插入: - -```markdown -状态图层和 Toast 同时显示总耗时与 `PathSearchElapsed`(路径搜索耗时)。总耗时从 -`CoarsePathPlanningService.Plan` 入口开始,包含地图创建;路径搜索耗时从地图和起终点 -预检通过、即将进入 Hybrid A* 时开始,包含二维启发式、Hybrid A*、回溯、装配与最终复核, -不包含建图。搜索开始前即失败时该值为 0 ms。 -``` - -- [ ] **Step 5: 运行 UI 检查并确认绿灯** - -Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1` - -Expected: `Coarse path P1 UI source checks passed.`。 - -### Task 3: 最终回归与验收 - -**Files:** -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - -**Interfaces:** -- Consumes: 任务 1 和任务 2 的已完成代码与脚本。 -- Produces: 已验证的构建、集成回归和 UI/README 检查结果。 - -- [ ] **Step 1: 重新阅读计时边界** - -确认 `PathSearchElapsed` 只在 `_search.Search` 前启动;所有搜索后成功和失败路径均使用同一秒表;任何搜索前返回保持零值;总预算仍由 `PlanningOperationBudget` 控制。 - -- [ ] **Step 2: 运行最终构建** - -Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore` - -Expected: exit code 0。 - -- [ ] **Step 3: 运行最终自动化验证** - -Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1` - -Expected: `Coarse path P1 UI source checks passed.`。 - -Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1` - -Expected: `Coarse path integration checks passed.`、`Coarse path facade checks passed.`、`Coarse path P1 scenario checks passed.`。 - -- [ ] **Step 4: 进行手动界面验收** - -在可用 Clumsy 界面运行“粗路径规划-显式空图”或“粗路径规划-单矩形绕行”,确认状态图层和 Toast 都包含 `总耗时` 与 `路径搜索` 两个毫秒值,且点击停止仍能取消当前任务。 - diff --git a/docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md b/docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md deleted file mode 100644 index abe7ab7..0000000 --- a/docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md +++ /dev/null @@ -1,837 +0,0 @@ -# Coarse Path Manual Test Diagnostics Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 让 `[MovementTest(name = "粗路径规划")]` 支持每次输入正数秒总预算,并在失败时显示真实终止原因、搜索统计、总耗时和路径搜索耗时。 - -**Architecture:** `HybridAStarSearchResult` 在搜索边界保留原始终止原因,`HybridAStarPlanner` 负责把原因、资源限制和节点统计装配成公开诊断。`PlanningDiagnostics` 增加兼容的 `PathSearchElapsed`,MovementTest 只覆盖本次任务的超时配置并在图层和 Toast 中显示诊断;搜索、安全和路径发布规则保持不变。 - -**Tech Stack:** C# / .NET Standard 2.0、`System.Diagnostics.Stopwatch`、PowerShell 反射回归脚本、Clumsy `MovementTest`/Painter。 - -## Global Constraints - -- 普通规划失败继续通过 `PlanningResult` 返回,不把超时、无解、碰撞或资源上限改成异常。 -- `PlanningDiagnostics.Elapsed` 继续表示从 `CoarsePathPlanningService.Plan` 开始、包含建图的总耗时。 -- `PlanningDiagnostics.PathSearchElapsed` 从搜索前预检通过后开始,包含二维启发式、Hybrid A*、回溯、装配和最终复核,不包含建图。 -- 搜索前失败的 `PathSearchElapsed` 为 `TimeSpan.Zero`;搜索后的所有出口满足 `TimeSpan.Zero <= PathSearchElapsed <= Elapsed`。 -- 手动超时只接受 `TimeSpan` 可表示范围内的有限正数秒;`0` 不表示不限时。 -- 不修改碰撞步长、终点容差、Open List 排序、地图边界策略、倒车开关或成功路径发布条件。 -- 不实现 Reeds-Shepp/Dubins 精确连接、横移、蟹行、原地旋转、平滑、速度规划或控制。 -- 手动入口继续使用长 `0.80 m`、宽 `0.60 m`、安全余量 `0.05 m`、最小转弯半径 `1.20 m` 的固定演示车辆参数。 -- 工作区已有未提交内容;每次提交只能暂存任务中明确列出的文件,不得暂存其他路径。 - -## File Structure - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` — 搜索结果保留发生位置一致的原始终止原因。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` — 公开地图就绪后的独立路径搜索耗时。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` — 计时搜索/发布阶段并组合原因、资源上限和节点统计。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` — 读取单次超时并在图层/Toast 显示诊断。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` — 记录手动预算、演示车辆、两类耗时和第一版能力边界。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` — 验证搜索原始原因和内部异常不再静默。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` — 验证规划器诊断、路径搜索耗时和慢可行场景。 -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` — 验证手动超时、图层/Toast 和 README 文本。 - ---- - -### Task 1: Preserve Search Termination Reasons - -**Files:** -- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1:302-391` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs:15-318` - -**Interfaces:** -- Consumes: `HybridAStarSearch.Search(PlanningRequest, CancellationToken)` 和内部 `Search(PlanningRequest, PlanningOperationBudget)`。 -- Produces: `HybridAStarSearchResult.TerminationReason : string`,成功时为空,所有失败状态非空。 -- Produces: `CreateResult(..., int? successNodeIndex, string terminationReason = null)`,未显式提供原因时按状态生成稳定中文原因。 - -- [ ] **Step 1: Add failing search-result reason assertions** - -在搜索结果属性断言后加入: - -```powershell -Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) ` - 'Search result must expose its original termination reason.' -``` - -在取消、节点上限和超时状态断言后分别加入: - -```powershell -Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) ` - 'Cancelled search must retain a non-empty reason.' -Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) ` - 'Node-limited search must retain a non-empty reason.' -Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) ` - 'Timed-out search must retain a non-empty reason.' -Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) ` - 'Cancelled and node-limited searches must retain different reasons.' -Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) ` - 'Node-limited and timed-out searches must retain different reasons.' -``` - -在脚本末尾通过内部预算重载制造一个可重复的未预期错误,并断言异常类型没有被吞掉: - -```powershell -$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') | - Where-Object { - $_.Name -eq 'Search' -and - $_.GetParameters().Length -eq 2 -and - $_.GetParameters()[1].ParameterType -eq $operationBudgetType - } | - Select-Object -First 1 -Assert-True ($internalSearchMethod -ne $null) ` - 'Search must retain its internal shared-budget overload.' -$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null)) -Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() ` - 'A missing internal budget must be mapped to InternalError.' -Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) ` - 'Internal search errors must retain the exception type.' -``` - -- [ ] **Step 2: Run the search script and verify RED** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -``` - -Expected: build succeeds; the script fails first with `Search result must expose its original termination reason.` - -- [ ] **Step 3: Extend `HybridAStarSearchResult`** - -Add the final constructor parameter, assignment, and public property: - -```csharp -internal HybridAStarSearchResult( - PlanningStatus status, - IEnumerable nodes, - int expandedNodeCount, - int generatedNodeCount, - int reopenedNodeCount, - int staleOpenListEntryCount, - int peakOpenListCount, - int? successNodeIndex, - string terminationReason) -{ - Status = status; - Nodes = new ReadOnlyCollection( - new List(nodes ?? Array.Empty())); - ExpandedNodeCount = expandedNodeCount; - GeneratedNodeCount = generatedNodeCount; - ReopenedNodeCount = reopenedNodeCount; - StaleOpenListEntryCount = staleOpenListEntryCount; - PeakOpenListCount = peakOpenListCount; - SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null; - TerminationReason = status == PlanningStatus.Success - ? string.Empty - : terminationReason ?? string.Empty; -} - -/// 搜索边界记录的原始终止原因;成功时为空字符串,失败时非空。 -public string TerminationReason { get; } -``` - -- [ ] **Step 4: Centralize default reasons and retain exception details** - -Replace `CreateResult` with: - -```csharp -private static HybridAStarSearchResult CreateResult( - PlanningStatus status, - IEnumerable nodes, - int expandedNodeCount, - int generatedNodeCount, - int reopenedNodeCount, - int staleOpenListEntryCount, - int peakOpenListCount, - int? successNodeIndex, - string terminationReason = null) -{ - string reason = status == PlanningStatus.Success - ? string.Empty - : terminationReason ?? GetDefaultTerminationReason(status); - return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, - staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason); -} - -private static string GetDefaultTerminationReason(PlanningStatus status) -{ - switch (status) - { - case PlanningStatus.Cancelled: - return "Hybrid A* 搜索已取消。"; - case PlanningStatus.InvalidRequest: - return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。"; - case PlanningStatus.InvalidMap: - return "Hybrid A* 搜索地图结构无效。"; - case PlanningStatus.MapNotReady: - return "Hybrid A* 搜索地图尚未准备好。"; - case PlanningStatus.InvalidVehicleParameters: - return "Hybrid A* 搜索车辆参数无效。"; - case PlanningStatus.InvalidCurvatureConfiguration: - return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。"; - case PlanningStatus.StartOutsideMap: - return "Hybrid A* 搜索起始扩大车体不完全位于地图内。"; - case PlanningStatus.StartInCollision: - return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。"; - case PlanningStatus.GoalOutsideMap: - return "Hybrid A* 搜索目标扩大车体不完全位于地图内。"; - case PlanningStatus.GoalInCollision: - return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。"; - case PlanningStatus.SearchTimeout: - return "Hybrid A* 搜索使用的总规划预算已耗尽。"; - case PlanningStatus.SearchNodeLimitExceeded: - return "Hybrid A* 搜索达到扩展节点上限。"; - case PlanningStatus.NoFeasiblePath: - return "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"; - case PlanningStatus.BacktrackingFailed: - return "Hybrid A* 成功节点无法回溯为完整父链。"; - case PlanningStatus.FinalValidationFailed: - return "Hybrid A* 路径未通过最终复核。"; - case PlanningStatus.InternalError: - return "Hybrid A* 搜索发生未预期内部错误。"; - default: - return "Hybrid A* 搜索以未识别状态终止:" + status + "。"; - } -} -``` - -Change the two `NoFeasiblePath` exits so the caller can distinguish their location: - -```csharp -if (openList.Count == 0) - return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, - reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, - "二维启发式标记起点不可达目标,或起始方向无法进入 Open List。"); -``` - -```csharp -return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, - reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, - "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"); -``` - -Replace the catch block with: - -```csharp -catch (Exception exception) -{ - string reason = "Hybrid A* 搜索内部错误:" + exception.GetType().Name + - (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); - return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount, - reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, reason); -} -``` - -- [ ] **Step 5: Run the search script and verify GREEN** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -``` - -Expected: - -```text -Coarse path search primitive checks passed. -Coarse path Hybrid A star search checks passed. -``` - -- [ ] **Step 6: Commit the search reason contract** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs ClumsyPilot/tests/verify_coarse_path_search.ps1 -git commit -m "fix: retain coarse path search failure reasons" -``` - -### Task 2: Add Planner-Level Diagnostics and Path Search Timing - -**Files:** -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:75-135,189-210,357-360` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs:6-60` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs:1-226` - -**Interfaces:** -- Consumes: Task 1 `HybridAStarSearchResult.TerminationReason`. -- Produces: `PlanningDiagnostics.PathSearchElapsed : TimeSpan`. -- Produces: `BuildSearchFailureReason(HybridAStarSearchResult, HybridAStarConfiguration) : string`. -- Contract: planner success and search-stage failure satisfy `0 <= PathSearchElapsed <= Elapsed`; map/preflight failure remains zero. - -- [ ] **Step 1: Add failing planner-diagnostic assertions** - -After the existing successful planner diagnostic assertions add: - -```powershell -Assert-True ($result.Diagnostics.GetType().GetProperty('PathSearchElapsed') -ne $null) ` - 'Planner diagnostics must expose path-search elapsed time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) ` - 'Successful planning must retain non-negative path-search time.' -Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) ` - 'Path-search time must not exceed total elapsed time.' -``` - -After the invalid-map result assertions add: - -```powershell -Assert-Equal ([TimeSpan]::Zero) $mapFailureResult.PlanningResult.Diagnostics.PathSearchElapsed ` - 'Map failure must report zero path-search time.' -``` - -After creating a valid planner request, add a search-stage node-limit case: - -```powershell -$nodeLimitedRequest = [Activator]::CreateInstance($requestType) -$nodeLimitedRequest.Map = $request.Map -$nodeLimitedRequest.Start = $request.Start -$nodeLimitedRequest.Goal = $request.Goal -$nodeLimitedRequest.Vehicle = $request.Vehicle -$nodeLimitedRequest.Configuration = [Activator]::CreateInstance($configurationType) -$nodeLimitedRequest.Configuration.MaximumExpandedNodes = 0 -$nodeLimitedRequest.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2) -$nodeLimitedRequest.Configuration.GoalPositionToleranceMeters = 0.001 -$nodeLimitedRequest.Configuration.GoalHeadingToleranceRadians = 0.001 -$nodeLimitedRequest.Configuration.AllowReverse = $false -$nodeLimitedRequest.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') -$nodeLimitedResult = $plan.Invoke($planner, @($nodeLimitedRequest, [Threading.CancellationToken]::None)) -Assert-Equal 'SearchNodeLimitExceeded' $nodeLimitedResult.Status.ToString() ` - 'A zero node limit must fail after planner preflight.' -Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) ` - 'Search-stage node-limit failure must retain path-search time.' -Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -le $nodeLimitedResult.Diagnostics.Elapsed) ` - 'Failed path-search time must not exceed total elapsed time.' -Assert-True (-not [string]::IsNullOrWhiteSpace($nodeLimitedResult.Diagnostics.TerminationReason)) ` - 'Planner diagnostics must retain a node-limit reason.' -``` - -Extend the no-path scenario assertions: - -```powershell -Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('Open List')) ` - 'No-path diagnostics must retain the exact search exhaustion reason.' -Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('扩展=')) ` - 'No-path diagnostics must include search statistics.' -``` - -- [ ] **Step 2: Run integration checks and verify RED** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: build succeeds; the script fails first with `Planner diagnostics must expose path-search elapsed time.` - -- [ ] **Step 3: Extend `PlanningDiagnostics` compatibly** - -Append the optional constructor parameter: - -```csharp -TimeSpan pathSearchElapsed = default(TimeSpan) -``` - -Assign it after `Elapsed`: - -```csharp -Elapsed = elapsed; -PathSearchElapsed = pathSearchElapsed; -TerminationReason = terminationReason ?? string.Empty; -``` - -Add the property: - -```csharp -/// -/// 地图和起终点预检通过后,二维启发式、Hybrid A*、回溯、装配和最终复核的耗时; -/// 不含建图,搜索前失败时为零。 -/// -public TimeSpan PathSearchElapsed { get; } -``` - -Update the constructor XML comment so `elapsed` is described as total service elapsed time and `pathSearchElapsed` as the map-ready path-production elapsed time. - -- [ ] **Step 4: Start the path-search stopwatch at the exact boundary** - -Add: - -```csharp -using System.Diagnostics; -using System.Globalization; -``` - -Declare the stopwatch before the `try`: - -```csharp -Stopwatch pathSearchStopwatch = null; -``` - -Start it immediately before invoking the search: - -```csharp -pathSearchStopwatch = Stopwatch.StartNew(); -HybridAStarSearchResult searchResult = _search.Search(request, budget); -``` - -For `searchResult == null`, search failure, backtracking failure, assembly failure and final validation failure, pass `pathSearchStopwatch` into `Failure`. On success, pass its elapsed value: - -```csharp -return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, - pathLengthMeters, minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); -``` - -Replace the planner catch block with: - -```csharp -catch (Exception exception) -{ - string reason = "规划内部错误:" + exception.GetType().Name + - (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); - return Failure(PlanningStatus.InternalError, - budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None), - reason, null, pathSearchStopwatch); -} -``` - -- [ ] **Step 5: Compose actionable search diagnostics** - -For non-success search results, use: - -```csharp -if (searchResult.Status != PlanningStatus.Success) - return Failure(searchResult.Status, budget, - BuildSearchFailureReason(searchResult, request.Configuration), - searchResult, pathSearchStopwatch); -``` - -Add: - -```csharp -private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult, - HybridAStarConfiguration configuration) -{ - string reason = string.IsNullOrEmpty(searchResult.TerminationReason) - ? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。" - : searchResult.TerminationReason; - string limit = string.Empty; - if (searchResult.Status == PlanningStatus.SearchTimeout) - { - limit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString( - "F3", CultureInfo.InvariantCulture) + "s;"; - } - else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded) - { - limit = "节点上限=" + configuration.MaximumExpandedNodes.ToString( - CultureInfo.InvariantCulture) + ";"; - } - - return reason + limit + - "扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + - "生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + - "重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + - "陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "," + - "Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。"; -} -``` - -Replace the two helper signatures and bodies: - -```csharp -private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, - HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) -{ - TimeSpan pathSearchElapsed = pathSearchStopwatch == null - ? TimeSpan.Zero - : pathSearchStopwatch.Elapsed; - return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, - reason, pathSearchElapsed)); -} - -private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed, - double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed) -{ - return new PlanningDiagnostics( - searchResult == null ? 0 : searchResult.ExpandedNodeCount, - searchResult == null ? 0 : searchResult.GeneratedNodeCount, - searchResult == null ? 0 : searchResult.ReopenedNodeCount, - searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, - searchResult == null ? 0 : searchResult.PeakOpenListCount, - pathLengthMeters, - minimumClearanceMeters, - elapsed, - reason, - pathSearchElapsed); -} -``` - -All search-preflight `Failure(...)` calls continue omitting the optional stopwatch and therefore report zero. All calls after `_search.Search` pass the running stopwatch. - -- [ ] **Step 6: Run integration checks and verify GREEN** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: - -```text -Coarse path integration checks passed. -Coarse path facade checks passed. -Coarse path P1 scenario checks passed. -``` - -- [ ] **Step 7: Commit planner diagnostics** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1 -git commit -m "feat: add actionable coarse path diagnostics" -``` - -### Task 3: Add Manual Timeout Input, UI Diagnostics, and Documentation - -**Files:** -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:19-92` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:104-190,340-519` -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:114-116,285-359` - -**Interfaces:** -- Consumes: Task 2 `PlanningDiagnostics.PathSearchElapsed`. -- Produces: `ReadPositiveTimeoutInput(string) : TimeSpan`. -- Produces: `DrawStatus(string, CoarsePathPlanningJob, CoarsePathPlanningJobResult, PlanningGridMap)`. -- Contract: only the manual `[MovementTest(name = "粗路径规划")]` prompts for and overrides its request timeout; fixed scenarios keep their existing budgets. - -- [ ] **Step 1: Add failing UI and README source checks** - -After the current manual input assertions add: - -```powershell -Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' ` - 'The manual UI must read a finite positive timeout.' -Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' ` - 'The manual UI must apply the timeout to the current job.' -Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' ` - 'The manual timeout must convert seconds to TimeSpan.' -Assert-Match $source 'timeoutSeconds\s*<=\s*0' ` - 'The manual timeout must reject zero and negative values.' -Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) ` - 'The status layer and Toast must both show path-search elapsed time.' -Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' ` - 'The failure Toast must include the termination reason.' -Assert-Match $source 'ExpandedNodeCount' ` - 'The status layer must show expanded-node statistics.' -Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' ` - 'The status layer must show the effective turning radius.' -``` - -Append these entries to `$readmeStructure`: - -```powershell -'PathSearchElapsed', -'1.20 m', -'Reeds-Shepp', -``` - -- [ ] **Step 2: Run UI checks and verify RED** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: FAIL with `The manual UI must read a finite positive timeout.` - -- [ ] **Step 3: Read and apply one-shot manual timeout** - -Add: - -```csharp -using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -``` - -In `CoarsePathPlanningTest.Test()`, read the timeout after goal heading and apply it after creating the job: - -```csharp -double goalHeadingDeg = ReadFiniteInput("粗路径终点航向(世界 deg)"); -TimeSpan searchTimeout = ReadPositiveTimeoutInput("粗路径规划总超时(秒,必须大于 0)"); -IReadOnlyList obstacles = ReadManualObstacles(); -long snapshotVersion = obstacles.Count == 0 ? 0L : - Interlocked.Increment(ref _nextManualObstacleSnapshotVersion); -CoarsePathPlanningJob job = CoarsePathScenarioFactory.CreateManualObstacleDemo( - amrPose.x, amrPose.y, amrPose.th, goalXmm, goalYmm, goalHeadingDeg, - obstacles, snapshotVersion); -job.Configuration.SearchTimeout = searchTimeout; -CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job); -``` - -Add: - -```csharp -private static TimeSpan ReadPositiveTimeoutInput(string prompt) -{ - double timeoutSeconds = ReadFiniteInput(prompt); - if (timeoutSeconds <= 0d) - throw new ArgumentOutOfRangeException(nameof(prompt), "输入必须为正数:" + prompt); - try - { - return TimeSpan.FromSeconds(timeoutSeconds); - } - catch (OverflowException) - { - throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt); - } -} -``` - -- [ ] **Step 4: Show timings, search counts, vehicle configuration, and failure reason** - -Change the call site to: - -```csharp -DrawStatus(scenarioName, job, result, map); -``` - -Replace `DrawStatus` with: - -```csharp -private static void DrawStatus(string scenarioName, CoarsePathPlanningJob job, - CoarsePathPlanningJobResult result, PlanningGridMap map) -{ - float x = map == null ? 0f : map.Bounds.XMin + 150f; - float y = map == null ? -250f : map.Bounds.YMin + 150f; - string snapshot = map == null ? "无" : map.SnapshotId.ToString(CultureInfo.InvariantCulture); - string resolution = map == null ? "无" : - map.ResolutionMm.ToString("F0", CultureInfo.InvariantCulture) + " mm"; - PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics; - string reason = diagnostics.TerminationReason ?? string.Empty; - string turningRadius = "无"; - if (job != null && VehicleKinematics.TryGetMaximumCurvaturePerMeter( - job.Vehicle, out double maximumCurvaturePerMeter)) - { - turningRadius = (1d / maximumCurvaturePerMeter).ToString( - "F2", CultureInfo.InvariantCulture) + " m"; - } - - Painter.DrawText(Color.White, "场景:" + scenarioName, x, y); - Painter.DrawText(Color.White, "地图:" + result.MapResult.Status + ",缓存:" + - result.MapResult.CacheHit + ",快照:" + snapshot, x, y + 120f); - Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" + - result.PlanningResult.Status + ",总耗时:" + - diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - " ms,路径搜索:" + - diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - " ms", x, y + 240f); - Painter.DrawText(Color.White, "节点:扩展=" + - diagnostics.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + ",生成=" + - diagnostics.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + ",Open List峰值=" + - diagnostics.PeakOpenListCount.ToString(CultureInfo.InvariantCulture), x, y + 360f); - if (job != null && job.Vehicle != null) - { - Painter.DrawText(Color.White, "演示车辆:长=" + - job.Vehicle.LengthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,宽=" + - job.Vehicle.WidthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,余量=" + - job.Vehicle.SafetyMarginMeters.ToString("F2", CultureInfo.InvariantCulture) + - " m,最小转弯半径=" + turningRadius, x, y + 480f); - } - if (!string.IsNullOrEmpty(reason)) - Painter.DrawText(Color.LightYellow, "原因:" + reason, x, y + 600f); -} -``` - -Replace `BuildToastMessage` with: - -```csharp -private static string BuildToastMessage(string scenarioName, CoarsePathPlanningJobResult result) -{ - PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics; - string message = "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status + - ",规划=" + result.PlanningResult.Status + ",总耗时=" + - diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + - "ms,路径搜索=" + - diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + "ms"; - if (result.PlanningResult.Status != PlanningStatus.Success && - !string.IsNullOrEmpty(diagnostics.TerminationReason)) - { - message += ",原因=" + diagnostics.TerminationReason; - } - return message + "。"; -} -``` - -- [ ] **Step 5: Document the exact manual-test boundaries** - -After “总预算与取消” add: - -```markdown -### 总耗时与路径搜索耗时 - -`PlanningDiagnostics.Elapsed` 是从 `CoarsePathPlanningService.Plan` 开始的总耗时,包含 -地图来源、缓存、栅格化、距离场和路径规划。`PathSearchElapsed` 是地图和起终点预检 -通过后的路径搜索耗时,包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核; -搜索开始前失败时为零。 -``` - -In “AMR 位姿、手动终点与障碍物”, add: - -```markdown -手动入口还要求输入一次“粗路径规划总超时”,单位为秒,只接受 `TimeSpan` 可表示范围内 -的有限正数秒;`0`、负数、NaN、Infinity 或溢出值都会在启动规划前拒绝。该值只覆盖 -本次 `CoarsePathPlanningJob.Configuration.SearchTimeout`,不会改变固定场景或全局默认值。 - -此入口使用固定演示车辆:长 `0.80 m`、宽 `0.60 m`、四周安全余量 `0.05 m`、最小转弯 -半径 `1.20 m`。这些值不是从现场 AMR 配置读取的,判断现场可行性前必须确认车辆参数一致。 -``` - -In “后台执行、停止与图层”, add: - -```markdown -状态图层显示规划状态、总耗时、`PathSearchElapsed`(路径搜索耗时)、扩展/生成节点数、 -Open List 峰值、失败原因和固定演示车辆参数。Toast 同时显示两种耗时,并在失败时附加 -`TerminationReason`,因此超时、节点上限、无解、碰撞和内部错误不会只显示成泛化失败。 -``` - -Extend “第一版限制” with: - -```markdown -- Reeds-Shepp 或 Dubins 精确终点连接; -- 原地旋转。 - -当前只生成汽车式恒曲率前进/倒车原语,并允许在原语边界换向;未实现的 Reeds-Shepp、 -横移、蟹行和原地旋转是整个粗规划核心的第一版能力边界,不是 MovementTest 单独关闭。 -``` - -- [ ] **Step 6: Run UI checks and verify GREEN** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: - -```text -Coarse path P1 UI source checks passed. -``` - -- [ ] **Step 7: Commit the manual-test UX** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1 -git commit -m "feat: expose coarse path test diagnostics" -``` - -### Task 4: Regress the Slow Feasible Case and Run the Full Suite - -**Files:** -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:273-369` -- Verify: all files modified by Tasks 1-3 - -**Interfaces:** -- Consumes: Task 3 manual scenario factory and configurable `SearchTimeout`. -- Produces: regression proof that the formerly 5-second-limited feasible pose succeeds under a caller-selected longer budget. -- Contract: the regression uses a 30 秒 upper bound without asserting an exact wall-clock duration. - -- [ ] **Step 1: Add the slow feasible-case regression** - -After the manual empty-map factory assertions, add: - -```powershell -$slowFeasibleJob = $factoryManual.Invoke($null, @( - 1000.0, 2000.0, 0.0, 1500.0, 2500.0, 90.0)) -$slowFeasibleJob.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(30) -$slowFeasibleJob.Configuration.MaximumExpandedNodes = 1000000 -$slowFeasibleService = [Activator]::CreateInstance($serviceType) -$slowFeasibleResult = $servicePlan.Invoke( - $slowFeasibleService, @($slowFeasibleJob, [Threading.CancellationToken]::None)) -Assert-Equal 'Success' $slowFeasibleResult.PlanningResult.Status.ToString() ` - 'The previously five-second-limited feasible pose must succeed with a caller-selected longer budget.' -Assert-True ($slowFeasibleResult.PlanningResult.Diagnostics.PathSearchElapsed -le - $slowFeasibleResult.PlanningResult.Diagnostics.Elapsed) ` - 'Slow feasible path-search time must remain within total elapsed time.' -``` - -- [ ] **Step 2: Run the integration script and confirm the regression passes** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: - -```text -Coarse path integration checks passed. -Coarse path facade checks passed. -Coarse path P1 scenario checks passed. -``` - -The slow case is allowed up to 30 seconds and should normally complete near the observed 10-second baseline; do not assert an exact wall-clock value. - -- [ ] **Step 3: Run all coarse-path and map regressions** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: build exits 0 and every script prints its existing `passed` summary without an unhandled exception. - -- [ ] **Step 4: Inspect the final diff for scope and accidental edits** - -Run: - -```powershell -git diff -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_search.ps1 ClumsyPilot/tests/verify_coarse_path_integration.ps1 ClumsyPilot/tests/verify_coarse_path_ui.ps1 -``` - -Expected: only the approved reason propagation, timing, manual timeout, UI, documentation and tests are present; collision/search semantics and unrelated worktree files are unchanged. - -- [ ] **Step 5: Perform the Clumsy UI acceptance** - -Run `[MovementTest(name = "粗路径规划")]` with: - -```text -Start from current AMR pose corresponding to: 1000 mm, 2000 mm, 0 deg -Goal X: 1500 mm -Goal Y: 2500 mm -Goal heading: 90 deg -Timeout: 30 seconds -Manual obstacle count: 0 -``` - -Confirm: - -```text -Planning status: Success -Toast and status layer both show total elapsed and path-search elapsed -Status layer shows expanded/generated/Open List peak and the fixed demo vehicle parameters -``` - -Then run an intentionally impossible barrier or a deliberately short positive timeout and confirm the Toast includes a non-empty reason. - -- [ ] **Step 6: Commit the regression coverage** - -```powershell -git add -- ClumsyPilot/tests/verify_coarse_path_integration.ps1 -git commit -m "test: cover slow feasible coarse path planning" -``` diff --git a/docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md b/docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md deleted file mode 100644 index f45f0b4..0000000 --- a/docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md +++ /dev/null @@ -1,185 +0,0 @@ -# CoarsePath README 结构化重构 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 将 CoarsePath README 重构为与 Map README 相同的“结构—数据流—契约—最小示例—分步指南—常见错误”说明方式,同时保留准确的 P0/P1 边界。 - -**Architecture:** 保持所有生产代码不变。先为 README 的结构性事实增加稳定的 ASCII 文本断言,再将现有 README 的正确内容重组为面向调用者的模块说明,最后运行文档、构建与集成回归,证明这只是文档交付。 - -**Tech Stack:** Markdown、PowerShell、.NET `netstandard2.0` Debug 构建、现有 CoarsePath 验证脚本。 - -## Global Constraints - -- 只修改 `CoarsePath/README.md` 与其文档断言;不得改动 Map、CoarsePath、P1 UI 或测试场景的运行行为。 -- README 只陈述当前已实现并经自动化验证的 P0/P1 能力;实际 Clumsy 的人工视觉验收仍要明确为待执行。 -- 业务调用示例固定使用 `CoarsePathPlanningService.Plan(job, cancellationToken)`;不得鼓励 UI 或调用方直接拼接搜索组件。 -- Map 障碍物投影、栅格化和缓存细节只链接到 `../Map/README.md`,不复制为 CoarsePath 实现说明。 -- 坐标说明必须保持:Map 输入为 mm,核心位姿/路径为 m,核心航向为 rad;P1 UI 的 AMR 输入航向为 deg 并在边界转换。 -- 显式空图只能描述为 P1 单位/可视化演示,不能描述为真实作业地图。 -- 不恢复、清理或迁移 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。 - ---- - -## 文件结构 - -| 文件 | 修改职责 | -| --- | --- | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 重组现有 P0/P1 内容,加入实际目录树、规划数据流、分步指南与常见错误。 | -| `ClumsyPilot/tests/verify_coarse_path_ui.ps1` | 用 ASCII 关键字保护 README 的结构、核心边界和 P1 说明。 | - -### Task 1: 为 README 重构建立失败的结构断言 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` -- Verify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - -**Consumes:** 现有 `$readmePath`、`$readme`、`Assert-True` 及 P1 UI 源码检查。 - -**Produces:** 文档结构保护;README 缺少新的 Map 风格章节或 P1 边界时脚本失败。 - -- [ ] **Step 1: 在现有 README 断言后加入目标结构的失败检查** - -在当前 `$requiredText` 循环之后插入以下 PowerShell。所有匹配项保持 ASCII,避免 Windows PowerShell 无 BOM 脚本中的中文编码差异: - -```powershell -$readmeStructure = @( - 'File Structure', - 'Planning Data Flow', - 'Build Status and Stop', - 'Coordinates and Units', - 'Minimal Call Example', - 'Cache and SourceVersion', - 'Detailed Usage Guide', - 'P1 Manual Tests and Visualization', - 'Common Errors', - 'First-Version Limits', - 'CoarsePathPlanningService.Plan(job, cancellationToken)', - 'CoarsePathPlanningJob', - 'PlanningGridMap', - 'SourceVersion', - 'CoarsePathPlanningV1', - 'CancellationTokenSource', - 'NoFeasiblePath', - 'IsGearSwitchPoint', - '../Map/README.md' -) -foreach ($requiredText in $readmeStructure) { - Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText." -} -``` - -- [ ] **Step 2: 运行脚本确认 README 仍缺少新结构** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: `Restructured CoarsePath README must document File Structure.`;源码 UI 断言仍通过。 - -### Task 2: 重构 CoarsePath README 的模块说明与调用文档 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Test: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` - -**Consumes:** Map README 的组织方式;现有 CoarsePath README 的真实 P0/P1 契约;`CoarsePathPlanningService.Plan(job, cancellationToken)`。 - -**Produces:** 一份可从零开始阅读的 CoarsePath 模块说明,内容与当前实现一致。 - -- [ ] **Step 1: 用 Map 风格的顶层章节替换现有 README 的章节顺序** - -保留 README 标题 `# CoarsePath 粗路径规划(P0/P1)`,然后按以下顺序重新组织内容;将每个二级标题同时写为中文说明和括号中的 ASCII 稳定标识,例如 `## 文件结构(File Structure)`,使人类读者与 Task 1 断言都能使用: - -1. `## 模块说明(Module Overview)`:说明 Map 提供只读快照,CoarsePath 输出已复核的粗路径;唯一业务入口是 `CoarsePathPlanningService.Plan(job, cancellationToken)`;列出不负责的控制、速度、实时重规划等职责。 -2. `## 文件结构(File Structure)`:使用 `text` 目录树列出实际 `Contracts/`、`Vehicle/`、`Search/`、`Output/`、`Facade/` 与 `Test/` 文件,逐项写出与当前目录对应的职责。 -3. `## 规划数据流(Planning Data Flow)`:画出 `CoarsePathPlanningJob -> CoarsePathPlanningService -> PlanningMapFactory.Create -> PlanningGridMap -> HybridAStarPlanner -> PlanningResult -> CoarsePathPlanningJobResult`;在失败分支注明地图失败不启动搜索。 -4. `## 构建状态与停止(Build Status and Stop)`:说明 `MapResult` 和 `PlanningResult` 必须一起处理,解释 `Success`、`Cancelled`、`SearchTimeout`、`NoFeasiblePath` 与空路径规则。 -5. `## 坐标与单位(Coordinates and Units)`:用表格列出地图 mm、`Pose2D`/路径 m、核心航向 rad、P1 AMR 输入 deg;明确起点为车身几何中心和安全余量由 `VehicleParameters.SafetyMarginMeters` 表达。 -6. `## 最小调用示例(Minimal Call Example)`:保留并精简当前服务调用示例;包含 `PlanningMapRequest`、`Pose2D`、`VehicleParameters`、`HybridAStarConfiguration`、`MapResult` 与 `PlanningResult` 的失败处理。 -7. `## 缓存与 SourceVersion(Cache and SourceVersion)`:说明服务长期存活、`Input`/`Occupancy`/`None` 缓存层级,及来源内容变更必须递增 `SourceVersion`。 -8. `## 详细使用指南(Detailed Usage Guide)`:用六步小节解释长期服务、准备地图请求、填写起终点、填写车辆、调整搜索配置、调用及消费路径/方向段;链接 `../Map/README.md` 说明障碍物来源和栅格化。 -9. `## P1 手动测试与可视化(P1 Manual Tests and Visualization)`:包含七个 MovementTest 的场景表、`CoarsePathPlanningTest` 的 `getCartLocation`/手动目标转换、`CancellationTokenSource`/`Task.Run`/`TestStop` 停止语义、`CoarsePathPlanningV1` 图层及颜色图例。明确人工视觉验收尚待在实际 Clumsy 中执行。 -10. `## 常见错误(Common Errors)`:以“现象 / 原因 / 处理”表格写入:mm 当作 m、deg 当作 rad、`SourceVersion` 未递增、隐式空图、未处理非成功结果、把粗路径当作底盘可执行轨迹。 -11. `## 第一版限制(First-Version Limits)`:保留并归并路径平滑、速度/时间轨迹、底盘控制、实时重规划、真实作业地图、Release 基准等明确非目标。 - -- [ ] **Step 2: 对照实际目录和 P1 实现,校验每个文件树项与说明的真实性** - -确认目录树只引用下列已存在组件: - -```text -Contracts/: Pose2D, PlanningRequest, PlanningResult, PlanningStatus, - CoarsePathPoint, PathSegment, VehicleParameters, HybridAStarConfiguration -Vehicle/: VehicleKinematics, VehicleFootprint, FootprintCollisionChecker, - OrientedRectangleCellIntersection -Search/: BinaryMinHeap, GridDijkstraHeuristic, GoalToleranceChecker, - MotionPrimitive, MotionPrimitiveGenerator, SearchCostCalculator, - HybridAStarNode, HybridAStarNodeKey, HybridAStarSearch -Output/: PathBacktracker, CoarsePathAssembler, CoarsePathValidator -Facade/: CoarsePathPlanningJob, CoarsePathPlanningJobResult, - CoarsePathPlanningService, PlanningDebugOptions, IPlanningDebugSink -Test/: CoarsePathScenarioFactory, MovementTest.CoarsePathTest -``` - -不要在 README 中承诺不存在的平滑器、控制器、实时数据源或 Release 基准。 - -- [ ] **Step 3: 运行文档结构检查确认通过** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: `Coarse path P1 UI source checks passed.` - -### Task 3: 验证文档重构没有影响 P0/P1 行为 - -**Files:** - -- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Verify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` -- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Tasks 1–2 的 README 与断言。 - -**Produces:** 从最终工作区获得的文档、构建和集成验证证据。 - -- [ ] **Step 1: 构建项目** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -``` - -Expected: `0 个错误`;允许项目已有的两条过时 API 警告。 - -- [ ] **Step 2: 运行 P1 文档/UI 结构检查** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: `Coarse path P1 UI source checks passed.` - -- [ ] **Step 3: 运行粗路径集成回归** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 依次输出 `Coarse path integration checks passed.`、`Coarse path facade checks passed.` 与 `Coarse path P1 scenario checks passed.`。 - -## 自检 - -- **规格覆盖:** Task 2 覆盖设计中的十个 README 章节、P0/P1 已完成边界、Map 链接、单位、空图限制与人工验收状态;Task 1 保护可自动检查的结构事实;Task 3 给出最终证据。 -- **完整性检查:** 本计划不含未决实现、泛化错误处理或未命名的验证步骤;每项改动均有文件路径、具体内容与命令。 -- **一致性:** 所有调用名、状态名、场景工厂、P1 图层和坐标单位均与现有 CoarsePath 代码一致;计划不引入新 C# 接口或依赖。 diff --git a/docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md b/docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md deleted file mode 100644 index 52742ea..0000000 --- a/docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md +++ /dev/null @@ -1,365 +0,0 @@ -# P1 手动障碍物输入 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 让“粗路径规划”MovementTest 支持一次输入最多 20 个圆形或轴对齐矩形障碍物,并通过既有门面规划、快照绘制和取消流程验证结果。 - -**Architecture:** 纯几何输入和 Map 请求构造保留在 `CoarsePathScenarioFactory`,UI 只读取、验证和冻结操作者输入。每次含障碍物的手动运行由共享执行器颁发单调递增快照版本,保证 Map 缓存不会错误复用旧障碍物;Painter 继续只读取最终 `PlanningGridMap`。 - -**Tech Stack:** C# / `netstandard2.0`、现有 `ManualObstacleSource`、Clumsy `MovementTest`/`UI.GetInput`、PowerShell 反射与源码验证脚本。 - -## Global Constraints - -- 不改变 `CoarsePathPlanningService.Plan(job, token)` 作为唯一业务规划入口的边界;UI 不得直接创建地图工厂、搜索器、碰撞器或原语。 -- 手动输入的 X/Y、圆半径和矩形长宽全部使用世界 mm;AMR/目标航向输入使用 deg;核心 `Pose2D` 使用 m/rad。 -- 障碍物数量范围固定为 0–20;圆半径、矩形 X 长度和 Y 宽度必须是有限正数;矩形始终与世界坐标轴平行。 -- 有障碍物时使用必需的 `ManualObstacleSource("manual-user-input", version, true, ...)` 且关闭显式空图;零障碍物时才允许显式空图。 -- 手动地图边界必须覆盖起点、终点及每个障碍物完整外轮廓,再保留 2000 mm 留白并按 50 mm 向外取整。 -- 含障碍物手动提交必须使用单调递增快照版本;固定场景的缓存命中行为不得改变。 -- 保留后台 `Task.Run`、`CancellationTokenSource`、`TestStop`、结果快照绘制和无底盘命令边界。 -- 不支持旋转矩形、多边形、文件导入、拖拽编辑或运行中修改障碍物;不执行 Git 操作。 - ---- - -## 文件结构 - -| 文件 | 修改职责 | -| --- | --- | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新增手动障碍物纯数据类型、工厂方法、几何校验、动态边界和来源快照构造。 | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 为“粗路径规划”读取数量、类型、中心和尺寸,生成单调来源版本并提交工厂请求。 | -| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 通过程序集反射验证工厂、障碍来源、空图分支、几何边界和无效尺寸。 | -| `ClumsyPilot/tests/verify_coarse_path_ui.ps1` | 验证 UI 入口包含手动障碍物输入与工厂调用,同时保持无直接地图/搜索依赖。 | -| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 补充手动障碍物的输入顺序、单位、上限、矩形方向和空图限制。 | - -### Task 1: 手动障碍物工厂契约与行为验证 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` -- Modify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` - -**Consumes:** 现有 `$assembly`、`$testNamespace`、`$scenarioFactoryType`、`Find-Method`、`Assert-True`、`Assert-Equal`、`Assert-Near`。 - -**Produces:** `ManualCoarsePathObstacleKind`、`ManualCoarsePathObstacle` 和 `CreateManualObstacleDemo` 的反射/行为契约。 - -- [ ] **Step 1: 在 P1 工厂断言后加入失败的手动障碍物检查** - -在 `$manualJob` 的现有断言之后插入下面代码。它使用数组传入 `IReadOnlyList`,并检查请求尚未存在时的类型/方法失败。 - -```powershell -$manualObstacleKindType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacleKind', $false) -$manualObstacleType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacle', $false) -Assert-True ($manualObstacleKindType -ne $null) 'Manual obstacle kind enum must exist.' -Assert-True ($manualObstacleType -ne $null) 'Manual obstacle value type must exist.' - -$manualCircle = Find-Method $manualObstacleType 'Circle' @([double], [double], [double]) -$manualRectangle = Find-Method $manualObstacleType 'AxisAlignedRectangle' @([double], [double], [double], [double]) -$manualObstacleFactory = $scenarioFactoryType.GetMethods() | Where-Object { - $_.Name -eq 'CreateManualObstacleDemo' -and $_.GetParameters().Length -eq 8 -} | Select-Object -First 1 -Assert-True ($manualCircle -ne $null) 'Manual obstacle type must create circles from center and radius.' -Assert-True ($manualRectangle -ne $null) 'Manual obstacle type must create rectangles from center and X/Y dimensions.' -Assert-True ($manualObstacleFactory -ne $null) 'Scenario factory must expose CreateManualObstacleDemo with six poses, obstacles and version.' - -$manualObstacles = [Array]::CreateInstance($manualObstacleType, 2) -$manualObstacles.SetValue($manualCircle.Invoke($null, @([double]6500, [double]2000, [double]200)), 0) -$manualObstacles.SetValue($manualRectangle.Invoke($null, @([double]-2000, [double]500, [double]600, [double]400)), 1) -$manualObstacleJob = $manualObstacleFactory.Invoke($null, @( - 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $manualObstacles, [long]77)) -Assert-False $manualObstacleJob.MapRequest.AllowExplicitEmptyMap 'Manual obstacles must disable the explicit-empty-map mode.' -Assert-Equal 1 $manualObstacleJob.MapRequest.ObstacleSources.Count 'Manual obstacles must create one unified source.' -Assert-Equal 'manual-user-input' $manualObstacleJob.MapRequest.ObstacleSources[0].SourceId 'Manual source ID must be stable.' -Assert-Equal 77 $manualObstacleJob.MapRequest.ObstacleSources[0].SourceVersion 'Manual source version must be preserved.' -Assert-True ($manualObstacleJob.MapRequest.Bounds.XMin -le -4300.0) 'Manual map must include the rectangle outline and padding.' -Assert-True ($manualObstacleJob.MapRequest.Bounds.XMax -ge 8700.0) 'Manual map must include the circle outline and padding.' - -$emptyManualObstacles = [Array]::CreateInstance($manualObstacleType, 0) -$emptyManualJob = $manualObstacleFactory.Invoke($null, @( - 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $emptyManualObstacles, [long]0)) -Assert-True $emptyManualJob.MapRequest.AllowExplicitEmptyMap 'Zero manual obstacles must retain explicit empty-map mode.' -Assert-Equal 0 $emptyManualJob.MapRequest.ObstacleSources.Count 'Zero manual obstacles must not create a fake source.' - -try { - $null = $manualCircle.Invoke($null, @([double]1000, [double]1000, [double]0)) - throw 'Zero-radius manual circle must be rejected.' -} -catch [Reflection.TargetInvocationException] { - Assert-True ($_.Exception.InnerException -is [ArgumentOutOfRangeException]) 'Invalid manual geometry must report argument range.' -} -``` - -- [ ] **Step 2: 构建并运行脚本确认新契约失败** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建成功;脚本报出 `Manual obstacle kind enum must exist.`。 - -- [ ] **Step 3: 在场景工厂实现不可变手动障碍物类型** - -在 `CoarsePathScenarioFactory.cs` 的固定场景枚举之后加入如下公共类型。构造函数保持私有,强制圆形与矩形分别通过语义明确的静态工厂创建;所有几何输入均为 mm。 - -```csharp -/// 手动障碍物的支持几何类型。 -public enum ManualCoarsePathObstacleKind -{ - /// 由圆心和半径定义的圆形障碍物。 - Circle, - /// 由几何中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。 - AxisAlignedRectangle, -} - -/// 手动粗路径测试的不可变障碍物输入;全部几何数据使用世界 mm。 -public sealed class ManualCoarsePathObstacle -{ - private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters, - double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters) - { - Kind = kind; CenterXMillimeters = centerXMillimeters; CenterYMillimeters = centerYMillimeters; - SizeXMillimeters = sizeXMillimeters; SizeYMillimeters = sizeYMillimeters; - } - - public ManualCoarsePathObstacleKind Kind { get; } - public double CenterXMillimeters { get; } - public double CenterYMillimeters { get; } - public double SizeXMillimeters { get; } - public double SizeYMillimeters { get; } - - public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters, - double radiusMillimeters) - { - EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); - EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); - EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters)); - return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters, - centerYMillimeters, radiusMillimeters, radiusMillimeters); - } - - public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters, - double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters) - { - EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); - EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); - EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters)); - EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters)); - return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle, - centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters); - } -} -``` - -将 `EnsureFinite` 与新增 `EnsurePositiveFinite` 定义为可被同一命名空间类型调用的内部静态校验辅助方法,或在 `ManualCoarsePathObstacle` 中实现等价私有辅助方法;无效值必须抛出 `ArgumentOutOfRangeException`。 - -- [ ] **Step 4: 实现手动障碍物请求和动态边界** - -在 `CoarsePathScenarioFactory` 加入下面公共方法,并让现有 `CreateManualGoalDemo` 调用它的零障碍物分支,以保留当前空图契约: - -```csharp -public static CoarsePathPlanningJob CreateManualObstacleDemo( - double startXMillimeters, double startYMillimeters, double startHeadingDegrees, - double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees, - IReadOnlyList obstacles, long obstacleSnapshotVersion) -{ - ValidateManualPoseInputs(startXMillimeters, startYMillimeters, startHeadingDegrees, - goalXMillimeters, goalYMillimeters, goalHeadingDegrees); - IReadOnlyList items = obstacles ?? - throw new ArgumentNullException(nameof(obstacles)); - if (items.Count > MaximumManualObstacleCount) - throw new ArgumentOutOfRangeException(nameof(obstacles)); - if (items.Count == 0) - return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, - goalXMillimeters, goalYMillimeters, Array.Empty()), - ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), - ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); - if (obstacleSnapshotVersion <= 0) - throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion)); - - IMapObstacle[] mapObstacles = ConvertManualObstacles(items); - IMapObstacleSource[] sources = - { - new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, mapObstacles), - }; - return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, - goalXMillimeters, goalYMillimeters, items), - ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), - ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); -} -``` - -Use `CreateManualMapRequest(items, sources)` rather than leaving the above source array unused: it must create a `PlanningMapRequest` with the dynamic bounds, `ResolutionMm = 50f`, those sources and `AllowExplicitEmptyMap = false`. `ConvertManualObstacles` must map a circle to `new CircleObstacle(centerX, centerY, radius)` and a rectangle to `new AxisAlignedRectangleObstacle(centerX - lengthX / 2, centerX + lengthX / 2, centerY - widthY / 2, centerY + widthY / 2)` after range-safe float conversion. - -Refactor `CreateManualDemoMap` to accept an obstacle collection and include its circle/rectangle extents before adding 2000 mm padding and applying `ToGridLowerBound`/`ToGridUpperBound`. Zero obstacles must retain `Array.Empty()` and `AllowExplicitEmptyMap = true`. - -- [ ] **Step 5: 运行工厂行为检查确认通过** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建成功;输出既有三行集成通过信息,且手动圆/矩形、空障碍物和无效半径断言均通过。 - -### Task 2: MovementTest 逐项输入与来源版本 - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` -- Modify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` - -**Consumes:** Task 1 的 `ManualCoarsePathObstacle.Circle`、`ManualCoarsePathObstacle.AxisAlignedRectangle` 与 `CoarsePathScenarioFactory.CreateManualObstacleDemo`。 - -**Produces:** `CoarsePathPlanningTest` 在启动规划前读取并冻结最多 20 个手动障碍物,随后使用递增版本提交给工厂。 - -- [ ] **Step 1: 加入失败的 UI 源码边界断言** - -在现有手动工厂断言后加入: - -```powershell -Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.' -Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.' -Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.' -Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.' -Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.' -Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.' -Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.' -``` - -- [ ] **Step 2: 运行 UI 脚本确认新断言失败** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: `The manual UI must construct typed manual obstacles.`。 - -- [ ] **Step 3: 实现输入辅助方法与提交逻辑** - -在 `CoarsePathPlanningTest` 中新增: - -```csharp -private const int MaximumManualObstacleCount = 20; -private static long _nextManualObstacleSnapshotVersion; - -private static IReadOnlyList ReadManualObstacles() -{ - int count = ReadBoundedIntegerInput("手动障碍物数量(0-20)", 0, MaximumManualObstacleCount); - var obstacles = new List(count); - for (int index = 0; index < count; index++) - { - int kind = ReadBoundedIntegerInput("障碍物 " + (index + 1) + " 类型(1圆形,2矩形)", 1, 2); - double centerX = ReadFiniteInput("障碍物 " + (index + 1) + " 中心 X(世界 mm)"); - double centerY = ReadFiniteInput("障碍物 " + (index + 1) + " 中心 Y(世界 mm)"); - if (kind == 1) - { - double radius = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " 半径 r(mm)"); - obstacles.Add(ManualCoarsePathObstacle.Circle(centerX, centerY, radius)); - } - else - { - double lengthX = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " X方向长度(mm)"); - double widthY = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " Y方向宽度(mm)"); - obstacles.Add(ManualCoarsePathObstacle.AxisAlignedRectangle(centerX, centerY, lengthX, widthY)); - } - } - return obstacles; -} -``` - -`ReadBoundedIntegerInput` 复用 `UI.GetInput` 和当前文化/InvariantCulture 解析,拒绝非整数或超出 `[minimum, maximum]` 的输入;`ReadPositiveFiniteInput` 在 `ReadFiniteInput` 返回后拒绝 `<= 0d`。所有失败继续由现有 `ShowInputFailure` 显示。 - -在 `Test()` 中的终点读取后调用 `ReadManualObstacles()`。当集合非空时,用 `Interlocked.Increment(ref _nextManualObstacleSnapshotVersion)` 取得版本;集合为空时使用 `0L`。随后替换现有工厂调用: - -```csharp -IReadOnlyList obstacles = ReadManualObstacles(); -long snapshotVersion = obstacles.Count == 0 ? 0L : - Interlocked.Increment(ref _nextManualObstacleSnapshotVersion); -CoarsePathPlanningJob job = CoarsePathScenarioFactory.CreateManualObstacleDemo( - amrPose.x, amrPose.y, amrPose.th, goalXmm, goalYmm, goalHeadingDeg, - obstacles, snapshotVersion); -CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job); -``` - -保留 `TestStop`、`Run`、Painter 和底盘禁止边界,不在 UI 内构造 `ManualObstacleSource`、`PlanningMapRequest` 或搜索对象。 - -- [ ] **Step 4: 运行 UI 结构检查确认通过** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: `Coarse path P1 UI source checks passed.` - -### Task 3: README 输入说明与最终回归 - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` -- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` -- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Consumes:** Tasks 1–2 的工厂与 UI 输入契约。 - -**Produces:** README 中与实际输入顺序一致的手动障碍物说明,以及最终的构建、UI 和集成证据。 - -- [ ] **Step 1: 为 README 增加失败的 ASCII 文档断言** - -在 `$readmeStructure` 的数组中加入: - -```powershell -'CreateManualObstacleDemo', -'ManualCoarsePathObstacle', -'manual-user-input', -'0-20', -'AxisAlignedRectangle', -``` - -- [ ] **Step 2: 运行 UI 脚本确认 README 断言失败** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -``` - -Expected: `Restructured CoarsePath README must document CreateManualObstacleDemo.` - -- [ ] **Step 3: 更新 README 的 P1 手动测试段落** - -在 `## P1 手动测试与可视化(P1 Manual Tests and Visualization)` 的“AMR 位姿与手动终点”小节中,替换“空图入口”的单一说明,加入以下事实: - -1. 目标输入之后先输入 `0-20` 的障碍物数量; -2. 每项输入 `1` 圆形或 `2` 矩形、中心 X/Y(mm),圆形半径或矩形 X 长度/Y 宽度(mm); -3. 矩形是 `AxisAlignedRectangle`,不支持旋转;尺寸必须为正; -4. `CreateManualObstacleDemo` 将它们包装为 `manual-user-input` 快照,有障碍物时关闭显式空图; -5. 零障碍物才是坐标/取消演示的显式空图;真实作业仍必须提供真实障碍物来源; -6. 地图边界自动覆盖起终点和障碍物完整外轮廓,保留 2000 mm 留白并按 50 mm 对齐; -7. 每次含障碍物提交使用新版本,Painter 仍显示最终 `PlanningGridMap` 占据格而不是原始几何。 - -- [ ] **Step 4: 运行完整验证** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: 构建为 `0 个错误`;UI 脚本输出 `Coarse path P1 UI source checks passed.`;集成脚本依次输出既有三行 `passed` 消息。 - -## 自检 - -- **规格覆盖:** Task 1 覆盖几何类型、非空/空地图、动态边界、版本和无效尺寸;Task 2 覆盖 0–20 输入、形状输入、版本和后台门面边界;Task 3 覆盖 README 与回归。 -- **完整性检查:** 每个实现步骤指定了文件、调用签名、验证规则和命令;不引入未命名接口或外部依赖。 -- **一致性:** `ManualCoarsePathObstacle`、`CreateManualObstacleDemo`、`manual-user-input` 与 `obstacleSnapshotVersion` 在所有任务中使用相同名称和单位定义。 diff --git a/docs/superpowers/plans/2026-07-28-path-smoothing-comparison.md b/docs/superpowers/plans/2026-07-28-path-smoothing-comparison.md deleted file mode 100644 index 408d78a..0000000 --- a/docs/superpowers/plans/2026-07-28-path-smoothing-comparison.md +++ /dev/null @@ -1,1177 +0,0 @@ -# Path Smoothing Comparison Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a validated coarse-path smoothing module that compares cubic B-spline, local cubic Bézier, and piecewise quintic paths, then exports reproducible IEEE-style Chinese reports for existing and fast fixture scenarios. - -**Architecture:** Add `PathSmoothing` beside `CoarsePath`; keep contracts, shared geometry processing, algorithms, validation, comparison, and rendering isolated. Every algorithm consumes the same preprocessed direction segments, every candidate passes the same analyzer and full-footprint validator, and only accepted results reach the formal facade. Reporting consumes immutable comparison data and cannot affect planning status. - -**Tech Stack:** C# 10, .NET Standard 2.0, existing `PlanningGridMap`/`FootprintCollisionChecker`, PowerShell reflection tests, Newtonsoft.Json 13.0.4, StbImageWriteSharp 1.16.7, System.Drawing.Common 10.0.10 on Windows. - -## Global Constraints - -- Core units are m, rad, and 1/m; no mm or degree conversion inside smoothing algorithms. -- Preserve start, goal, direction-segment order, gear-switch count, and gear-switch poses exactly. -- Never differentiate, resample, or fit across a gear-switch duplicate pair. -- Default output spacing is `0.05 m`; default swept-collision step is `0.025 m`. -- Default clearance reserve is `0.02 m`; default smoothing strength is `1.00`. -- A formal result publishes a path only for `Success` or explicitly verified `FallbackToCoarsePath`. -- Movement-bound rejection is retryable; invalid input, singular coefficients, and non-finite geometry are terminal failures. -- Every algorithm uses a read-only snapshot of its strong-typed options and maps evaluated points to the original direction segment by local arc length, never by raw point index. -- Rejected candidates may appear only in comparison diagnostics, never in `PathSmoothingResult.Path`. -- No speed, acceleration, time, SQP, Frenet, chassis, sensor, localization, or UI dependencies. -- Figure size is `7.16 × 5.2 in`; PNG size is `4296 × 3120 px` with 600 dpi metadata. -- Chinese text uses `SimSun`; English, numbers, Greek, and mathematics use `Times New Roman`. -- Fixed method colors are raw `#4D4D4D`, B-spline `#0072B2`, Bézier `#D55E00`, quintic `#009E73`, and curvature limits `#CC79A7`. -- `System.Drawing.Common` is a Windows-only report-rendering dependency; smoothing, validation, comparison, SVG, and CSV remain independent of its runtime availability. -- Text SVG is the editable master, not a directly submittable IEEE artifact; submission conversion to font-embedded or outlined PDF/EPS is an explicit external publishing step. -- Offline timing uses one warm-up and five measured deterministic runs per scenario and method; ranking uses the measured median only. -- Follow TDD for every task: failing verification first, minimal implementation second, full relevant verification before commit. - ---- - -### Task 1: Immutable smoothing contracts - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_contracts.ps1` - -**Interfaces:** -- Consumes: `CoarsePathPoint`, `PathSegment`, `PlanningGridMap`, `VehicleParameters`. -- Produces: `PathSmoothingRequest`; `PathSmoothingResult.Success(...)`; `PathSmoothingResult.Fallback(...)`; `PathSmoothingResult.Failure(...)`; all public enums and value objects used below. - -- [ ] **Step 1: Write the failing contract verification** - -Create a reflection script that loads `ClumsyPilot.dll`, resolves every type listed above, and verifies these exact defaults and invariants: - -```powershell -$configuration = [Activator]::CreateInstance($configurationType) -Assert-Near 0.05 $configuration.OutputSpacingMeters 'Default output spacing must be 0.05 m.' -Assert-Near 0.025 $configuration.MaximumCollisionCheckStepMeters 'Default collision step must be 0.025 m.' -Assert-Equal 4 $configuration.RetryStrengthScales.Count 'Retry schedule must contain four entries.' -Assert-Near 1.0 $configuration.RetryStrengthScales[0] 'First retry scale must be 1.0.' -Assert-Near 0.25 $configuration.RetryStrengthScales[3] 'Last retry scale must be 0.25.' - -$failed = $resultType.GetMethod('Failure').Invoke( - $null, - @([Enum]::Parse($statusType, 'InvalidInput'), - [Activator]::CreateInstance($diagnosticsType))) -Assert-Equal 0 $failed.Path.Count 'Failure must publish no path.' -Assert-Equal 0 $failed.Segments.Count 'Failure must publish no segments.' -``` - -Also construct a `SmoothedPathPoint` and assert all units/properties, construct two `SmoothedPathSegment` instances, and prove returned collections cannot be mutated. - -- [ ] **Step 2: Run the verification and confirm RED** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_contracts.ps1 -``` - -Expected: build succeeds on existing code; script fails because `MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingConfiguration` is missing. - -- [ ] **Step 3: Implement enums, immutable values, and result factories** - -Use namespace `MultiWheelC.TrajectoryPlanning.PathSmoothing`. Implement these exact public shapes: - -```csharp -public enum SmoothingMethod { CubicBSpline, LocalCubicBezier, PiecewiseQuintic } -public enum PathSmoothingStatus { Success, FallbackToCoarsePath, InvalidInput, Infeasible, Failed, Cancelled } -public enum SmoothedPathPointSource { Anchor, Interpolated, GearSwitch, CoarsePathFallback } - -public sealed class PathSmoothingConfiguration -{ - public PathSmoothingConfiguration() - { - OutputSpacingMeters = 0.05d; - MaximumCollisionCheckStepMeters = 0.025d; - MinimumClearanceReserveMeters = 0.02d; - SmoothingStrength = 1d; - AllowFallbackToCoarsePath = true; - RetryStrengthScales = new ReadOnlyCollection( - new List { 1d, 0.75d, 0.50d, 0.25d }); - } - public SmoothingMethod Method { get; set; } - public double OutputSpacingMeters { get; set; } - public double MaximumCollisionCheckStepMeters { get; set; } - public double MinimumClearanceReserveMeters { get; set; } - public double SmoothingStrength { get; set; } - public bool AllowFallbackToCoarsePath { get; set; } - public IReadOnlyList RetryStrengthScales { get; } - public CubicBSplineOptions CubicBSpline { get; } = new CubicBSplineOptions(); - public LocalCubicBezierOptions LocalCubicBezier { get; } = new LocalCubicBezierOptions(); - public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions(); -} -``` - -Use these exact option defaults: - -```csharp -public sealed class CubicBSplineOptions -{ - public double EndpointTangentScale { get; set; } = 1d / 3d; -} -public sealed class LocalCubicBezierOptions -{ - public double CornerHeadingThresholdRadians { get; set; } = Math.PI / 18d; - public double MaximumWindowLengthMeters { get; set; } = 0.60d; - public double HandleLengthRatio { get; set; } = 1d / 3d; -} -public sealed class PiecewiseQuinticOptions -{ - public double KnotSpacingMeters { get; set; } = 0.50d; - public double MinimumKnotSpacingMeters { get; set; } = 0.10d; -} -``` - -Define the request constructor exactly as: - -```csharp -public PathSmoothingRequest( - IReadOnlyList coarsePath, - IReadOnlyList segments, - PlanningGridMap map, - VehicleParameters vehicle, - PathSmoothingConfiguration configuration) -``` - -Define parameterless immutable-empty defaults for `PathQualityMetrics` and `PathSmoothingDiagnostics`, plus overloads that accept all measured values. The three result factories are: - -```csharp -public static PathSmoothingResult Success( - SmoothingMethod method, IReadOnlyList path, - IReadOnlyList segments, PathSmoothingDiagnostics diagnostics); -public static PathSmoothingResult Fallback( - SmoothingMethod attemptedMethod, IReadOnlyList path, - IReadOnlyList segments, PathSmoothingDiagnostics diagnostics); -public static PathSmoothingResult Failure( - PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics); -``` - -`PathSmoothingResult` must copy all input lists into `ReadOnlyCollection`. Reject `Success`/`Fallback` factories with empty paths or segments; reject `Failure` with a success-like status. - -- [ ] **Step 4: Run contract verification and existing integration verification** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_contracts.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: both scripts end with their passed messages. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 -git commit -m "feat: define path smoothing contracts" -``` - -### Task 2: Input preparation, direction splitting, resampling, and geometry analysis - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_geometry.ps1` - -**Interfaces:** -- Consumes: `PathSmoothingRequest`. -- Produces: `PathSmoothingPreprocessor.TryPrepare(request, out PreparedPath, out string)` and `PathGeometryAnalyzer.TryAnalyze(candidateSegments, spacing, out PathGeometryAnalysis analysis, out string reason)`. - -- [ ] **Step 1: Write analytic failing tests** - -Build reflection helpers that create: - -```text -forward straight: (0,0,0) → (1,0,0), expected κ = 0 -forward quarter circle: R=2, expected vehicle κ = +0.5 -reverse quarter circle: R=2, expected vehicle κ = -0.5 -gear switch: duplicated pose and arc length with opposite directions -``` - -Assert `0.05 m` spacing, exact final point, continuous `UnwrappedHeading`, correct curvature signs, and no derivative across the gear-switch pair. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -``` - -Expected: fails because `PathGeometryAnalyzer` is missing. - -- [ ] **Step 3: Implement deterministic preparation and resampling** - -Implement segment-local linear arc interpolation: - -```csharp -double ratio = (targetArc - left.ArcLength) / (right.ArcLength - left.ArcLength); -double x = left.X + ratio * (right.X - left.X); -double y = left.Y + ratio * (right.Y - left.Y); -double heading = left.UnwrappedHeading + - ratio * (right.UnwrappedHeading - left.UnwrappedHeading); -``` - -Always append the exact segment end. Preserve separate gear-switch endpoints. Reject non-finite values, non-monotonic arc length, illegal duplicates, and segment coverage gaps. - -- [ ] **Step 4: Implement shared geometry formulas** - -For each direction segment, recompute arc length from Euclidean position increments, unwrap heading from travel tangents, and use stable one-sided/central differences: - -```csharp -double geometricCurvature = deltaHeading / deltaArc; -double directionSign = direction == TravelDirection.Forward ? 1d : -1d; -double vehicleCurvature = directionSign * geometricCurvature; -double totalVariationIncrement = Math.Abs(currentCurvature - previousCurvature); -double variationEnergyIncrement = - Math.Pow((currentCurvature - previousCurvature) / deltaArc, 2d) * deltaArc; -``` - -At segment boundaries use only samples from that segment. Never include the gear-switch duplicate distance in a denominator. - -- [ ] **Step 5: Run geometry, contract, and utility checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_contracts.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -``` - -Expected: all pass; circle curvature error stays within the tolerance encoded in the test. - -- [ ] **Step 6: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -git commit -m "feat: add smoothing path geometry processing" -``` - -### Task 3: Full-footprint smoothing validator - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_validation.ps1` - -**Interfaces:** -- Consumes: analyzed `SmoothedPathPoint`/`SmoothedPathSegment`, original prepared endpoints, `PlanningGridMap`, `VehicleParameters`, collision step. -- Produces: `TryValidate(..., out IReadOnlyList pathWithClearance, out double minimumClearance, out string reason)`. - -- [ ] **Step 1: Write failing safety cases** - -Create an empty map, an occupied rectangle map, and paths that are: valid; point-colliding; swept-motion-colliding; outside bounds; over maximum curvature; changed at a gear switch; and overclaiming clearance. - -```powershell -Assert-False $collisionAccepted 'A smoothing candidate that cuts through an obstacle must be rejected.' -Assert-False $curvatureAccepted 'A smoothing candidate above vehicle maximum curvature must be rejected.' -Assert-False $switchAccepted 'A moved gear-switch pose must be rejected.' -Assert-True $validAccepted 'A valid straight candidate must pass.' -``` - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -``` - -Expected: missing `SmoothedPathValidator`. - -- [ ] **Step 3: Implement validator using existing collision semantics** - -For every point call `FootprintCollisionChecker.IsPoseCollisionFree`; for every non-duplicate adjacent pair call `IsSweptMotionCollisionFree`. Enforce: - -```csharp -if (Math.Abs(point.VehicleCurvature) > maximumCurvature + 1e-6d) - return Fail("平滑路径车辆曲率超过车辆上限。"); -if (!SamePose(candidateStart, originalStart) || !SamePose(candidateEnd, originalEnd)) - return Fail("平滑路径改变了方向段端点。"); -``` - -Replace candidate clearance with the conservative checked value; never retain a larger claimed value. - -- [ ] **Step 4: Run validator and existing collision checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -``` - -Expected: both pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation ClumsyPilot/tests/verify_path_smoothing_validation.ps1 -git commit -m "feat: validate smoothed vehicle paths" -``` - -### Task 4: Algorithm contract and finite retry runner - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_runner.ps1` - -**Interfaces:** -- Consumes: `PreparedPath`, method-specific `IPathSmoother`, analyzer, validator, retry scales. -- Produces: one accepted candidate or stable `Infeasible`/`Failed` diagnostics with attempted strengths and optional rejected comparison geometry. - -Use this exact internal algorithm interface: - -```csharp -internal interface IPathSmoother -{ - SmoothingMethod Method { get; } - SmoothingCandidate Smooth( - SmoothingAlgorithmInput input, - double effectiveStrength, - CancellationToken cancellationToken); -} -``` - -- [ ] **Step 1: Write failing fake-smoother tests** - -Use reflection plus internal test doubles exposed from a nested test helper to prove: - -```text -strengths attempted exactly: 1.00, 0.75, 0.50, 0.25 -stop immediately after first accepted candidate -do not retry numerical invalid input -cancel before the next attempt -retain rejected candidate only in comparison diagnostics -``` - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -``` - -Expected: missing `SmoothingAlgorithmRunner`. - -- [ ] **Step 3: Implement the runner** - -Use this control flow: - -```csharp -foreach (double scale in configuration.RetryStrengthScales) -{ - cancellationToken.ThrowIfCancellationRequested(); - SmoothingCandidate candidate = smoother.Smooth(input, configuration.SmoothingStrength * scale); - if (!candidate.Succeeded) return AlgorithmRunResult.Failed(candidate.Reason); - if (!_analyzer.TryAnalyze(candidate.Segments, configuration.OutputSpacingMeters, out PathGeometryAnalysis analyzed, out var reason)) - return AlgorithmRunResult.Failed(reason); - if (_validator.TryValidate(analyzed.Path, analyzed.Segments, input, out var safePath, out var clearance, out reason)) - return AlgorithmRunResult.Success(safePath, analyzed.Segments, metrics, scale); - rejected = candidate; - failures.Add(reason); -} -return AlgorithmRunResult.Infeasible(rejected, failures); -``` - -Convert `OperationCanceledException` to `Cancelled` at the facade boundary, not inside numerical algorithms. - -- [ ] **Step 4: Run runner, geometry, and validator tests** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -``` - -Expected: all pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms ClumsyPilot/tests/verify_path_smoothing_runner.ps1 -git commit -m "feat: add smoothing algorithm retry runner" -``` - -### Task 5: Clamped cubic B-spline smoother - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_bspline.ps1` - -**Interfaces:** -- Implements: `IPathSmoother.Method == SmoothingMethod.CubicBSpline`. -- Produces: one geometry-only candidate per input direction segment. - -- [ ] **Step 1: Write failing deterministic shape tests** - -Verify a straight segment remains collinear, a five-point corner becomes tangent-continuous, endpoints and travel tangents are exact, and every displacement stays within its supplied movement radius. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bspline.ps1 -``` - -Expected: missing `CubicBSplineSmoother`. - -- [ ] **Step 3: Implement clamped basis evaluation and constrained control points** - -Use degree 3, endpoint knot multiplicity 4, and Cox–de Boor basis evaluation. Fix the first/last control points to endpoints; place adjacent control points on endpoint travel tangents. Blend remaining control points toward local three-point averages: - -```csharp -SmoothingPoint2D target = (previous + current + next) / 3d; -SmoothingPoint2D proposed = current + strength * (target - current); -SmoothingPoint2D bounded = ClampDisplacement(current, proposed, allowedRadius); -``` - -Return the original straight samples when all perpendicular deviations are below `1e-9`. - -- [ ] **Step 4: Run algorithm and shared safety tests** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bspline.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -``` - -Expected: all pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs ClumsyPilot/tests/verify_path_smoothing_bspline.ps1 -git commit -m "feat: add cubic b-spline path smoother" -``` - -### Task 6: Align retryable feasibility, option snapshots, and arc-length references - -**Context:** Tasks 1–5 are already committed. This corrective task resolves the review-discovered contract gaps before adding the remaining algorithms. - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_runner.ps1` -- Test: `ClumsyPilot/tests/verify_path_smoothing_bspline.ps1` -- Test: `ClumsyPilot/tests/verify_path_smoothing_algorithm_input.ps1` - -**Interfaces:** -- Produces: `SmoothingCandidateStatus.Success`, `RetryableInfeasible`, or `Failed`. -- Produces: immutable method-specific option snapshots carried by `SmoothingAlgorithmInput`. -- Produces: `PathReferenceInterpolator.TryInterpolateByArcLength(IReadOnlyList points, double targetArcLength, out SmoothingPoint2D reference, out string reason)` for all three algorithms. - -- [ ] **Step 1: Write failing status, snapshot, and reference tests** - -The reflection tests must prove: - -```text -RetryableInfeasible attempts exactly 1.00, 0.75, 0.50, 0.25 and ends Infeasible -Failed attempts exactly once and ends Failed -request/config mutation after construction cannot change the internal option snapshot -custom EndpointTangentScale changes the B-spline endpoint handle -non-uniform source samples interpolate by local ArcLength, not point-index ratio -non-finite/non-positive option scalars and a threshold outside (0, π] are rejected before retry -``` - -Use a non-uniform source with local arc lengths `0.00, 0.05, 0.10, 0.125`; at target arc `0.1125`, the reference must lie halfway through the final interval regardless of point count. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_algorithm_input.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bspline.ps1 -``` - -Expected: missing candidate status/snapshot/interpolator assertions fail, and B-spline still ignores the configured endpoint scale. - -- [ ] **Step 3: Implement the retryable candidate state** - -Use the exact internal states: - -```csharp -internal enum SmoothingCandidateStatus -{ - Success, - RetryableInfeasible, - Failed, -} -``` - -`SmoothingCandidate.Success(...)` requires complete segments. `RetryableInfeasible(reason)` and `Failed(reason)` carry no executable geometry. The runner continues only for `RetryableInfeasible`; it stops immediately for `Failed`. Exhausting retryable outcomes returns `AlgorithmRunResult.Infeasible(...)` even when no rejected comparison geometry exists. - -- [ ] **Step 4: Implement immutable options and arc-length interpolation** - -`SmoothingOptionsSnapshot` copies these six scalars from the request configuration into get-only values: - -```text -CubicBSplineEndpointTangentScale -BezierCornerHeadingThresholdRadians -BezierMaximumWindowLengthMeters -BezierHandleLengthRatio -QuinticKnotSpacingMeters -QuinticMinimumKnotSpacingMeters -``` - -Snapshot construction rejects non-finite values; all scales, ratios, windows, and knot lengths must be positive, the Bézier threshold must lie in `(0, π]`, and quintic knot spacing must be at least its configured minimum. These are terminal input failures, not retryable geometry outcomes. - -Use this construction boundary: - -```csharp -internal SmoothingOptionsSnapshot(PathSmoothingConfiguration configuration); - -internal SmoothingAlgorithmInput( - PreparedPath originalPath, - PlanningGridMap map, - VehicleParameters vehicle, - double maximumCollisionCheckStepMeters, - double minimumClearanceReserveMeters, - SmoothingOptionsSnapshot options); -``` - -`SmoothingAlgorithmInput.Options` is get-only and never exposes the mutable public configuration objects. - -`PathReferenceInterpolator.TryInterpolateByArcLength` locates the bracketing source samples by `SmoothingPoint2D.ArcLength` and linearly interpolates position, heading, unwrapped heading, and clearance. A normalized full-segment parameter maps to `targetArc = u * segment.Points[last].ArcLength`; local algorithms map their window or knot interval directly to its endpoint arc lengths. - -- [ ] **Step 5: Correct B-spline option and rejection semantics** - -Replace the hard-coded endpoint scale with `input.Options.CubicBSplineEndpointTangentScale`. Replace point-index reference interpolation with `PathReferenceInterpolator`. Evaluated-point movement excess returns `RetryableInfeasible`; invalid values, impossible endpoint-tangent construction, and non-finite controls remain `Failed`. Never clamp evaluated curve samples. - -- [ ] **Step 6: Run corrective and shared regression checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_algorithm_input.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bspline.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -``` - -Expected: all pass; retryable geometry rejection uses all four strengths, while numerical failure still uses one. - -- [ ] **Step 7: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs ClumsyPilot/tests/verify_path_smoothing_algorithm_input.ps1 ClumsyPilot/tests/verify_path_smoothing_runner.ps1 ClumsyPilot/tests/verify_path_smoothing_bspline.ps1 -git commit -m "fix: align smoothing feasibility and option flow" -``` - -### Task 7: Local cubic Bézier smoother - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_bezier.ps1` - -**Interfaces:** -- Implements: `IPathSmoother.Method == SmoothingMethod.LocalCubicBezier`. -- Consumes: the immutable Bézier heading-change threshold, maximum local window, and handle-length ratio from `SmoothingAlgorithmInput.Options`. - -- [ ] **Step 1: Write failing local-behavior tests** - -Assert no window is created for a straight line; one corner creates one replacement; overlapping windows merge; outside-window samples remain bitwise equal; endpoints and switch points remain fixed. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bezier.ps1 -``` - -Expected: missing `LocalCubicBezierSmoother`. - -- [ ] **Step 3: Implement window detection and Bézier evaluation** - -Detect corners from absolute change in travel tangent. Merge touching windows. For each window: - -```csharp -SmoothingPoint2D p1 = p0 + entryTangent * handleLength; -SmoothingPoint2D p2 = p3 - exitTangent * handleLength; -SmoothingPoint2D value = - Math.Pow(1d - t, 3d) * p0 + - 3d * Math.Pow(1d - t, 2d) * t * p1 + - 3d * (1d - t) * t * t * p2 + - t * t * t * p3; -``` - -For each evaluated point, map `t` to `s_ref = s_entry + t * (s_exit - s_entry)` and obtain the source reference through `PathReferenceInterpolator`. Compare displacement with `max(0, reference.BodyClearance - MinimumClearanceReserveMeters)`. If any point exceeds that radius, return `RetryableInfeasible` with no executable geometry; do not pointwise clamp or project curve samples. Retain original samples outside merged windows. - -Tests must set non-default threshold, window length, and handle ratio values and prove each option changes only its intended behavior. - -- [ ] **Step 4: Run Bézier and regression checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bezier.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: all pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs ClumsyPilot/tests/verify_path_smoothing_bezier.ps1 -git commit -m "feat: add local cubic bezier smoother" -``` - -### Task 8: Piecewise quintic Hermite smoother - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_quintic.ps1` - -**Interfaces:** -- Implements: `IPathSmoother.Method == SmoothingMethod.PiecewiseQuintic`. -- Consumes: immutable knot and minimum-knot spacing from `SmoothingAlgorithmInput.Options`. -- Produces: C2-connected local polynomial segments without crossing direction boundaries. - -- [ ] **Step 1: Write failing continuity and degeneracy tests** - -At every shared knot evaluate left/right position, first derivative, and second derivative; assert equality within `1e-6`. Verify exact endpoint pose, bounded movement, and stable failure for a segment shorter than the configured minimum knot spacing. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_quintic.ps1 -``` - -Expected: missing `PiecewiseQuinticSmoother`. - -- [ ] **Step 3: Implement normalized quintic Hermite basis** - -For each knot interval use normalized `t ∈ [0,1]` and solve the six coefficients independently for X and Y from: - -```text -p(0)=p0, p(1)=p1 -p'(0)=v0, p'(1)=v1 -p''(0)=a0, p''(1)=a1 -``` - -Derive endpoint velocities from travel tangents times interval length. Blend shared accelerations once per knot and reuse the same value on both adjacent intervals. Reject singular or non-finite coefficients before sampling. - -Map every interval sample to `s_ref = s_knot0 + t * (s_knot1 - s_knot0)` through `PathReferenceInterpolator`. Movement-bound excess returns `RetryableInfeasible`; singular coefficients, non-finite derivatives, and invalid spacing return `Failed`. Never clamp evaluated polynomial samples. - -- [ ] **Step 4: Run quintic, geometry, and validator checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_quintic.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -``` - -Expected: all pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs ClumsyPilot/tests/verify_path_smoothing_quintic.ps1 -git commit -m "feat: add piecewise quintic path smoother" -``` - -### Task 9: Formal smoothing facade and explicit coarse-path fallback - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_service.ps1` - -**Interfaces:** -- Produces: `PathSmoothingResult Smooth(PathSmoothingRequest, CancellationToken)`. -- Consumes: preprocessor, method registry, retry runner, analyzer, validator. - -- [ ] **Step 1: Write failing facade tests** - -Cover valid straight success, invalid input, cancellation, infeasible-without-fallback, and infeasible-with-verified-fallback. Invalid input must include NaN, non-positive scales/windows/spacing, Bézier threshold outside `(0, π]`, and `KnotSpacingMeters < MinimumKnotSpacingMeters`. Assert fallback points use `CoarsePathFallback` and status never equals `Success`. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_service.ps1 -``` - -Expected: missing `PathSmoothingService`. - -- [ ] **Step 3: Implement orchestration** - -Use a stable method registry: - -```csharp -private IPathSmoother Resolve(SmoothingMethod method) => - method switch - { - SmoothingMethod.CubicBSpline => _bSpline, - SmoothingMethod.LocalCubicBezier => _bezier, - SmoothingMethod.PiecewiseQuintic => _quintic, - _ => throw new ArgumentOutOfRangeException(nameof(method)), - }; -``` - -Validate the full configuration and coarse path before constructing `SmoothingOptionsSnapshot` or starting finite retries; map every configuration-contract violation to `InvalidInput`. On allowed fallback, convert the revalidated coarse points to `SmoothedPathPoint` with `CoarsePathFallback`, re-run shared geometry analysis, and return `Fallback`, preserving the failed method diagnostics. - -- [ ] **Step 4: Run all core smoothing verifications** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_contracts.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_service.ps1 -``` - -Expected: all pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade ClumsyPilot/tests/verify_path_smoothing_service.ps1 -git commit -m "feat: expose validated path smoothing service" -``` - -### Task 10: Comparison metrics, isolation, and deterministic ranking - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` - -**Interfaces:** -- Produces: `Compare(PathSmoothingComparisonRequest, CancellationToken)`. -- Ranking order: feasible count; median variation energy; worst peak utilization; worst clearance loss; median length increase; median elapsed. -- Timing protocol: one unmeasured warm-up plus five measured deterministic executions per scenario and method; rank by measured median. -- `SmoothingTimingSummary` exposes a read-only five-value `MeasuredElapsedMilliseconds`, `MedianElapsedMilliseconds`, and `IsDeterministic`. - -- [ ] **Step 1: Write failing comparison and ranking tests** - -Create synthetic entries whose order changes at each tie-break level. Verify one method failure does not stop the remaining methods, cancellation does, and no feasible method produces a null recommendation. - -Also assert the result always contains one separately analyzed raw-path baseline plus exactly one entry for each requested method; the raw baseline is never treated as a candidate method in ranking. - -For timing, assert the warm-up is excluded, exactly five samples remain, and a mismatch in status, point count, segment count, or stable geometry digest produces a non-deterministic diagnostic that excludes the method from recommendation. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 -``` - -Expected: missing comparison service/ranker. - -- [ ] **Step 3: Implement metric normalization and lexicographic comparison** - -Use stable median: - -```csharp -double Median(IReadOnlyList values) -{ - double[] sorted = values.OrderBy(value => value).ToArray(); - int middle = sorted.Length / 2; - return sorted.Length % 2 == 1 - ? sorted[middle] - : (sorted[middle - 1] + sorted[middle]) / 2d; -} -``` - -Use absolute deltas when a raw denominator has magnitude below `1e-12`. The comparison service must force `AllowFallbackToCoarsePath = false` so fallback cannot masquerade as method success. - -Run each method once for warm-up and five times for measurement against the same immutable prepared input. Use the first measured result as the canonical comparison geometry only after all five measured outputs match its stable status and geometry digest. `StableGeometryDigest` writes status, segment metadata, enum values, and every double through `BitConverter.DoubleToInt64Bits` in fixed little-endian order, then computes SHA-256; do not use `GetHashCode()`. Store all five elapsed values plus their median; only the median participates in the final lexicographic timing tie-break. - -- [ ] **Step 4: Run comparison and service tests** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_service.ps1 -``` - -Expected: all pass. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 -git commit -m "feat: compare and rank smoothing methods" -``` - -### Task 11: Versioned fast fixtures and existing end-to-end scenarios - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json` -- Create: `ClumsyPilot/tests/generate_path_smoothing_fixtures.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_integration.ps1` - -**Interfaces:** -- Produces: eight immutable fast fixtures and four end-to-end comparison requests. -- Consumes: existing `CoarsePathScenarioFactory.Create(scenario)` for end-to-end requests. - -- [ ] **Step 1: Write failing fixture tests** - -Assert the exact IDs: - -```powershell -$expected = @( - 'straight', 'single-turn', 's-bend', 'large-heading-change', - 'rectangle-detour', 'multi-obstacle-detour', - 'narrow-corridor', 'forward-reverse-switch') -``` - -Verify unique IDs, version > 0, matching fingerprint, valid segment coverage, and that fixture-only loading has no reference to `HybridAStarPlanner`. - -- [ ] **Step 2: Write failing end-to-end tests** - -For `ExplicitEmpty`, `RectangleDetour`, `ManualAndTwoLeg`, and `ReverseGearSwitch`, call the existing planner then comparison service. Assert the raw path is unchanged and every successful smoothing entry preserves segment count and gear-switch count. - -- [ ] **Step 3: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_fixtures.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_integration.ps1 -``` - -Expected: missing fixture loader/factory. - -- [ ] **Step 4: Implement deterministic JSON fixtures** - -Serialize with invariant numbers and explicit schema: - -```json -{ - "schemaVersion": 1, - "scenarios": [ - { - "id": "straight", - "fixtureVersion": 1, - "configurationFingerprint": "sha256:", - "map": { - "xMinMm": 0, "xMaxMm": 6000, - "yMinMm": 0, "yMaxMm": 4000, - "resolutionMm": 50, - "obstacles": [] - }, - "vehicle": { "lengthMeters": 0.8, "widthMeters": 0.6, "safetyMarginMeters": 0.05, "maximumCurvaturePerMeter": 0.8333333333333334 }, - "path": [], - "segments": [] - } - ] -} -``` - -Populate all eight scenarios from explicitly verified successful coarse-path outputs by running: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\generate_path_smoothing_fixtures.ps1 -OutputPath .\ClumsyPilot\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json -``` - -`SmoothingFixtureGenerator.Generate(string outputPath, bool overwrite)` must refuse an existing target when `overwrite` is false. The script maps `-Overwrite` to that argument; normal tests call only `LoadAndVerify` and never regenerate data. Each obstacle is serialized as a typed circle or axis-aligned rectangle DTO so the loader can rebuild the exact `PlanningGridMap` without Hybrid A*. - -- [ ] **Step 5: Run fixture and integration checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_fixtures.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_integration.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -``` - -Expected: all pass. Record actual end-to-end elapsed values as diagnostics, not hardware-dependent pass thresholds. - -- [ ] **Step 6: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test ClumsyPilot/tests/generate_path_smoothing_fixtures.ps1 ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 ClumsyPilot/tests/verify_path_smoothing_integration.ps1 -git commit -m "test: add path smoothing scenarios and fixtures" -``` - -### Task 12: Shared figure model, IEEE SVG, and UTF-8 CSV - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` - -**Interfaces:** -- Consumes: immutable `PathSmoothingComparisonResult`, map, start/goal, scenario label. -- Produces: one immutable figure model, UTF-8 SVG, and UTF-8-BOM CSV. -- Portability boundary: text SVG is the editable master and requires exact fonts on the viewing machine; it is not claimed as a directly submittable IEEE vector file. - -- [ ] **Step 1: Write failing style and serialization tests** - -Assert exact colors, line styles, panel labels, view box, equal XY scale, legend order, XML escaping, UTF-8 Chinese, BOM bytes `EF BB BF`, and invariant decimal points. - -Include one rejected candidate in the model and assert its normal method-colored curve plus violation cross markers are present while its metrics row says `Infeasible`. Include one numerical failure and assert its legend/row remains but no fake path element is emitted. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 -``` - -Expected: missing `IeeeFigureStyle`. - -- [ ] **Step 3: Implement one physical layout model** - -Define all coordinates in typographic points (`72 pt/in`): - -```csharp -public const double FigureWidthPoints = 7.16d * 72d; -public const double FigureHeightPoints = 5.20d * 72d; -public const string RawColor = "#4D4D4D"; -public const string BSplineColor = "#0072B2"; -public const string BezierColor = "#D55E00"; -public const string QuinticColor = "#009E73"; -public const string LimitColor = "#CC79A7"; -``` - -Allocate 60% width to panel `(a)`, split the right side between `(b)` curvature and `(c)` metrics, and compute one world-to-panel transform with equal X/Y scale. - -Use `9 pt` for coordinate ticks, axis labels, legend, and table body; use `10 pt` for panel labels. Do not create any text smaller than `9 pt` at final physical size. - -- [ ] **Step 4: Implement SVG and CSV** - -SVG text must use explicit runs: - -```xml -车辆曲率 κ (m⁻¹) -``` - -CSV header order is fixed: - -```text -ScenarioId,Method,Status,PathLengthMeters,MaximumAbsoluteVehicleCurvaturePerMeter,RootMeanSquareVehicleCurvaturePerMeter,TotalAbsoluteCurvatureVariationPerMeter,CurvatureVariationEnergy,MinimumBodyClearanceMeters,MedianElapsedMilliseconds,TimingSampleCount,IsDeterministic,RetryCount,AcceptedStrength -``` - -- [ ] **Step 5: Run SVG/CSV and comparison checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 -``` - -Expected: all pass. - -- [ ] **Step 6: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 -git commit -m "feat: render ieee smoothing svg and metrics" -``` - -### Task 13: Windows font validation and 600 dpi PNG export - -**Files:** -- Modify: `ClumsyPilot/ClumsyPilot.csproj` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_png.ps1` - -**Interfaces:** -- Produces: atomic `.svg`, `.png`, `.csv` export; report failures do not mutate comparison results. -- Runtime boundary: `SmoothingPngRenderer` is Windows-only; SVG/CSV remain usable without GDI+. -- Submission boundary: conversion of the verified SVG master to font-embedded or outlined PDF/EPS is explicit external publishing work, not a hidden exporter side effect. - -- [ ] **Step 1: Add the failing PNG/font verification** - -The script must assert: - -```text -PNG signature and CRC-valid chunks -IHDR width=4296 and height=3120 -pHYs X=23622 and Y=23622 pixels/meter -SimSun and Times New Roman were resolved by exact family name -mixed sample "粗路径 κ(s) X (m) −π" produced non-empty glyph bounds -missing-font test returns FontUnavailable -atomic export leaves no .tmp file -``` - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 -``` - -Expected: missing renderer and exporter. - -- [ ] **Step 3: Add the exact Windows rendering dependency** - -Add: - -```xml - -``` - -Then run: - -```powershell -dotnet restore .\ClumsyPilot\ClumsyPilot.csproj -``` - -Expected: restore exits 0. Do not add cross-platform fallback switches; Microsoft documents `System.Drawing.Common` as Windows-only. - -- [ ] **Step 4: Implement exact font resolution and mixed-run baseline layout** - -Use `InstalledFontCollection` to require `SimSun` and `Times New Roman`. Split text by Unicode category/CJK ranges, measure each run with typographic `StringFormat`, and align runs by font-family ascent: - -```csharp -float baseline = top + maxAscentPixels; -float runTop = baseline - family.GetCellAscent(style) * emPixels / - family.GetEmHeight(style); -``` - -Return `FontUnavailable` before creating output files if either exact family is absent. - -- [ ] **Step 5: Render the shared model and write validated PNG** - -Render at `4296 × 3120`, opaque white background, anti-aliased geometry, and no gradients/shadows. Convert bitmap pixels to RGBA and use the existing validated PNG path, extending it to insert a `pHYs` chunk with `23622` pixels/meter and correct CRC before `IDAT`. - -Export all three files to temporary siblings, validate each, then rename into place. On failure, remove only those exact temporary siblings. - -- [ ] **Step 6: Run PNG, map-image, and build checks** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 -``` - -Expected: all pass; existing map PNG export remains valid. - -- [ ] **Step 7: Commit** - -```powershell -git add -- ClumsyPilot/ClumsyPilot.csproj ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization ClumsyPilot/tests/verify_path_smoothing_png.ps1 -git commit -m "feat: export ieee smoothing png reports" -``` - -### Task 14: Module documentation, report batch entry, and full verification - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs` -- Create: `ClumsyPilot/tests/run_path_smoothing_comparison.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_documentation.ps1` - -**Interfaces:** -- Produces: documented formal call example and a developer-only batch command that writes reports under `ClumsyPilot/obj/path_smoothing_reports`. - -- [ ] **Step 1: Write failing documentation and batch-entry verification** - -Assert README contains exact sections for units, facade usage, status handling, fallback, fixture freshness, IEEE colors/fonts, Windows PNG limitation, SQP boundary, and output files. Assert the batch runner offers `-FixtureOnly` and never writes under source directories. - -- [ ] **Step 2: Run and confirm RED** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_documentation.ps1 -``` - -Expected: missing README and demo. - -- [ ] **Step 3: Write README and batch demo** - -Document this minimal formal flow: - -```csharp -if (coarseResult.PlanningResult.Status != PlanningStatus.Success) - return; -var smoothing = new PathSmoothingService().Smooth( - new PathSmoothingRequest( - coarseResult.PlanningResult.Path, - coarseResult.PlanningResult.Segments, - coarseResult.MapResult.Map, - job.Vehicle, - smoothingConfiguration), - cancellationToken); -if (smoothing.Status == PathSmoothingStatus.Success || - smoothing.Status == PathSmoothingStatus.FallbackToCoarsePath) - ConsumeSpatialReference(smoothing.Path, smoothing.Segments); -``` - -The batch script builds once, runs eight fixtures by default, optionally runs the four end-to-end cases, prints one-line metrics per method, and writes only beneath `ClumsyPilot/obj/path_smoothing_reports`. - -- [ ] **Step 4: Run the complete verification suite** - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_contracts.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bspline.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_bezier.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_quintic.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_service.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_fixtures.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_integration.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_documentation.ps1 -``` - -Expected: build exits 0 and every script prints its passed message with no terminating errors. - -- [ ] **Step 5: Generate and inspect representative reports** - -```powershell -powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\run_path_smoothing_comparison.ps1 -FixtureOnly -``` - -Inspect `straight`, `rectangle-detour`, and `forward-reverse-switch` PNG/SVG files. Confirm four-method legend order, equal path axes, readable 9–10 pt text, no Chinese mojibake, no clipping/overlap, curvature limits, and infeasible markers where applicable. Confirm the text SVG portability limitation and external PDF/EPS publishing step are documented. - -- [ ] **Step 6: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs ClumsyPilot/tests/run_path_smoothing_comparison.ps1 ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 -git commit -m "docs: document path smoothing comparison workflow" -``` diff --git a/docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md b/docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md deleted file mode 100644 index 32bf1fb..0000000 --- a/docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md +++ /dev/null @@ -1,1574 +0,0 @@ -# Local G2 Path Presmoothing Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 在 Hybrid A* 与 SQP 之间新增局部 G2 五次 Hermite 预平滑主路线,安全地改善可处理的运动基元曲率跳变,并输出完整、可诊断的 SQP 初始路径。 - -**Architecture:** 新路线作为 `PathSmoothingService` 中的专用分支实现,不塞入旧的“强度重试”运行器。`LocalG2` 子目录负责曲率事件检测、窗口规划、五次 Hermite 几何、候选构造与评价;现有 `PathGeometryAnalyzer` 和 `SmoothedPathValidator` 继续作为统一几何和完整车体安全真源。 - -**Tech Stack:** C# 10、.NET Standard 2.0、现有规划栅格和完整车体碰撞检查器、PowerShell 反射验证脚本、Git。 - -**Design spec:** `docs/superpowers/specs/2026-07-30-local-g2-path-presmoothing-design.md` - -## Global Constraints - -- 不修改 Hybrid A* 的几何中心参考模型、搜索代价或运动基元生成策略。 -- 不实现 SQP、速度规划、轮速分解或每点速度上限。 -- 前进/倒车是硬分段;绝不跨换向点计算或平滑曲率。 -- 单事件窗口默认 `0.20 m`、`0.50 m`、`0.80 m`,允许左右不对称。 -- 最大局部路径偏移默认 `0.10 m`。 -- 曲率事件检测门槛为 `max(0.001 1/m, 0.05 × 最大车辆曲率)`。 -- 成功区域的峰值 `|dκ/ds|` 至少改善 `20%`。 -- 曲率变化代价只允许 `2%` 数值容差。 -- 每个区域最多评估 `12` 个确定性候选。 -- 输出采样间距和连续碰撞检查步长默认均为 `0.025 m`。 -- 扩大车体之外仍需保留默认 `0.02 m` 净空。 -- 旧 B 样条、局部 Bézier和旧五次算法不重写;其现有测试必须保持通过。 -- 所有新行为先写失败测试,再写最小实现;每个任务单独提交。 -- 工作区已有未提交修改。每次暂存必须显式列出本任务文件,禁止覆盖或顺带提交无关改动。 - ---- - -## File Structure - -### Public contracts - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs` - 新主路线的语义化配置和默认值。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs` - 单区域“已改善/保留原路径”状态。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs` - 稳定的区域失败原因枚举。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs` - 不可变区域报告。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs` - 追加 `LocalG2Quintic`,不改变旧成员顺序。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs` - 追加 `Complete`、`PartialImprovement`、`NotNeeded`、`Unchanged`。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs` - 追加 `LocalG2Transition`。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs` - 新增 `VehicleCurvatureDerivative`,保留旧构造函数兼容性。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs` - 新增峰值曲率变化率和“曲率变化代价”别名。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs` - 新增区域报告和本地主路线发布工厂。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs` - 暴露 `LocalG2Quintic` 配置。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs` - 深复制新配置。 - -### Local G2 implementation - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs` - 已校验的不可变配置快照。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs` - 单个同方向运动基元曲率事件。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs` - 从原始粗路径检测事件。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs` - 一个独立或合并后的局部区域。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` - 生成长度和左右分配均有界的区域候选。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs` - 纯二维五次 Hermite 数学。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` - 一个待验证的局部替换几何。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` - 用方向、端点曲率和内部共享曲率生成有限候选。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs` - 将局部替换拼入完整 `PreparedPath`。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` - 统一分析、偏移、质量、碰撞、净空和确定性排序。 -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` - 按区域运行、局部回退、全局复验并生成最终状态和报告。 - -### Shared infrastructure - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs` - 保留真实起点车辆曲率。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs` - 暴露峰值 `|dκ/ds|`。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs` - 在方向段内统一计算每点 `dκ/ds` 和峰值。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs` - 可选保留方向段起点的真实车辆曲率边界条件。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs` - 将粗路径首点曲率传入方向段边界条件。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs` - 粗路径也使用统一几何分析器。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs` - 校验并保留每点 `dκ/ds`。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` - 验证新配置并分派至专用流水线。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs` - 适配统一粗路径基线签名;默认比较列表仍只有旧三种算法。 -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` - 记录新主路线、状态和 SQP 边界。 - -### Verification - -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_contracts.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_geometry.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_documentation.ps1` - ---- - -### Task 1: Add Local G2 public contracts and immutable configuration - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs` -- Test: `ClumsyPilot/tests/verify_path_smoothing_contracts.ps1` - -**Interfaces:** - -- Produces: `SmoothingMethod.LocalG2Quintic`. -- Produces: `PathSmoothingStatus.Complete`, `PartialImprovement`, `NotNeeded`, `Unchanged`. -- Produces: `LocalG2QuinticOptions` with the exact defaults from Global Constraints. -- Produces: `SmoothedPathPoint.VehicleCurvatureDerivative`. -- Produces: `PathSmoothingResult.PublishLocalG2(...)`. -- Produces: immutable `PathSmoothingResult.RegionReports`. - -- [ ] **Step 1: Extend the contract verification first** - -Append assertions equivalent to: - -```powershell -$localOptionsType = Get-RequiredType ($root + 'LocalG2QuinticOptions') -$regionStatusType = Get-RequiredType ($root + 'PathSmoothingRegionStatus') -$regionFailureType = Get-RequiredType ($root + 'PathSmoothingRegionFailureReason') -$regionReportType = Get-RequiredType ($root + 'PathSmoothingRegionReport') - -Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic,LocalG2Quintic' ` - ([string]::Join(',', [Enum]::GetNames($methodType))) ` - 'The Local G2 method must be appended without reordering legacy methods.' -Assert-True ([Enum]::GetNames($statusType) -contains 'Complete') 'Complete status is required.' -Assert-True ([Enum]::GetNames($statusType) -contains 'PartialImprovement') 'Partial status is required.' -Assert-True ([Enum]::GetNames($statusType) -contains 'NotNeeded') 'NotNeeded status is required.' -Assert-True ([Enum]::GetNames($statusType) -contains 'Unchanged') 'Unchanged status is required.' - -$local = [Activator]::CreateInstance($localOptionsType) -Assert-Near 0.20 $local.MinimumWindowLengthMeters 'Minimum Local G2 window must be 0.20 m.' -Assert-Near 0.50 $local.PreferredWindowLengthMeters 'Preferred Local G2 window must be 0.50 m.' -Assert-Near 0.80 $local.MaximumWindowLengthMeters 'Maximum Local G2 window must be 0.80 m.' -Assert-Near 0.10 $local.MaximumDeviationMeters 'Maximum Local G2 deviation must be 0.10 m.' -Assert-Near 0.001 $local.AbsoluteCurvatureJumpFloorPerMeter 'Absolute jump floor must be 0.001 1/m.' -Assert-Near 0.05 $local.CurvatureJumpRatioOfMaximum 'Relative jump threshold must be 5 percent.' -Assert-Near 0.20 $local.MinimumPeakGradientImprovementRatio 'Peak improvement must be 20 percent.' -Assert-Near 0.02 $local.MaximumVariationCostRegressionRatio 'Variation cost tolerance must be 2 percent.' -Assert-Equal 12 $local.MaximumCandidatesPerRegion 'At most twelve candidates are allowed.' - -Assert-True ($pointType.GetProperty('VehicleCurvatureDerivative') -ne $null) ` - 'Smoothed points must expose d-kappa/d-s.' -Assert-True ($resultType.GetProperty('RegionReports') -ne $null) ` - 'Local G2 results must expose immutable region reports.' -``` - -Also mutate every new option on the original configuration after building a `PathSmoothingRequest`, then assert the request snapshot still contains defaults. Repeat through the configuration getter to prove defensive copying. - -- [ ] **Step 2: Run the unchanged assembly against the new contract test** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 -``` - -Expected: build passes; contract script fails because `LocalG2QuinticOptions` or the new enum members do not exist. - -- [ ] **Step 3: Implement the public options and enum additions** - -Create the options exactly as: - -```csharp -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; - -public sealed class LocalG2QuinticOptions -{ - public double MinimumWindowLengthMeters { get; set; } = 0.20d; - public double PreferredWindowLengthMeters { get; set; } = 0.50d; - public double MaximumWindowLengthMeters { get; set; } = 0.80d; - public double MaximumDeviationMeters { get; set; } = 0.10d; - public double AbsoluteCurvatureJumpFloorPerMeter { get; set; } = 0.001d; - public double CurvatureJumpRatioOfMaximum { get; set; } = 0.05d; - public double MinimumPeakGradientImprovementRatio { get; set; } = 0.20d; - public double MaximumVariationCostRegressionRatio { get; set; } = 0.02d; - public int MaximumCandidatesPerRegion { get; set; } = 12; -} -``` - -Append, rather than reorder, enum members: - -```csharp -public enum PathSmoothingRegionStatus -{ - Improved, - RetainedOriginal, -} - -public enum PathSmoothingRegionFailureReason -{ - None, - WindowUnavailable, - CandidateGenerationFailed, - Collision, - InsufficientClearance, - CurvatureExceeded, - CurvatureOvershoot, - DeviationExceeded, - InsufficientImprovement, - VariationCostRegression, - GlobalValidationRollback, -} -``` - -Add `LocalG2Transition` at the end of `SmoothedPathPointSource`. Add a new `SmoothedPathPoint` constructor containing `vehicleCurvatureDerivativePerSquareMeter` immediately after vehicle curvature; retain the old constructor and delegate to the new one with `0d`. - -- [ ] **Step 4: Implement immutable report and result publication** - -Use this exact public report constructor: - -```csharp -public PathSmoothingRegionReport( - int segmentIndex, - double startArcLengthMeters, - double endArcLengthMeters, - IReadOnlyList curvatureJumpsPerMeter, - double plannedWindowLengthMeters, - double actualWindowLengthMeters, - double leftWindowLengthMeters, - double rightWindowLengthMeters, - int candidateCount, - int selectedCandidateIndex, - PathSmoothingRegionStatus status, - PathSmoothingRegionFailureReason failureReason, - double rawPeakCurvatureDerivativePerSquareMeter, - double resultPeakCurvatureDerivativePerSquareMeter, - double rawCurvatureVariationCost, - double resultCurvatureVariationCost, - double maximumDeviationMeters, - double minimumBodyClearanceMeters, - double maximumAbsoluteVehicleCurvaturePerMeter) -``` - -Use this factory boundary: - -```csharp -public static PathSmoothingResult PublishLocalG2( - PathSmoothingStatus status, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics, - IReadOnlyList regionReports) -``` - -Allow only `Complete`, `PartialImprovement`, `NotNeeded`, and `Unchanged`; require feasible diagnostics and a non-empty path. `Failure` continues to publish an empty path. Existing `Success` and `Fallback` factories remain source-compatible and publish an empty immutable report collection. - -`PathSmoothingRegionReport` must defensively copy its curvature-jump list. Use `SelectedCandidateIndex = -1` when no candidate was selected. - -Add `MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter` to `PathQualityMetrics`. Retain the old constructor and delegate to a new constructor that accepts the peak derivative immediately after maximum absolute curvature. Keep: - -```csharp -public double CurvatureVariationCost => CurvatureVariationEnergy; -``` - -as a compatibility-safe name for new code and reports. - -- [ ] **Step 5: Copy and validate the new configuration** - -Add: - -```csharp -public LocalG2QuinticOptions LocalG2Quintic { get; } = new LocalG2QuinticOptions(); -``` - -to `PathSmoothingConfiguration`. Copy every new property in `PathSmoothingRequest.CopyConfiguration`; do not reuse the caller-owned options object. - -- [ ] **Step 6: Build and run the contract test** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 -``` - -Expected: build succeeds and output ends with `Path smoothing contract checks passed.` - -- [ ] **Step 7: Commit only Task 1 files** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs ` - ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 -git diff --cached --check -git commit -m "feat: add local G2 smoothing contracts" -``` - ---- - -### Task 2: Preserve the true Hybrid A* start curvature - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs` -- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - -**Interfaces:** - -- Consumes: `PlanningRequest.StartVehicleCurvature`. -- Produces: `PlanningResult.Path[0].VehicleCurvature == StartVehicleCurvature`. -- Later tasks rely on the first point to expose a real start-to-first-primitive transition. - -- [ ] **Step 1: Add a failing end-to-end assertion** - -Before the first successful `Plan` call in `verify_coarse_path_integration.ps1`, set: - -```powershell -$request.StartVehicleCurvature = [double]0.20 -``` - -After success, add: - -```powershell -Assert-Near 0.20 $result.Path[0].VehicleCurvature ` - 'The assembled start point must retain the requested physical vehicle curvature.' -``` - -- [ ] **Step 2: Run the regression test and verify RED** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_coarse_path_integration.ps1 -``` - -Expected: fail at the new start-curvature assertion because the assembler currently publishes the first primitive curvature at index zero. - -- [ ] **Step 3: Make the minimal assembler change** - -Replace: - -```csharp -double currentCurvature = backtrackedPath.Primitives.Count > 0 - ? backtrackedPath.Primitives[0].CurvaturePerMeter - : backtrackedPath.StartCurvaturePerMeter; -``` - -with: - -```csharp -double currentCurvature = backtrackedPath.StartCurvaturePerMeter; -``` - -Do not change `currentDirection`: direction still comes from the first primitive when one exists. - -- [ ] **Step 4: Run CoarsePath regression checks** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_coarse_path_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_coarse_path_search.ps1 -``` - -Expected: both scripts end in their `... checks passed.` messages. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs ` - ClumsyPilot/tests/verify_coarse_path_integration.ps1 -git diff --cached --check -git commit -m "fix: preserve coarse path start curvature" -``` - ---- - -### Task 3: Compute `dκ/ds` once and use one fair raw-path analyzer - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_geometry.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_validation.ps1` - -**Interfaces:** - -- Produces: `PathGeometryAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter`. -- Produces: each analyzed `SmoothedPathPoint.VehicleCurvatureDerivative`. -- Produces: optional `PreparedDirectionSegment.StartVehicleCurvaturePerMeter`, used only when a real boundary state is available. -- Produces: `RawPathBaselineBuilder.TryCreate(..., PathGeometryAnalyzer analyzer, double outputSpacingMeters, ...)`. -- Guarantees: no derivative crosses a gear-switch duplicate. - -- [ ] **Step 1: Add failing geometry assertions** - -Extend `verify_path_smoothing_geometry.ps1`: - -```powershell -Assert-Near 0.0 $straightAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 0.000000001 ` - 'A straight must have zero peak d-kappa/d-s.' -foreach ($point in $straightAnalysis.Path) { - Assert-Near 0.0 $point.VehicleCurvatureDerivative 0.000000001 ` - 'A straight point must carry zero d-kappa/d-s.' -} - -Assert-True (-not [double]::IsNaN($switchLeft.VehicleCurvatureDerivative)) ` - 'The forward side of a gear switch must have a finite one-sided derivative.' -Assert-True (-not [double]::IsNaN($switchRight.VehicleCurvatureDerivative)) ` - 'The reverse side of a gear switch must have a finite one-sided derivative.' -Assert-Near 0.0 $reverseStraightStart.VehicleCurvatureDerivative 0.000000001 ` - 'No curvature derivative may cross from the preceding forward arc into a reverse straight.' - -$physicalStart = New-DirectionSegmentWithStartCurvature 0 $forward 0.20 @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) -$physicalStartAnalysis = Invoke-Analysis @($physicalStart) -Assert-Near 0.20 $physicalStartAnalysis.Path[0].VehicleCurvature 0.000000001 ` - 'The unified analyzer must retain a real start steering-curvature boundary state.' -Assert-True ($physicalStartAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter -gt 0.0) ` - 'A real start-curvature mismatch must remain visible to the quality analyzer.' -``` - -Extend validation tests to assert that a nonzero derivative survives clearance recomputation unchanged. - -- [ ] **Step 2: Run tests and verify RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -``` - -Expected: fail because analysis and points do not expose the derivative. - -- [ ] **Step 3: Preserve real direction-segment start curvature** - -Add a constructor overload: - -```csharp -public PreparedDirectionSegment( - int segmentIndex, - TravelDirection direction, - IReadOnlyList points, - bool startsAtGearSwitch, - bool endsAtGearSwitch, - double? startVehicleCurvaturePerMeter) -``` - -The existing five-argument constructor delegates with `null`, preserving legacy algorithms. Validate a supplied value as finite and expose it as `double? StartVehicleCurvaturePerMeter`. - -`PathSmoothingPreprocessor` must call the new overload with the original `CoarsePathPoint.VehicleCurvature` at that direction segment’s first raw index. Local G2 splicing in Task 6 must carry the value forward unchanged. - -- [ ] **Step 4: Compute curvature derivatives per direction segment** - -In `TryAnalyzeSegment`, first compute all `vehicleCurvatures`, then compute: - -```csharp -if (count == 1) -{ - curvatureDerivatives[index] = 0d; -} -else if (index == 0) -{ - curvatureDerivatives[index] = - (vehicleCurvatures[1] - vehicleCurvatures[0]) / - (localArcLengths[1] - localArcLengths[0]); -} -else if (index == count - 1) -{ - curvatureDerivatives[index] = - (vehicleCurvatures[index] - vehicleCurvatures[index - 1]) / - (localArcLengths[index] - localArcLengths[index - 1]); -} -else -{ - curvatureDerivatives[index] = - (vehicleCurvatures[index + 1] - vehicleCurvatures[index - 1]) / - (localArcLengths[index + 1] - localArcLengths[index - 1]); -} -``` - -Reject non-finite derivatives. Track the maximum absolute derivative. Construct output points only after both arrays are complete. Keep all calculations inside the current direction segment loop. - -After ordinary geometric curvature is computed, but before `dκ/ds`, apply a finite start-boundary override: - -```csharp -if (segment.StartVehicleCurvaturePerMeter.HasValue) -{ - double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; - vehicleCurvatures[0] = segment.StartVehicleCurvaturePerMeter.Value; - geometricCurvatures[0] = directionSign * vehicleCurvatures[0]; -} -``` - -This is required because the vehicle’s physical start steering state is not recoverable from the first outgoing chord. Both raw and candidate paths pass through this same analyzer and the same boundary override. - -Rename only the user-facing description from “energy” to “cost”; retain the existing `CurvatureVariationEnergy` property as a compatibility alias: - -```csharp -public double CurvatureVariationCost => CurvatureVariationEnergy; -``` - -- [ ] **Step 5: Replace the unfair raw baseline** - -Change `RawPathBaselineBuilder.TryCreate` to analyze `preparedPath.Segments` through the same `PathGeometryAnalyzer` and `outputSpacingMeters`, then validate that analyzed path: - -```csharp -if (!analyzer.TryAnalyze( - preparedPath.Segments, - outputSpacingMeters, - out PathGeometryAnalysis analysis, - out reason)) -{ - return false; -} - -if (!validator.TryValidate( - analysis.Path, - analysis.Segments, - preparedPath, - request.Map, - request.Vehicle, - maximumCollisionCheckStepMeters, - out IReadOnlyList safePath, - out double minimumClearanceMeters, - out reason)) -{ - return false; -} -``` - -Build baseline metrics from `analysis`, including peak derivative, instead of recomputing from stored Hybrid A* curvature. Update all three callers with one shared analyzer and `configuration.OutputSpacingMeters`. - -- [ ] **Step 6: Preserve derivatives through validation** - -Update `SmoothedPathValidator.IsValidPoint` to require a finite derivative. When rebuilding points with checked clearance, copy `VehicleCurvatureDerivative` rather than falling back to the legacy constructor. - -- [ ] **Step 7: Run focused and legacy tests** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_validation.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 -``` - -Expected: all four scripts pass. If old comparison percentages change because the baseline is now fair, update assertions to the newly unified calculations, not to hard-coded legacy stored-curvature values. - -- [ ] **Step 8: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs ` - ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_validation.ps1 -git diff --cached --check -git commit -m "fix: unify path smoothing geometry metrics" -``` - ---- - -### Task 4: Detect curvature events and plan bounded merged windows - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1` - -**Interfaces:** - -- Produces: - -```csharp -internal bool CurvatureTransitionDetector.TryDetect( - PathSmoothingRequest request, - double maximumVehicleCurvaturePerMeter, - LocalG2OptionsSnapshot options, - out IReadOnlyList transitions, - out string reason); -``` - -- Produces: - -```csharp -internal bool LocalG2WindowPlanner.TryPlan( - PreparedPath originalPath, - IReadOnlyList transitions, - LocalG2OptionsSnapshot options, - out IReadOnlyList regions, - out string reason); -``` - -- `CurvatureTransition` exposes these immutable values: - -```csharp -int SegmentIndex -int LeftCoarsePathIndex -int RightCoarsePathIndex -double LocalArcLengthMeters -double X -double Y -double VehicleHeadingRadians -double LeftVehicleCurvaturePerMeter -double RightVehicleCurvaturePerMeter -double CurvatureJumpPerMeter -``` - -- Define `LocalG2WindowVariant` in the region file with `CandidateIndex`, `StartArcLengthMeters`, `EndArcLengthMeters`, `LeftWindowLengthMeters`, and `RightWindowLengthMeters`. `LocalG2SmoothingRegion` exposes `SegmentIndex`, ordered `Transitions`, the maximum merged range, and ordered `IReadOnlyList WindowVariants`. -- Guarantees: transition arc length is the left/ending point of the old primitive, not the first sampled point inside the new primitive. - -- [ ] **Step 1: Write detection and window tests** - -The new reflection script must cover these scenarios: - -```powershell -# Same direction: 0 -> 0.4167 is detected once. -Assert-Equal 1 $singleTransition.TransitionCount 'One primitive curvature jump must be detected.' -Assert-Near 0.4167 $singleTransition.MaximumJump 0.0001 'The jump magnitude must be retained.' - -# Same pose at a forward/reverse boundary: no event crosses the stop. -Assert-Equal 0 $gearSwitch.TransitionCount 'A stopped gear switch must not be a smoothing event.' - -# A 0.01 1/m numerical change is below max(0.001, 5% of 0.8333). -Assert-Equal 0 $noise.TransitionCount 'Sub-threshold curvature noise must be ignored.' - -# Two 0.50 m windows whose ranges overlap are merged. -Assert-Equal 1 $overlap.RegionCount 'Overlapping windows must form one joint region.' -Assert-Equal 2 $overlap.TransitionCountInFirstRegion 'The merged region must retain both events.' - -# Near a segment start, the window becomes asymmetric without crossing the hard boundary. -Assert-Near 0.0 $nearStart.StartArcLength 0.000000001 'A start window must be clamped to the segment.' -Assert-True ($nearStart.RightWindowLength -gt $nearStart.LeftWindowLength) ` - 'Unavailable left length must be shifted to the right.' -``` - -Expose a deterministic `public static TestHooks.Execute(string scenario)` snapshot on `CurvatureTransitionDetector` so the script does not need to instantiate internal event types directly. - -- [ ] **Step 2: Run and verify RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -``` - -Expected: fail because the Local G2 detector does not exist. - -- [ ] **Step 3: Implement strict option validation** - -`LocalG2OptionsSnapshot` must reject: - -```csharp -minimum <= 0 -preferred < minimum -maximum < preferred -maximumDeviation <= 0 -absoluteJumpFloor <= 0 -jumpRatio <= 0 || jumpRatio > 1 -minimumImprovement <= 0 || minimumImprovement >= 1 -costRegression < 0 -maximumCandidates < 1 -``` - -Copy scalar values into readonly properties. Do not retain `LocalG2QuinticOptions`. - -- [ ] **Step 4: Implement event detection** - -For each `PathSegment`, compare adjacent raw points only within its inclusive index range: - -```csharp -double threshold = Math.Max( - options.AbsoluteCurvatureJumpFloorPerMeter, - options.CurvatureJumpRatioOfMaximum * maximumVehicleCurvaturePerMeter); - -double delta = right.VehicleCurvature - left.VehicleCurvature; -if (Math.Abs(delta) >= threshold) -{ - double segmentStartArc = coarsePath[segment.StartIndex].ArcLength; - transitions.Add(new CurvatureTransition( - segment.SegmentIndex, - leftIndex, - rightIndex, - left.ArcLength - segmentStartArc, - left.X, - left.Y, - left.Heading, - left.VehicleCurvature, - right.VehicleCurvature)); -} -``` - -Never compare `segment.EndIndex` with the next segment start. Validate finite values and nondecreasing arc length. - -- [ ] **Step 5: Implement window variants and merging** - -Generate deterministic total-length targets in this order, removing duplicates after clamping: - -```text -preferred -0.75 × preferred -1.25 × preferred -minimum -maximum -``` - -For each target, start with a 50/50 split. If one side reaches a hard boundary, shift the missing length to the other side without exceeding the target or segment length. Also generate 40/60 and 60/40 variants when both are legal. Stop after `MaximumCandidatesPerRegion`. - -For region discovery, use each event's maximum legal window so that no later candidate can overlap a neighboring region that was treated independently. Sort by segment and start arc; merge ranges that overlap or touch within `1e-9 m`. A merged `LocalG2SmoothingRegion` retains all ordered events and all legal window variants. - -- [ ] **Step 6: Run tests** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -``` - -Expected: both scripts pass. - -- [ ] **Step 7: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -git diff --cached --check -git commit -m "feat: detect local curvature transition regions" -``` - ---- - -### Task 5: Implement the pure quintic Hermite geometry - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1` - -**Interfaces:** - -- Produces: - -```csharp -internal static bool QuinticHermiteCurve2D.TryCreate( - double x0, double y0, double dx0, double dy0, double ddx0, double ddy0, - double x1, double y1, double dx1, double dy1, double ddx1, double ddy1, - out QuinticHermiteCurve2D curve, - out string reason); - -internal void Evaluate( - double u, - out double x, out double y, - out double dx, out double dy, - out double ddx, out double ddy); -``` - -- Guarantees: exact endpoint position, first derivative and second derivative within `1e-10`. - -- [ ] **Step 1: Write the math tests** - -Add scenarios for a straight and a curved boundary: - -```powershell -$straight = Invoke-Curve 0 0 1 0 0 0 2 0 1 0 0 0 -Assert-Near 0.0 $straight.Start.X 1e-12 'Start X must match.' -Assert-Near 2.0 $straight.End.X 1e-12 'End X must match.' -Assert-Near 0.0 $straight.Mid.Y 1e-12 'A straight Hermite curve must remain on the axis.' - -$curved = Invoke-Curve 0 0 1 0 0 0.4 1 1 0 1 -0.4 0 -Assert-Near 0.4 $curved.Start.Curvature 1e-10 'Start curvature must match boundary derivatives.' -Assert-Near 0.4 $curved.End.Curvature 1e-10 'End curvature must match boundary derivatives.' -Assert-True $curved.AllFinite 'All sampled values must be finite.' - -Assert-Rejected (Invoke-InvalidCurve 'ZeroFirstDerivative') ` - 'A curve endpoint with zero first derivative must be rejected.' -``` - -Use curvature: - -```text -(dx * ddy - dy * ddx) / (dx² + dy²)^(3/2) -``` - -- [ ] **Step 2: Run and verify RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 -``` - -Expected: fail because `QuinticHermiteCurve2D` is absent. - -- [ ] **Step 3: Implement the coefficient solver** - -For each coordinate, compute: - -```csharp -a0 = p0; -a1 = v0; -a2 = acceleration0 / 2d; -double c0 = p1 - (a0 + a1 + a2); -double c1 = v1 - (a1 + 2d * a2); -double c2 = acceleration1 - 2d * a2; -a3 = 10d * c0 - 4d * c1 + 0.5d * c2; -a4 = -15d * c0 + 7d * c1 - c2; -a5 = 6d * c0 - 3d * c1 + 0.5d * c2; -``` - -Evaluate with Horner form. Validate `u ∈ [0,1]`, finite inputs and nonzero endpoint derivative norms. The math class must not know about maps, vehicles, SQP or public path contracts. - -- [ ] **Step 4: Run tests** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 -``` - -Expected: `Local G2 quintic curve checks passed.` - -- [ ] **Step 5: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 -git diff --cached --check -git commit -m "feat: add quintic Hermite curve geometry" -``` - ---- - -### Task 6: Build and splice deterministic Local G2 candidates - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - -**Interfaces:** - -- Produces: - -```csharp -internal IReadOnlyList Build( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - LocalG2OptionsSnapshot options, - CancellationToken cancellationToken); -``` - -- Produces: - -```csharp -internal bool LocalG2PathSplicer.TryReplace( - PreparedPath currentPath, - LocalG2CandidateGeometry candidate, - out PreparedPath replacedPath, - out string reason); -``` - -- `LocalG2CandidateGeometry` exposes: - -```csharp -int CandidateIndex -int SegmentIndex -double StartArcLengthMeters -double EndArcLengthMeters -double LeftWindowLengthMeters -double RightWindowLengthMeters -IReadOnlyList RegionPoints -``` - -- Guarantees: candidate endpoints exactly match original window reference; no duplicate non-gear points are introduced. - -- [ ] **Step 1: Add failing candidate tests** - -The script must assert: - -```powershell -Assert-True ($isolated.CandidateCount -gt 0 -and $isolated.CandidateCount -le 12) ` - 'An isolated event must produce a bounded non-empty candidate set.' -Assert-Near 0.0 $isolated.StartPositionError 1e-9 'Window start position must match.' -Assert-Near 0.0 $isolated.EndPositionError 1e-9 'Window end position must match.' -Assert-Near 0.0 $isolated.StartCurvatureError 1e-8 'Window start curvature must match.' -Assert-Near 0.0 $isolated.EndCurvatureError 1e-8 'Window end curvature must match.' -Assert-True $isolated.ContainsLocalG2Source 'Generated samples must identify their source.' - -Assert-Equal 1 $cluster.OutputRegionCount 'Overlapping events must be built as one region.' -Assert-True $cluster.InternalConnectionsAreG2 'Internal anchors must share tangent and curvature.' - -Assert-Equal 'Reverse' $reverse.Direction 'Reverse candidates must preserve segment direction.' -Assert-True $reverse.VehicleAndGeometricCurvatureSignsAreOpposite ` - 'Reverse candidates must convert vehicle curvature to geometric curvature exactly once.' - -Assert-True $spliced.NoDuplicateNonGearPoints 'Splicing must remove duplicate boundary samples.' -Assert-True $spliced.EndpointsUnchanged 'Splicing must preserve the complete segment endpoints.' -``` - -- [ ] **Step 2: Run and verify RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -Expected: fail because candidate construction is absent. - -- [ ] **Step 3: Construct Hermite boundary derivatives** - -For direction sign: - -```csharp -double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; -double travelHeading = segment.Direction == TravelDirection.Forward - ? vehicleHeading - : vehicleHeading - Math.PI; -double geometricCurvature = directionSign * vehicleCurvature; -double tx = Math.Cos(travelHeading); -double ty = Math.Sin(travelHeading); -double nx = -ty; -double ny = tx; -double dx = derivativeScale * tx; -double dy = derivativeScale * ty; -double ddx = derivativeScale * derivativeScale * geometricCurvature * nx; -double ddy = derivativeScale * derivativeScale * geometricCurvature * ny; -``` - -Use one shared position, heading, derivative scale and shared curvature at every internal primitive boundary. The initial shared curvature is the distance-weighted value from the design spec. Sample each curve densely enough that adjacent raw candidate points are no farther than `outputSpacingMeters`. - -If the first event lies at the direction-segment start, do not create a zero-length left Hermite piece. Use `PreparedDirectionSegment.StartVehicleCurvaturePerMeter` as the outer start curvature and construct the one-sided transition directly toward the first stable primitive state. - -- [ ] **Step 4: Generate finite candidate combinations** - -Use window variants from Task 4. For each window, use derivative-scale multipliers in fixed order: - -```text -1.00 -0.85 -1.15 -``` - -The base scale for a curve piece is that piece’s original arc length. Stop at `MaximumCandidatesPerRegion`. Reject a candidate immediately if any curve evaluation is non-finite or its derivative norm falls below `1e-10`. - -- [ ] **Step 5: Implement safe splicing** - -Interpolate exact window endpoints using `PathReferenceInterpolator.TryInterpolateByArcLength`. Copy original points strictly before the start, append candidate region points, then copy original points strictly after the end. Recalculate local prepared arc lengths monotonically from geometry. Preserve `StartsAtGearSwitch` and `EndsAtGearSwitch`. - -For a multi-segment `PreparedPath`, replace only `candidate.SegmentIndex`; copy all other immutable segments. -When reconstructing the replaced `PreparedDirectionSegment`, preserve `StartVehicleCurvaturePerMeter` exactly. - -- [ ] **Step 6: Run tests** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -``` - -Expected: both pass. - -- [ ] **Step 7: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git commit -m "feat: build local G2 smoothing candidates" -``` - ---- - -### Task 7: Evaluate candidates against safety and quality gates - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - -**Interfaces:** - -- Produces: - -```csharp -internal LocalG2CandidateEvaluation Evaluate( - PreparedPath rawPath, - PreparedPath currentPath, - LocalG2SmoothingRegion region, - LocalG2CandidateGeometry candidate, - PathSmoothingRequest request, - LocalG2OptionsSnapshot options, - CancellationToken cancellationToken); -``` - -- Produces: accepted/rejected status, stable failure reason, safe full path, region metrics, maximum deviation and candidate index. -- Produces: - -```csharp -internal static LocalG2CandidateEvaluation SelectBest( - IReadOnlyList evaluations); -``` - -- Define `LocalG2CandidateEvaluation` in the evaluator file with these immutable properties: - -```csharp -bool Accepted -PathSmoothingRegionFailureReason FailureReason -int CandidateIndex -PreparedPath SplicedPreparedPath -IReadOnlyList SafePath -IReadOnlyList SafeSegments -double RawPeakCurvatureDerivativePerSquareMeter -double ResultPeakCurvatureDerivativePerSquareMeter -double RawCurvatureVariationCost -double ResultCurvatureVariationCost -double MaximumDeviationMeters -double MinimumBodyClearanceMeters -double MaximumAbsoluteVehicleCurvaturePerMeter -double AbsolutePathLengthChangeMeters -string Reason -``` - -Rejected evaluations use `SplicedPreparedPath = null` and empty immutable safe-path collections. - -- [ ] **Step 1: Add quality-gate scenarios** - -Extend the candidate script with deterministic scenarios: - -```powershell -Assert-Equal 'DeviationExceeded' $tooFar.FailureReason ` - 'A collision-free candidate more than 0.10 m from the raw window must be rejected.' -Assert-Equal 'CurvatureOvershoot' $overshoot.FailureReason ` - 'A candidate outside the raw regional curvature range must be rejected.' -Assert-Equal 'InsufficientImprovement' $noOp.FailureReason ` - 'A safe no-op must not be accepted.' -Assert-Equal 'VariationCostRegression' $oscillating.FailureReason ` - 'Repeated curvature oscillation must fail the variation-cost gate.' -Assert-Equal 'InsufficientClearance' $lowClearance.FailureReason ` - 'A path with less than 0.02 m checked clearance must be rejected.' -Assert-Equal 'Accepted' $improved.Status ` - 'A safe candidate with at least 20 percent peak improvement must be accepted.' -Assert-Equal $smallestDeviation.CandidateIndex $best.CandidateIndex ` - 'Among sufficient candidates, minimum deviation must win before extra smoothness.' -``` - -- [ ] **Step 2: Run and verify RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -Expected: fail on missing evaluation results. - -- [ ] **Step 3: Implement same-analyzer regional comparison** - -Extract raw and candidate window point lists with exact interpolated endpoints. Analyze both through `PathGeometryAnalyzer` using `configuration.OutputSpacingMeters`. - -Accept quality only when: - -```csharp -candidatePeak <= rawPeak * (1d - options.MinimumPeakGradientImprovementRatio) -candidateCost <= rawCost * (1d + options.MaximumVariationCostRegressionRatio) -``` - -The first comparison is per region. The complete-path variation cost is checked again in Task 8. - -- [ ] **Step 4: Implement hard gates** - -In this order, reject: - -1. non-finite or failed geometry analysis; -2. curvature beyond vehicle maximum; -3. curvature outside the raw regional `[min, max]` plus `1e-6`; -4. maximum candidate-point-to-raw-polyline distance above `MaximumDeviationMeters`; -5. complete-path topology/collision validation failure; -6. checked minimum clearance below `MinimumClearanceReserveMeters`; -7. insufficient peak improvement; -8. variation cost regression. - -Compute point-to-segment distance with a clamped projection; do not compare equal normalized indices. - -- [ ] **Step 5: Implement lexicographic selection** - -Sort accepted evaluations by: - -```text -maximum deviation ascending -peak |dκ/ds| ascending -curvature variation cost ascending -absolute path-length change ascending -candidate index ascending -``` - -Do not combine these into an arbitrary weighted score. - -- [ ] **Step 6: Run focused tests** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_validation.ps1 -``` - -Expected: both pass. - -- [ ] **Step 7: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git commit -m "feat: validate local G2 candidate quality" -``` - ---- - -### Task 8: Integrate the dedicated pipeline and publish partial results - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` -- Consume unchanged: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_service.ps1` - -**Planned imports:** - -```csharp -using System.Collections.ObjectModel; -``` - -**Interfaces:** - -- Consumes: validated request, prepared path and fair raw baseline. -- Snapshots detector-order regions into an immutable read-only `reportOrder` before - calling `LocalG2RegionWorkOrder.TryCreate(...)`; work-order creation does not - mutate this snapshot, and pipeline iteration must not use report-order regions - directly. -- Publishes region reports by indexed iteration over `reportOrder` in the - detector's original ascending order, independently of processing order. -- Produces: - -```csharp -private readonly LocalG2RegionWorkOrder _workOrder = new LocalG2RegionWorkOrder(); - -internal PathSmoothingResult Smooth( - PathSmoothingRequest request, - PreparedPath preparedPath, - RawPathBaseline rawBaseline, - CancellationToken cancellationToken); -``` - -- Guarantees: every valid input publishes one complete safe path; invalid raw input publishes no path. - -- [ ] **Step 1: Add end-to-end fixture assertions** - -Use `SmoothingScenarioFactory.CreateFixtureRequests(...)`, rebuild each public `PathSmoothingRequest` with: - -```powershell -$configuration.Method = [Enum]::Parse($methodType, 'LocalG2Quintic') -$configuration.AllowFallbackToCoarsePath = $true -``` - -Assert: - -```powershell -Straight -> NotNeeded -SingleTurn -> Complete -LargeHeadingChange -> Complete -SBend -> Complete or PartialImprovement -RectangleDetour -> Complete or PartialImprovement -MultiObstacleDetour -> Complete or PartialImprovement -ReverseGearSwitch -> Complete, PartialImprovement, or NotNeeded -``` - -For every published path: - -```powershell -Assert-True ($result.Path.Count -gt 0) 'A valid request must publish a complete path.' -Assert-Equal 0 $result.Segments[0].StartIndex 'Segments must start at zero.' -Assert-Equal ($result.Path.Count - 1) $result.Segments[-1].EndIndex 'Segments must cover the path.' -Assert-True $result.Diagnostics.Metrics.IsFeasible 'Published diagnostics must be feasible.' -Assert-True ($result.Diagnostics.Metrics.MinimumBodyClearanceMeters -ge 0.02) ` - 'Published paths must retain the configured clearance reserve.' -``` - -For `Complete` and `PartialImprovement`, require at least one region report with `Improved`. For `PartialImprovement`, require at least one `RetainedOriginal`. Run every fixture twice and compare status, point count, coordinates and region reports. - -Create one deterministic same-direction fixture with two disjoint detected -regions. Both accepted candidates must change their local replacement length. -Assert: - -1. the larger-original-arc region is evaluated first; -2. the second processed region still matches the intended original front - window endpoints; -3. both reports are `Improved`; -4. reports are published in ascending original arc order; -5. both Local G2 replacements are present in the final full path; -6. two identical requests produce equal status, candidate indices, report - order, point count and point coordinates. - -The test must fail if the pipeline replaces `workRegions` with `regions`. - -Also create two explicit service cases: - -```powershell -# Force every real candidate below the acceptance bar without invalidating the request. -$strict.LocalG2Quintic.MinimumPeakGradientImprovementRatio = 0.99 -$unchanged = $service.Smooth($strictRequest, [Threading.CancellationToken]::None) -Assert-Equal 'Unchanged' $unchanged.Status.ToString() ` - 'Detected events with no accepted region must publish a verified raw path as Unchanged.' -Assert-True ($unchanged.Path.Count -gt 0) 'Unchanged must retain a complete SQP initial path.' - -$cancelSource = [Threading.CancellationTokenSource]::new() -$cancelSource.Cancel() -$cancelled = $service.Smooth($normalRequest, $cancelSource.Token) -Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancellation must propagate through Local G2.' -Assert-Equal 0 $cancelled.Path.Count 'A cancelled run must not publish a partial path.' -``` - -- [ ] **Step 2: Run and verify RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -``` - -Expected: fail because the service cannot resolve `LocalG2Quintic`. - -- [ ] **Step 3: Implement sequential regional processing** - -Pipeline pseudocode must be implemented directly: - -```csharp -IReadOnlyList reportOrder = - new ReadOnlyCollection(new List(regions)); - -if (!_workOrder.TryCreate( - reportOrder, - out IReadOnlyList workRegions, - out string orderReason)) -{ - return PathSmoothingResult.Failure( - PathSmoothingStatus.Failed, - new PathSmoothingDiagnostics( - new PathQualityMetrics(), - TimeSpan.Zero, - 0, - 0d, - orderReason)); -} - -PreparedPath current = preparedPath; -var reportsByRegion = new Dictionary(); -int improvedCount = 0; - -foreach (LocalG2SmoothingRegion region in workRegions) -{ - cancellationToken.ThrowIfCancellationRequested(); - IReadOnlyList candidates = - _builder.Build( - preparedPath.Segments[region.SegmentIndex], - region, - outputSpacing, - options, - cancellationToken); - var evaluations = new List(); - foreach (LocalG2CandidateGeometry candidate in candidates) - { - evaluations.Add( - _evaluator.Evaluate( - preparedPath, - current, - region, - candidate, - request, - options, - cancellationToken)); - } - - LocalG2CandidateEvaluation best = - LocalG2CandidateEvaluator.SelectBest(evaluations); - if (best != null && best.Accepted) - { - current = best.SplicedPreparedPath; - improvedCount++; - reportsByRegion.Add(region, CreateImprovedReport(region, best)); - } - else - { - reportsByRegion.Add( - region, - CreateRetainedReport(region, evaluations)); - } -} - -var reports = new List(reportOrder.Count); -for (int reportIndex = 0; reportIndex < reportOrder.Count; reportIndex++) - reports.Add(reportsByRegion[reportOrder[reportIndex]]); -``` - -Regions are disjoint after merging, so successful earlier replacements remain when a later region fails. - -- [ ] **Step 4: Perform final full-path analysis and rollback** - -Analyze and validate `current`. Require: - -```csharp -finalVariationCost <= rawVariationCost * (1d + options.MaximumVariationCostRegressionRatio) -``` - -If final validation fails, roll back improved regions in reverse acceptance order, marking `GlobalValidationRollback`, until validation passes. If all are rolled back, publish the verified raw baseline as `Unchanged`. If raw baseline was not valid, publish `Failed` with an empty path. - -- [ ] **Step 5: Derive final status exactly** - -```csharp -if (transitions.Count == 0) status = PathSmoothingStatus.NotNeeded; -else if (improvedCount == regions.Count) status = PathSmoothingStatus.Complete; -else if (improvedCount > 0) status = PathSmoothingStatus.PartialImprovement; -else status = PathSmoothingStatus.Unchanged; -``` - -`Complete` and `PartialImprovement` cannot be emitted with zero improved reports. - -- [ ] **Step 6: Dispatch without disturbing legacy methods** - -In `PathSmoothingService`: - -1. validate common inputs; -2. validate only `LocalG2QuinticOptions` when method is `LocalG2Quintic`; -3. prepare and build the fair raw baseline once; -4. branch to `_localG2Pipeline.Smooth(...)`; -5. otherwise continue through the existing `IPathSmoother` runner. - -Do not add Local G2 to `PathSmoothingComparisonRequest.DefaultMethods`. - -- [ ] **Step 7: Run focused and legacy service tests** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_integration.ps1 -``` - -Expected: all scripts pass. The old integration script may continue reporting zero successful legacy methods on difficult fixtures; that is outside this task. - -- [ ] **Step 8: Commit** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_service.ps1 -git diff --cached --check -git commit -m "feat: publish local G2 presmoothing results" -``` - ---- - -### Task 9: Document the main route and run the complete regression suite - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_documentation.ps1` - -**Interfaces:** - -- Documents: module role as SQP warm-start generator. -- Documents: statuses, defaults, output fields and non-goals. -- Does not claim old comparison algorithms are production choices. - -- [ ] **Step 1: Add failing documentation assertions** - -Require README sections or exact terms: - -```powershell -$required = @( - '局部 G2 预平滑', - 'SQP 初始路径', - 'Complete', - 'PartialImprovement', - 'NotNeeded', - 'Unchanged', - '0.20 m', - '0.50 m', - '0.80 m', - '0.10 m', - 'dκ/ds', - '不跨换向点', - '不输出速度' -) -foreach ($text in $required) { - if (-not $readme.Contains($text)) { throw "PathSmoothing README must contain: $text" } -} -``` - -- [ ] **Step 2: Run and verify RED** - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 -``` - -Expected: fail on the first missing Local G2 term. - -- [ ] **Step 3: Update README** - -Document: - -- Hybrid A* is G1 but can have primitive-boundary curvature jumps. -- Local G2 is the main pre-SQP route. -- hard direction segmentation and stopped gear-switch semantics; -- window defaults and maximum deviation; -- fair same-analyzer metrics; -- complete/partial/not-needed/unchanged/failed meanings; -- spatial output fields including `κ` and `dκ/ds`; -- speed planning owns `1 m/s`, `60°/s` and future speed configurations; -- old algorithms remain offline comparison only. - -- [ ] **Step 4: Run all PathSmoothing checks** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -$scripts = @( - 'verify_path_smoothing_contracts.ps1', - 'verify_path_smoothing_algorithm_input.ps1', - 'verify_path_smoothing_geometry.ps1', - 'verify_path_smoothing_validation.ps1', - 'verify_path_smoothing_runner.ps1', - 'verify_path_smoothing_service.ps1', - 'verify_path_smoothing_bspline.ps1', - 'verify_path_smoothing_bezier.ps1', - 'verify_path_smoothing_quintic.ps1', - 'verify_path_smoothing_comparison.ps1', - 'verify_path_smoothing_integration.ps1', - 'verify_path_smoothing_fixtures.ps1', - 'verify_path_smoothing_svg_csv.ps1', - 'verify_path_smoothing_png.ps1', - 'verify_path_smoothing_documentation.ps1', - 'verify_path_smoothing_local_g2_detection.ps1', - 'verify_path_smoothing_local_g2_curve.ps1', - 'verify_path_smoothing_local_g2_candidates.ps1', - 'verify_path_smoothing_local_g2_integration.ps1' -) -foreach ($script in $scripts) { - powershell -ExecutionPolicy Bypass -File (Join-Path 'ClumsyPilot/tests' $script) - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -} -``` - -Expected: build exits `0`; every script prints its success message and exits `0`. - -- [ ] **Step 5: Run CoarsePath regression checks** - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_coarse_path_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_coarse_path_collision.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_coarse_path_search.ps1 -``` - -Expected: all pass. - -- [ ] **Step 6: Inspect fixture outcomes** - -Run: - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -``` - -Record the status, improved-region count, retained-region count, peak `|dκ/ds|`, variation cost, maximum deviation and minimum clearance for every fixture. Do not weaken assertions merely to make a fixture green; if `SingleTurn` or `LargeHeadingChange` is not `Complete`, return to Tasks 4–7 and correct the algorithm. - -- [ ] **Step 7: Commit documentation and final verification changes** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md ` - ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 -git diff --cached --check -git commit -m "docs: describe local G2 presmoothing workflow" -``` - -- [ ] **Step 8: Final repository checks** - -```powershell -git diff --check -git status --short -git log --oneline -10 -``` - -Expected: - -- no whitespace errors; -- no staged files; -- only pre-existing unrelated working-tree changes remain; -- the task commits appear in order. - -Do not report implementation complete unless the full build and every listed verification script have fresh exit code `0`. diff --git a/docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md b/docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md deleted file mode 100644 index e9a96f6..0000000 --- a/docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md +++ /dev/null @@ -1,73 +0,0 @@ -# Path smoothing six-figure report implementation plan - -> **Execution:** Implement in this workspace without staging or committing. The worktree contains unrelated user changes; touch only the path-smoothing report code, its tests, and its documentation. - -**Goal:** Replace each scenario's legacy composite `comparison.svg/png` output with six focused SVG/PNG figures and one CSV, using discrete trajectory samples only (no path-connecting strokes). - -**Architecture:** Keep `SmoothingFigureModel` as the immutable source data extracted from comparison results. Add a figure-set layer that selects series, camera bounds, map decorations, axis configuration, and title/legend per output figure. Both renderers consume that same figure definition, so SVG and PNG communicate exactly the same data. The exporter creates all twelve images and the CSV in temporary sibling files, then publishes the completed set and removes legacy composite images. - -**Technology:** C#/.NET 10 (`System.Drawing.Common` for PNG); hand-authored SVG; existing PowerShell verification harness and `PathSmoothingPngVerificationHost`. - ---- - -## Task 1: Define six figure views from the common report model - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` -- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` - -1. Extend the C# verification host first with assertions for six ordered figure kinds/stems, selected series, equal-scale world bounds, labels with units, and point-only series metadata. Run the host and confirm it fails because no figure set exists. -2. Remove `DashArray` as a trajectory styling contract from `SmoothingFigureSeries` and legend entries. Preserve source points, status, colors, raw baseline flag, violations, map obstacles, start, goal, and metric rows. -3. Implement immutable figure definitions with fixed stems: - - `01-coarse-path-overview`: raw only; map obstacles and start/goal. - - `02-all-paths-comparison`: raw plus all three smoothing methods; paths and axes/legend only. - - `03-cubic-bspline-overview`, `04-local-cubic-bezier-overview`, `05-piecewise-quintic-overview`: faded raw reference plus the named method; map obstacles and start/goal. - - `06-curvature-comparison`: raw plus all smoother curvature samples. -4. Compute a trajectory-driven world view for each overhead figure: union only visible series points plus its relevant start/goal, add 10% padding with a 0.25 m minimum extent, and expand the smaller world dimension so projected X and Y scale are equal. Do not use full map bounds to zoom out a figure. -5. Include deterministic “nice” axis ticks/labels in metres for overhead figures and arc length/curvature units for the final figure. Preserve failed/infeasible method labels in legends even when their geometry has no points. -6. Rerun the host checks; expected result: it passes definition-level checks while renderer-output checks remain to be updated in Tasks 2–3. - -## Task 2: Render six focused point-cloud figures and publish the set - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs` -- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` - -1. Add output-level tests in the verification host for the twelve exact image names, the single CSV, non-empty parseable SVGs, readable 600 dpi PNGs, and absence of temporary files. Run them and confirm the legacy one-image exporter fails these expectations. -2. Refactor the SVG renderer to render one figure definition at a time. Draw axes, ticks, numeric labels, unit labels, legend point swatches, map rectangles (when requested), and start/goal markers. Draw every trajectory and curvature sample as a small marker; do not emit a trajectory ``, polyline, dash array, or line segment. -3. Apply the identical layout semantics in the PNG renderer. Draw points rather than calling a line-drawing API for path samples; give raw reference samples a reduced alpha in individual smoother figures. Keep 600 dpi metadata and the existing required-font behavior. -4. Refactor the exporter to build the six definitions and write twelve temporary image files plus the CSV before publishing. Return collections of SVG and PNG paths with the one CSV path. Delete `comparison.svg/png` after a successful new-set publish; on failure, clean temporary files and retain existing published outputs. -5. Update demo/host call sites from singular `SvgPath`/`PngPath` to the path collections. Run the verification host; expected result: six SVGs, six PNGs, and CSV are all present and valid. - -## Task 3: Update external verification, runner documentation, and visually inspect outputs - -**Files:** -- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_png.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_documentation.ps1` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` -- Modify: `ClumsyPilot/tests/run_path_smoothing_comparison.ps1` (only if it states/assumes legacy filenames) -- Modify: `docs/superpowers/specs/2026-07-30-path-smoothing-six-figure-report-design.md` (only if implementation exposes a necessary clarified contract) - -1. Update PowerShell tests to assert exactly six SVG + six PNG filenames, one CSV, no legacy composite output, required unit labels, marker-based trajectory rendering, and no trajectory dash/line styles. Ensure test source uses safe UTF-8 handling rather than brittle localized literal matching. -2. Update the README to document the six filenames, marker-only semantics, method statuses, coordinate units, and the one-command runner output structure. -3. Run the focused report verification scripts and the PNG host. Regenerate at least one fixture report with the current 0.025 m smoothing output sampling. -4. Render/open representative PNGs for visual QA: raw overview, all-path overlay, each individual smoother, and curvature. Check that curves fill the frame, coordinates/units are legible, all points are visible, individual figures retain context, and there are no joined path lines. -5. Run `dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore` and relevant contract/service/integration tests. Confirm `git diff --check` and report exact files changed; do not stage or commit. - -## Acceptance checklist - -- Each scenario produces exactly `01` through `06` SVGs and corresponding PNGs plus one CSV. -- Raw and smoothed trajectories use every sampled point and zero connecting lines. -- Overhead figures use equal X/Y scale, trajectory-focused bounds, numeric axes, and metre units. -- Curvature uses `s (m)` and `κ (m⁻¹)` axes with a complete legend and statuses. -- SVG and PNG agree on the six figure contents, fonts, units, colors, and point-only semantics. -- Every PNG is 600 dpi; failed export leaves no temporary files or partial newly generated set. diff --git a/docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md b/docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md deleted file mode 100644 index c30985d..0000000 --- a/docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md +++ /dev/null @@ -1,249 +0,0 @@ -# Local G2 Dailywork Reports Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 在 `dailywork_report/` 中交付两份清晰、可追溯的 Local G2 五次 Hermite 中文报告,并为每份提供一个可离线打开的 HTML 可视化附录。 - -**Architecture:** 报告按“算法事实”和“问题证据”拆分,避免把候选层能力误写成已完成的端到端功能。每份 HTML 均为独立单文件,以内嵌 CSS、SVG 和少量原生 JavaScript 将 Markdown 的核心结构可视化;不引入构建工具或外部资源。 - -**Tech Stack:** Markdown、HTML5、内嵌 CSS、内嵌 SVG、原生 JavaScript、PowerShell 验证。 - -## Global Constraints - -- 目录根为 `dailywork_report/`,大小写和下划线必须保持一致。 -- 建立 `Map_rep/` 与 `coarsepath_rep/` 作为空的未来报告入口;本次不填充其业务内容。 -- 本次四份正式内容只放在 `dailywork_report/pathsmoothing_rep/`。 -- 所有文字使用中文;首次出现的英文技术术语必须有中文解释或可由相邻中文短语理解。 -- 明确区分:专项测试通过、已复现故障、静态分析确认的逻辑缺口、待验证集成风险。 -- 不修改任何路径平滑、地图、粗路径或测试实现。 -- HTML 不使用 CDN、网络请求、第三方库、外部图片或构建步骤。 -- 工作区已有无关改动;本任务不暂存、不提交。 - ---- - -### Task 1: 建立稳定的日报目录边界 - -**Files:** -- Create: `dailywork_report/Map_rep/.gitkeep` -- Create: `dailywork_report/coarsepath_rep/.gitkeep` -- Create: `dailywork_report/pathsmoothing_rep/`(由后续两个任务创建内容) - -**Interfaces:** -- Consumes: 已批准的 `docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md`。 -- Produces: 可承载地图、粗路径和路径平滑报告的稳定目录边界。 - -- [ ] **Step 1: 创建两个空模块目录的保留文件** - -使用 `apply_patch` 创建两个空的 `.gitkeep` 文件,内容保持为空: - -```text -dailywork_report/Map_rep/.gitkeep -dailywork_report/coarsepath_rep/.gitkeep -``` - -- [ ] **Step 2: 验证目录边界** - -运行: - -```powershell -$paths = @( - 'dailywork_report/Map_rep/.gitkeep', - 'dailywork_report/coarsepath_rep/.gitkeep' -) -foreach ($path in $paths) { - if (-not (Test-Path -LiteralPath $path)) { throw "Missing report directory marker: $path" } -} -Write-Output 'Dailywork report directory checks passed.' -``` - -预期:输出 `Dailywork report directory checks passed.`。 - ---- - -### Task 2: 编写 Local G2 算法主报告及流程可视化附录 - -**Files:** -- Create: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md` -- Create: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` - -**Interfaces:** -- Consumes: `docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md`、`docs/superpowers/specs/2026-07-30-local-g2-path-presmoothing-design.md`、`ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/` 下的实现。 -- Produces: 一份说明 Local G2 候选层工作方式、输入、输出、约束和当前接入边界的报告;一份与该报告事实一致的可视化附录。 - -- [ ] **Step 1: 写入 Markdown 主报告的固定章节** - -按下列一级标题顺序撰写,并在每节中给出可核对的事实: - -```markdown -# Local G2 五次 Hermite 路径平滑算法说明 -## 1. 目标、位置与非目标 -## 2. 输入:进入算法前必须具备什么 -## 3. 模块架构:每个模块负责什么 -## 4. 数据流:从粗路径到候选安全路径 -## 5. 输出:路径、段、指标、区域报告与状态 -## 6. 安全与质量门 -## 7. 当前实现进度与边界 -``` - -必须写清:输入为成功的 Hybrid A* 粗路径、路径方向段、地图、车辆参数、`LocalG2QuinticOptions`、取消令牌;距离单位为米、航向为弧度、曲率为 `1/m`。数据流必须依次解释预处理、曲率跳变检测、窗口规划、五次 Hermite 候选构造、局部拼接、统一几何分析、完整车体验证、质量评价。输出必须解释 `SmoothedPathPoint`、`SmoothedPathSegment`、曲率 `κ`、曲率导数 `dκ/ds`、区域报告和诊断。 - -“当前实现进度与边界”必须明确:任务 1–7 已达到候选构造与评价层;`LocalG2PreSmoothingPipeline` 与服务分派尚未实现,因此不能声称目前可正式发布完整的 Local G2 SQP 初始路径。 - -- [ ] **Step 2: 写入单文件 HTML 算法附录** - -HTML 必须包含 `
`、一个“输入”卡片区、一个按顺序排列的 SVG 流程图、一个“输出”卡片区和一个“当前边界”提示区。SVG 流程节点必须使用以下稳定文字: - -```text -Hybrid A* 粗路径 -预处理与方向分段 -曲率跳变检测 -窗口规划 -五次 Hermite 候选 -拼接与统一几何分析 -完整车体安全与质量评价 -计划中的发布流水线(尚未接入) -``` - -用绿色标记已实现的候选层节点,用琥珀色标记“尚未接入”的发布流水线节点。HTML 中的“输入”和“输出”文字必须与 Markdown 报告一致,且页面顶部必须写明“离线静态可视化附录”。 - -- [ ] **Step 3: 校验主报告与 HTML 的算法事实** - -运行: - -```powershell -$markdown = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md' -$html = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html' -$markdownTerms = @('输入:进入算法前必须具备什么', '输出:路径、段、指标、区域报告与状态', 'dκ/ds', '尚未实现') -$htmlTerms = @(']+src=') { throw 'HTML appendices must be self-contained.' } -Write-Output 'Dailywork report package checks passed.' -``` - -预期:输出 `Dailywork report package checks passed.`。 - -- [ ] **Step 2: 在本地浏览器进行人工可读性审阅** - -依次打开两个 HTML 文件,检查以下具体条件: - -```text -算法附录:流程从左到右或从上到下可顺序阅读;绿色已实现节点与琥珀色未接入节点容易区分;输入和输出没有被流程图遮挡。 -问题附录:三类证据徽章颜色和文字均可分辨;三个问题卡片的例子、成因、措施没有被截断;行动顺序位于页面末尾且与 Markdown 一致。 -``` - -- [ ] **Step 3: 检查工作区改动范围** - -运行: - -```powershell -git diff --check -git status --short -- dailywork_report docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md -``` - -预期:无空白错误;改动只包含本计划的设计、计划和日报交付文件。 diff --git a/docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md b/docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md deleted file mode 100644 index 2fd0de6..0000000 --- a/docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md +++ /dev/null @@ -1,404 +0,0 @@ -# Local G2 Interactive Visualization Redesign Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 将两份 Local G2 HTML 报告附录从流程卡片重做为可逐步查看路径变化、并能对照错误与正确预期的离线交互式可视化。 - -**Architecture:** 两页均继续为独立 HTML 文件。算法页使用一个固定坐标系的 SVG 和六个可切换状态,曲线由内嵌 JavaScript 的五次 Hermite 基函数计算并绘制;问题页使用三个可切换的 SVG 场景,每个场景同时呈现“实际发生 / 正确应有 / 差异原因”。所有教学几何固定标为典型示例,真实测试结论只以已知状态与原始文本呈现。 - -**Tech Stack:** HTML5、CSS、内嵌 SVG、原生 JavaScript、PowerShell 静态验证。 - -## Global Constraints - -- 仅在 `dailywork_report/pathsmoothing_rep/` 下两个指定 HTML 路径重建内容;它们在本任务基线提交中尚未跟踪,因此 Git 可将首次纳入版本控制的重建页面显示为新增文件。不修改路径平滑算法、测试、地图、粗路径或两份 Markdown 报告的事实内容。 -- 不使用任何外部资源:不使用 CDN、网络请求、外部图片、外部字体、第三方库或构建步骤。 -- 算法页必须具备六个可访问步骤:粗路径、局部窗口、五次 Hermite、局部替换、曲率—弧长、安全与质量门。 -- 问题页必须具备三个可访问问题场景;每个场景同时可见“实际发生”“正确应有”“差异原因”。 -- 所有典型坐标、曲率图形和车辆示意均必须明确标注为机制解释,不得暗示为项目运行时实测结果。 -- `RectangleDetour` 只陈述已复现的 `Success → InvalidInput`;失败点仍标记“待定位”,不得画出伪造的具体坏样本或根因。 -- 用户已授权整体删除并从头重建两份旧 HTML;工作区存在无关改动,每个任务的提交只能包含其对应的一份 HTML,不得带入其他文件。 - -## File Structure - -| 文件 | 职责 | -|---|---| -| `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` | 六步 Local G2 路径几何演示、Hermite 曲线生成、键盘/按钮步骤导航与安全质量门示意。 | -| `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` | 三个问题的实际/正确对照、场景切换与证据边界标注。 | - ---- - -### Task 1: 重做算法页为六步路径几何演示 - -**Files:** -- Modify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` - -**Interfaces:** -- Consumes: 无运行时数据;仅使用固定、标为典型示例的二维点和已确认的 G2 术语/门限。 -- Produces: `setStep(index)`、`buildQuinticPath()`、`stepButtons`、`#algorithm-diagram`、`#step-title`、`#step-description`、`#step-status`、`#prev-step` 与 `#next-step`,供 HTML 初次渲染、按钮和左右方向键共用。 - -- [ ] **Step 1: 先运行会失败的结构验证** - -Run: - -```powershell -$file = 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html' -$html = Get-Content -Raw -Encoding UTF8 $file -$required = @('id="algorithm-diagram"', 'data-step="0"', 'data-step="5"', 'id="prev-step"', 'id="next-step"', 'function buildQuinticPath', 'function setStep') -$missing = @($required | Where-Object { -not $html.Contains($_) }) -if ($missing.Count -gt 0) { throw "Algorithm visual contract missing: $($missing -join ', ')" } -``` - -Expected: FAIL because the current static flow page has no interactive step contract. - -- [ ] **Step 2: 整体删除旧卡片页面并从头重建,让主 SVG 成为视觉中心** - -Use `apply_patch` to整体删除当前文件内容并添加一个响应式新文档,结构如下: - -```html -
-
…候选层已实现、正式发布流水线尚未接入…
-

典型示例:用于解释机制,不代表某次测试的精确坐标。

- -
-

- - Local G2 五次 Hermite 局部路径平滑步骤 - 典型粗路径在局部曲率跳变处被五次 Hermite 曲线安全替换的六步示意。 - - -
-
-
-``` - -CSS requirements: - -- `svg { width: 100%; height: auto; }`,不再使用 `min-width` 和横向滚动容器;窄屏按 `viewBox` 等比缩放。 -- 用同一组语义颜色稳定表达:灰色原始粗路径、蓝色窗口/约束、绿色已接受候选、琥珀色发布边界、红色拒绝或阻断;同时配合实线/虚线、文字和符号。 -- `.scene-layer` 默认淡出,`[data-step="N"] .scene-N` 显示;`prefers-reduced-motion: reduce` 时禁用转场。 -- 不设置固定视口高度、不设置内部滚动,并保证按钮触摸目标和焦点状态清晰。 - -- [ ] **Step 3: 用真实五次 Hermite 基函数绘制典型候选曲线** - -In the page script, define the fixed example endpoints and use the six quintic Hermite basis functions—not an SVG cubic Bézier substitute—to sample the visual candidate: - -```javascript -const hermite = { - p0: { x: 290, y: 462 }, p1: { x: 690, y: 258 }, - d0: { x: 180, y: 0 }, d1: { x: 210, y: -135 }, - a0: { x: 0, y: -18 }, a1: { x: 22, y: -12 } -}; - -function quinticBasis(t) { - const t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t; - return [ - 1 - 10 * t3 + 15 * t4 - 6 * t5, - t - 6 * t3 + 8 * t4 - 3 * t5, - 0.5 * (t2 - 3 * t3 + 3 * t4 - t5), - 10 * t3 - 15 * t4 + 6 * t5, - -4 * t3 + 7 * t4 - 3 * t5, - 0.5 * (t3 - 2 * t4 + t5) - ]; -} - -function buildQuinticPath() { - const points = []; - for (let i = 0; i <= 48; i += 1) { - const [h00, h10, h20, h01, h11, h21] = quinticBasis(i / 48); - points.push({ - x: h00 * hermite.p0.x + h10 * hermite.d0.x + h20 * hermite.a0.x + h01 * hermite.p1.x + h11 * hermite.d1.x + h21 * hermite.a1.x, - y: h00 * hermite.p0.y + h10 * hermite.d0.y + h20 * hermite.a0.y + h01 * hermite.p1.y + h11 * hermite.d1.y + h21 * hermite.a1.y - }); - } - return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' '); -} -``` - -Set the generated string on `#quintic-candidate`. Draw a separate raw polyline that shares the same window endpoints but has a visible heading/curvature break in its interior. Add persistent labels for start, end, direction, window boundary and `κ` jump; do not attach real-world units or claim these fixed coordinates are measured data. - -- [ ] **Step 4: 实现六个可读状态的 SVG 图层** - -Create six SVG groups, each carrying both `scene-layer` and `scene-0` through `scene-5` as appropriate. They must communicate these exact visual effects: - -```text -scene-0: 原始离散点、方向箭头、突变点,候选曲线隐藏。 -scene-1: 左右窗口边界和淡蓝色局部带高亮,其余粗路径降低不透明度。 -scene-2: 两端切向箭头、二阶趋势弧线、虚线 quintic-candidate 可见。 -scene-3: 灰色原折线与绿色候选曲线叠加,接缝用“替换开始/结束”标记。 -scene-4: 上方替换后路径;下方 κ—s 趋势示意显示原始跳变与候选连续过渡,并标“趋势示意,非实测数据”。 -scene-5: 三个车辆轮廓沿候选曲线放置;净空带、通过标记和“碰撞 / 净空 / 曲率 / 偏移”四个质量门可见。 -``` - -Use ``, ``, ``, ``, `` and simple `` vehicle rectangles; do not use raster images. Put the existing “正式发布流水线尚未接入”的事实边界 below the visual, outside the six state layers. - -- [ ] **Step 5: 接入状态更新与键盘操作** - -Use one state function and no inline event handlers: - -```javascript -const steps = [ - ['0 / 5', '原始 Hybrid A* 粗路径', '可行离散路径在局部接口处仍可能有曲率跳变。'], - ['1 / 5', '检测并框定局部窗口', '只处理跳变附近,不重新搜索整条路径。'], - ['2 / 5', '由端点约束构造五次 Hermite 候选', '位置、切向和曲率趋势共同确定局部曲线。'], - ['3 / 5', '替换窗口内部的原始几何', '窗口外路径保持不变,接缝需要连续。'], - ['4 / 5', '观察曲率—弧长连续性', '目标是消除接口处的趋势跳变,而不是只让外形更圆。'], - ['5 / 5', '经安全与质量门决定接受或回退', '候选必须同时满足车体安全、净空、曲率和偏移约束。'] -]; - -function setStep(index) { - const next = Math.max(0, Math.min(steps.length - 1, index)); - const [status, title, description] = steps[next]; - document.getElementById('local-g2-algorithm-demo').dataset.step = String(next); - document.getElementById('step-status').textContent = `步骤 ${status}`; - document.getElementById('step-title').textContent = title; - document.getElementById('step-description').textContent = description; - stepButtons.forEach((button) => button.setAttribute('aria-pressed', String(Number(button.dataset.step) === next))); - document.getElementById('prev-step').disabled = next === 0; - document.getElementById('next-step').disabled = next === steps.length - 1; -} -``` - -Declare `stepButtons` before `setStep`, register each button, register `#prev-step`/`#next-step`, handle only unmodified `ArrowLeft` and `ArrowRight` key presses, then call `setStep(0)`. Do not override keyboard interaction when the event target is a form control. - -- [ ] **Step 6: 重新运行算法页验证,确认由失败转为通过** - -Run the Step 1 command again, then run: - -```powershell -$file = 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html' -$html = Get-Content -Raw -Encoding UTF8 $file -$terms = @('典型示例:用于解释机制', '曲率—弧长', '趋势示意,非实测数据', '完整车体安全', '正式发布流水线尚未接入') -$missing = @($terms | Where-Object { -not $html.Contains($_) }) -if ($missing.Count -gt 0) { throw "Algorithm visual content missing: $($missing -join ', ')" } -Write-Output 'Algorithm interactive visualization checks passed.' -``` - -Expected: `Algorithm interactive visualization checks passed.` - ---- - -### Task 2: 重做问题页为错误与正确预期的几何对照 - -**Files:** -- Modify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` - -**Interfaces:** -- Consumes: 已确认的三类证据、`RectangleDetour` 端到端输出、窗口约束数值和弧长陈旧风险的事实边界。 -- Produces: `selectIssue(issueId)`、`issueButtons`、`#issue-visual`、`#issue-evidence`、`#issue-title`、`#issue-actual`、`#issue-expected`、`#issue-cause`、`#issue-action`、`#prev-issue` 与 `#next-issue`。 - -- [ ] **Step 1: 先运行会失败的结构验证** - -Run: - -```powershell -$file = 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' -$html = Get-Content -Raw -Encoding UTF8 $file -$required = @('id="issue-visual"', 'data-issue="baseline"', 'data-issue="window"', 'data-issue="arclength"', 'function selectIssue', 'id="issue-actual"', 'id="issue-expected"', 'id="issue-cause"') -$missing = @($required | Where-Object { -not $html.Contains($_) }) -if ($missing.Count -gt 0) { throw "Issue visual contract missing: $($missing -join ', ')" } -``` - -Expected: FAIL because the current page has only static issue cards and one causal flow diagram. - -- [ ] **Step 2: 整体删除旧问题卡片页面并构建共享选择器与三栏事实说明** - -Use `apply_patch` to整体删除当前文件内容并添加一个新文档,结构如下: - -```html -
-
…三种证据等级…
- -
-

- -
-

实际发生

-

正确应有

-

差异原因

-
-

下一步:

-
-
-
-``` - -Use a single shared scale and distinct, labelled SVG lanes rather than three textual cards. Keep the priority/action order in a compact section below the interactive visual, not above it. - -- [ ] **Step 3: 画出三个“实际 / 正确”对照场景,并保持证据边界** - -Create `.issue-scene` SVG groups and make the selected group visible using `[data-issue="…"]` CSS. Each scene must implement these marks: - -```text -baseline: - - 上方:概念性 RectangleDetour 绕障路径、障碍物、灰色原始路径;红色问号标“首次失败样本待定位”。 - - 中间:实际链路 Hybrid A* Success → 原始基线复验 InvalidInput ⛔ → G2 候选未开始。 - - 下方:正确链路 Hybrid A* Success → 原始基线复验 Success → G2 候选评价。 - - 固定脚注:概念性几何,不代表尚未定位的实际坏样本。 - -window: - - 上方:同一条典型路径上的事件 A/B 和弧长标尺,中心距离直接标为 1.0 m。 - - 中间左侧“实际”:两个 ±0.8 m 影响范围重叠后合并,合并总区间标“> 0.8 m”,红色叉号和“无候选”。 - - 中间右侧“正确”:可行的拆分窗口或一致的长度策略,绿色窗口 A/B 和“可评价候选”。 - - 下方:明确标“静态分析确认的逻辑缺口”。 - -arclength: - - 上方:替换前路径的 A、B 两个局部窗口和原始弧长标尺。 - - 下方左侧“实际风险”:A 替换后路径长度改变,B-old 仍按旧弧长落在偏早位置;用虚线箭头表达旧映射。 - - 下方右侧“正确”:重算/稳定锚点后 B-new 落在预期局部;用实线箭头表达新映射。 - - 固定脚注:待正式多区域流水线接入后通过回归测试验证。 -``` - -Do not draw a red collision marker or concrete bad curvature sample in `baseline`; only the question marker is allowed there. Pair every colored status with text (`实际`, `正确`, `待定位`, `无候选`, `重定位`) and a different line style or marker. - -- [ ] **Step 4: 接入问题状态数据、导航和可访问性** - -In the page script, use immutable descriptive data and one update function: - -```javascript -const issues = { - baseline: { - evidence: '已复现故障 · RectangleDetour', - title: '原始基线在 Local G2 开始前被拒绝', - actual: 'Hybrid A* 粗路径规划成功,但原始基线统一复验返回 InvalidInput;候选生成没有开始。', - expected: '同一条成功规划的粗路径应先作为可行基线通过统一复验,再进入候选评价。', - cause: '首次非法数值或超限曲率样本尚未定位;不能把该失败归因于 G2 候选。', - action: '记录首次异常的方向段、样本、曲率、净空和验证结果,在不放松安全门的前提下定位源头。' - }, - window: { - evidence: '静态分析确认的逻辑缺口', - title: '合并范围比可用候选窗口更宽', - actual: '相距 1.0 m 的事件被 ±0.8 m 范围合并,但候选总长度不能超过 0.8 m,结果没有候选。', - expected: '窗口合并和最大总长度应采用一致语义,或在不满足时拆分为可行局部窗口。', - cause: '影响范围的合并规则与候选总长度约束没有共同的可行性判断。', - action: '加入 1.0 m 间距回归,统一窗口长度语义并验证候选仍可生成。' - }, - arclength: { - evidence: '待验证集成风险', - title: '区域 A 替换后,区域 B 的弧长坐标可能陈旧', - actual: 'A 拼接并重算弧长后,B 若仍使用替换前坐标,可能指向错误局部。', - expected: '处理 B 前应按当前路径重定位,或由稳定锚点映射恢复其原始语义位置。', - cause: '候选记录的原始弧长与每次拼接后重新计算的当前弧长处于不同坐标系。', - action: '构造 A 改变长度、B 仍准确定位的双区域回归,再接入正式流水线。' - } -}; - -function selectIssue(issueId) { - const issue = issues[issueId]; - if (!issue) return; - document.getElementById('local-g2-issue-demo').dataset.issue = issueId; - document.getElementById('issue-evidence').textContent = issue.evidence; - document.getElementById('issue-title').textContent = issue.title; - document.getElementById('issue-actual').textContent = issue.actual; - document.getElementById('issue-expected').textContent = issue.expected; - document.getElementById('issue-cause').textContent = issue.cause; - document.getElementById('issue-action').textContent = issue.action; - issueButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.issue === issueId))); -} -``` - -Declare an ordered `issueIds = ['baseline', 'window', 'arclength']`, implement previous/next by index, register button clicks, then call `selectIssue('baseline')`. Make the initial baseline state useful without JavaScript by placing its copy in the HTML before the script runs, then allow JavaScript to overwrite it with the same factually equivalent text. - -- [ ] **Step 5: 重新运行问题页验证,确认由失败转为通过** - -Run the Step 1 command again, then run: - -```powershell -$file = 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' -$html = Get-Content -Raw -Encoding UTF8 $file -$terms = @('Success → InvalidInput', '首次失败样本待定位', '1.0 m', '±0.8 m', '> 0.8 m', '弧长重算', '实际发生', '正确应有', '差异原因') -$missing = @($terms | Where-Object { -not $html.Contains($_) }) -if ($missing.Count -gt 0) { throw "Issue visual content missing: $($missing -join ', ')" } -Write-Output 'Issue interactive visualization checks passed.' -``` - -Expected: `Issue interactive visualization checks passed.` - ---- - -### Task 3: 离线完整性、交互契约和可视化验收 - -**Files:** -- Verify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` -- Verify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` - -**Interfaces:** -- Consumes: 两页已实现的 DOM id、数据属性、函数名和离线资源限制。 -- Produces: 可复查的 PowerShell 验证输出,以及在浏览器可用时的人工交互验收结论。 - -- [ ] **Step 1: 验证离线性、文件结构和静态交互契约** - -Run: - -```powershell -$files = @( - 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html', - 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' -) -$contracts = @{ - $files[0] = @('', '', 'function buildQuinticPath', 'function setStep', 'id="algorithm-diagram"', 'data-step="5"') - $files[1] = @('', '', 'function selectIssue', 'id="issue-visual"', 'data-issue="arclength"', 'id="issue-cause"') -} -foreach ($file in $files) { - $html = Get-Content -Raw -Encoding UTF8 $file - $missing = @($contracts[$file] | Where-Object { -not $html.Contains($_) }) - if ($missing.Count -gt 0) { throw "$file missing: $($missing -join ', ')" } - if ($html -match 'https?://|]+\bsrc\s*=|]+\bsrc\s*=') { throw "$file must not depend on external resources." } - if (($html -split ' **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 修复进入任务 8 前的曲率基线误判、局部窗口总长度错误和多区域弧长错位风险,并用失败先行回归证明安全边界没有被削弱。 - -**Architecture:** `PathGeometryAnalyzer` 首先通过受认证的圆弧—弦长估计器消除极限圆弧的离散高估;若认证失败,或原始基线的共享重建因非有限/超限曲率被未改动验证器拒绝而可信粗路径曲率仍有限且在车辆上限内,只让 `RawPathBaselineBuilder` 使用粗路径自带曲率和弧长,并重新通过完整验证。`LocalG2WindowPlanner` 以“是否能生成覆盖全部事件的合法候选”决定分组,新的 `LocalG2RegionWorkOrder` 只负责把报告顺序转换为同方向段从后向前的工作顺序。 - -**Tech Stack:** C# 10、.NET Standard 2.0、PowerShell 反射测试、现有 `dotnet build` 与 `verify_path_smoothing_*.ps1` 回归脚本。 - -## Global Constraints - -- `MinimumWindowLengthMeters = 0.20`、`PreferredWindowLengthMeters = 0.50`、`MaximumWindowLengthMeters = 0.80` 都表示左窗口加右窗口的总长度。 -- 不修改车辆最大曲率、`SmoothedPathValidator` 的 `1e-6` 容差、碰撞门限、净空门限或候选质量门。 -- 不新增 RectangleDetour 碰撞位置、最小净空或可视化诊断作为本阶段验收内容。 -- 不创建 `LocalG2PreSmoothingPipeline.cs`,不修改 `PathSmoothingService` 分派,不把 `LocalG2Quintic` 接入正式服务。 -- 工作顺序为方向段编号升序、同一方向段原始局部弧长降序;报告顺序仍为检测器的原始升序。 -- 候选从不可变原始路径构造;任务 8 才能把评价和拼接接到当前路径并完成端到端证明。 -- 采用 TDD:每项行为先看到预期失败,再写最小实现;每个任务只暂存其列出的文件,保留工作区其他修改。 - ---- - -## File Structure - -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs` - - 共享几何重采样、航向展开、圆弧—弦长离散曲率和曲率导数统计。 -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs` - - 继续使用共享分析器;仅在认证失败,或原始基线重建因非有限/超限曲率被验证器拒绝而可信粗路径曲率仍在车辆上限内时,承载粗路径专用限定回退。 -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` - - 生成总长度受限的窗口变体,并以变体可生成性决定事件分组和区域包络。 -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs` - - 保留区域与窗口不可变数据合同,不新增第二套长度定义。 -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs` - - 新建;验证区域并生成确定性工作顺序,不改变输入报告顺序。 -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` - - 只扩展现有窄范围 `TestHooks`,承载双区域拼接证明,不改变候选构造主逻辑。 -- `ClumsyPilot/tests/verify_path_smoothing_geometry.ps1` - - 极限/超限圆弧、端点、倒车、分支切换和解析五次曲线认证。 -- `ClumsyPilot/tests/verify_path_smoothing_integration.ps1` - - 保持 RectangleDetour 原始基线和全场景集成验收。 -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1` - - `0.80 m` 总窗口、分组、边界偏置、包络和确定性回归。 -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - - 区域工作顺序和双窗口拼接回归。 -- `docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md` - - 更新任务 8,使其显式消费工作顺序组件并包含真实双区域硬门。 - ---- - -### Task 1: Certify and Fix the Shared Curvature Estimator - -**Files:** - -- Modify: `ClumsyPilot/tests/verify_path_smoothing_geometry.ps1` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs` -- Verify unchanged: `ClumsyPilot/tests/verify_path_smoothing_validation.ps1` -- Verify unchanged: `ClumsyPilot/tests/verify_path_smoothing_integration.ps1` -- Conditional modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs` - -**Interfaces:** - -- Consumes: `PreparedDirectionSegment.Points`,连续展开后的车辆航向、重采样点位置和 `TravelDirection`。 -- Produces: 保持现有公开签名不变: - -```csharp -public bool TryAnalyze( - IReadOnlyList candidateSegments, - double spacingMeters, - out PathGeometryAnalysis analysis, - out string reason); -``` - -- Internal rule: for sample pair `(left, right)`, - -```text -deltaHeading = unwrappedHeading[right] - unwrappedHeading[left] -chordLength = distance(position[left], position[right]) -kappa = 2 * sin(deltaHeading / 2) / chordLength -``` - -- Safety gate: `abs(deltaHeading) >= π`、非有限弦长或退化弦长必须稳定失败。 - -- [ ] **Step 1: Add exact-circle and analytic-reference helpers to the geometry script** - -Insert these complete helpers after `Invoke-Analysis` in -`ClumsyPilot/tests/verify_path_smoothing_geometry.ps1`: - -```powershell -function Normalize-Angle([double]$Angle) { - return [Math]::Atan2([Math]::Sin($Angle), [Math]::Cos($Angle)) -} - -function New-ConstantCurvatureSegment( - [double]$VehicleCurvature, - $Direction, - [int]$IntervalCount = 20, - [double]$ChordLength = 0.05) { - $radius = 1.0 / [Math]::Abs($VehicleCurvature) - $geometricSign = if ($Direction.ToString() -eq 'Forward') { - [Math]::Sign($VehicleCurvature) - } else { - -[Math]::Sign($VehicleCurvature) - } - $deltaTheta = $geometricSign * 2.0 * [Math]::Asin($ChordLength / (2.0 * $radius)) - $points = New-Object System.Collections.Generic.List[object] - for ($index = 0; $index -le $IntervalCount; $index++) { - $theta = $index * $deltaTheta - $x = 2.0 + $geometricSign * $radius * [Math]::Sin($theta) - $y = 2.0 + $geometricSign * $radius * (1.0 - [Math]::Cos($theta)) - $travelHeading = $theta - $vehicleHeading = if ($Direction.ToString() -eq 'Forward') { - $travelHeading - } else { - $travelHeading + [Math]::PI - } - [void]$points.Add((New-GeometryPoint ` - $x $y ($index * $ChordLength) (Normalize-Angle $vehicleHeading) $vehicleHeading)) - } - return New-DirectionSegment 0 $Direction $points.ToArray() -} - -function Get-LegacyMaximumCurvature($Analysis) { - $maximum = 0.0 - foreach ($segment in $Analysis.Segments) { - for ($index = $segment.StartIndex; $index -le $segment.EndIndex; $index++) { - $leftIndex = if ($index -eq $segment.StartIndex) { $index } else { $index - 1 } - $rightIndex = if ($index -eq $segment.EndIndex) { $index } else { $index + 1 } - if ($leftIndex -eq $rightIndex) { continue } - $arc = 0.0 - for ($pointIndex = $leftIndex + 1; $pointIndex -le $rightIndex; $pointIndex++) { - $left = $Analysis.Path[$pointIndex - 1] - $right = $Analysis.Path[$pointIndex] - $dx = $right.X - $left.X - $dy = $right.Y - $left.Y - $arc += [Math]::Sqrt($dx * $dx + $dy * $dy) - } - $delta = $Analysis.Path[$rightIndex].UnwrappedHeading - - $Analysis.Path[$leftIndex].UnwrappedHeading - $maximum = [Math]::Max($maximum, [Math]::Abs($delta / $arc)) - } - } - return $maximum -} - -function New-AnalyticQuinticSegment( - [double[]]$Coefficients, - [double]$DistributionPower) { - $points = New-Object System.Collections.Generic.List[object] - for ($index = 0; $index -le 400; $index++) { - $u = $index / 400.0 - $t = [Math]::Pow($u, $DistributionPower) - $c2 = $Coefficients[0] - $c3 = $Coefficients[1] - $c4 = $Coefficients[2] - $c5 = $Coefficients[3] - $y = $c2 * $t * $t + $c3 * [Math]::Pow($t, 3) + - $c4 * [Math]::Pow($t, 4) + $c5 * [Math]::Pow($t, 5) - $dy = 2.0 * $c2 * $t + 3.0 * $c3 * $t * $t + - 4.0 * $c4 * [Math]::Pow($t, 3) + 5.0 * $c5 * [Math]::Pow($t, 4) - $heading = [Math]::Atan2($dy, 1.0) - [void]$points.Add((New-GeometryPoint (1.0 + $t) (1.0 + $y) $t $heading $heading)) - } - return New-DirectionSegment 0 $forward $points.ToArray() -} - -function Get-AnalyticQuinticMaximum( - [double[]]$Coefficients) { - $maximum = 0.0 - for ($index = 0; $index -le 20000; $index++) { - $t = $index / 20000.0 - $c2 = $Coefficients[0] - $c3 = $Coefficients[1] - $c4 = $Coefficients[2] - $c5 = $Coefficients[3] - $dy = 2.0 * $c2 * $t + 3.0 * $c3 * $t * $t + - 4.0 * $c4 * [Math]::Pow($t, 3) + 5.0 * $c5 * [Math]::Pow($t, 4) - $ddy = 2.0 * $c2 + 6.0 * $c3 * $t + - 12.0 * $c4 * $t * $t + 20.0 * $c5 * [Math]::Pow($t, 3) - $curvature = [Math]::Abs($ddy / [Math]::Pow(1.0 + $dy * $dy, 1.5)) - $maximum = [Math]::Max($maximum, $curvature) - } - return $maximum -} - -function New-PreparedPathFromSegment($Segment) { - $segments = [Array]::CreateInstance($segmentType, 1) - $segments.SetValue($Segment, 0) - return [Activator]::CreateInstance( - $preparedPathType, - [object[]]@(,$segments)) -} - -function Invoke-AnalyzedValidation($Analysis, $Original, $Vehicle) { - $arguments = [object[]]@( - $Analysis.Path, - $Analysis.Segments, - $Original, - (New-EmptyGeometryMap), - $Vehicle, - [double]0.05, - $null, - [double]0.0, - $null) - $accepted = $validateMethod.Invoke($validator, $arguments) - return [PSCustomObject]@{ - Accepted = $accepted - Reason = $arguments[8] - } -} -``` - -- [ ] **Step 2: Add the failing certification assertions** - -Add these declarations beside the existing reflected geometry types and -analyzer setup: - -```powershell -$validatorType = Get-RequiredType ( - $root + 'Validation.SmoothedPathValidator') -$validator = [Activator]::CreateInstance($validatorType) -$validateMethod = $validatorType.GetMethod('TryValidate') -Assert-True ($null -ne $validateMethod) ` - 'The curvature certification must use the existing full path validator.' -``` - -Insert this block after the existing forward/reverse circle assertions: - -```powershell -$maximumAllowedCurvature = 5.0 / 6.0 -$certificationVehicle = [Activator]::CreateInstance($vehicleType) -$certificationVehicle.LengthMeters = 0.20 -$certificationVehicle.WidthMeters = 0.20 -$certificationVehicle.SafetyMarginMeters = 0.0 -$certificationVehicle.MaximumCurvaturePerMeter = $maximumAllowedCurvature -foreach ($direction in @($forward, $reverse)) { - $signedCurvature = if ($direction.ToString() -eq 'Forward') { - $maximumAllowedCurvature - } else { - -$maximumAllowedCurvature - } - $limitSegment = New-ConstantCurvatureSegment $signedCurvature $direction - $limitAnalysis = Invoke-Analysis @($limitSegment) 0.05 - foreach ($point in $limitAnalysis.Path) { - Assert-True ([Math]::Abs($point.VehicleCurvature) -le $maximumAllowedCurvature + 1e-9) ` - 'Every endpoint and interior sample on an exact-limit circle must remain within the limit.' - } - $limitValidation = Invoke-AnalyzedValidation ` - $limitAnalysis ` - (New-PreparedPathFromSegment $limitSegment) ` - $certificationVehicle - Assert-True $limitValidation.Accepted ` - ('The existing validator must accept an exact-limit circle. Reason=' + $limitValidation.Reason) -} - -$quinticCases = @( - [PSCustomObject]@{ Name = 'SBend'; Coefficients = [double[]]@(0.0, 0.30, -0.45, 0.18); Power = 1.0 }, - [PSCustomObject]@{ Name = 'EndpointPeak'; Coefficients = [double[]]@(0.18, -0.12, 0.0, 0.0); Power = 1.0 }, - [PSCustomObject]@{ Name = 'NonUniformFinalInterval'; Coefficients = [double[]]@(-0.12, 0.36, -0.30, 0.08); Power = 1.7 } -) -foreach ($case in $quinticCases) { - $analysis = Invoke-Analysis @( - (New-AnalyticQuinticSegment $case.Coefficients $case.Power) - ) 0.013 - $analyticMaximum = Get-AnalyticQuinticMaximum $case.Coefficients - $legacyMaximum = Get-LegacyMaximumCurvature $analysis - $newUnderestimate = [Math]::Max( - 0.0, $analyticMaximum - $analysis.MaximumAbsoluteVehicleCurvaturePerMeter) - $legacyUnderestimate = [Math]::Max(0.0, $analyticMaximum - $legacyMaximum) - Assert-True ($newUnderestimate -le $legacyUnderestimate + 1e-6) ` - "$($case.Name) must not increase one-sided maximum-curvature underestimation." -} - -$ambiguousTurn = New-DirectionSegment 0 $forward @( - (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), - (New-GeometryPoint 1.0 0.0 1.0 ([Math]::PI) ([Math]::PI))) -Invoke-RejectedAnalysis @($ambiguousTurn) ` - 'A curvature sample spanning an ambiguous pi-radian heading change must be rejected.' -``` - -The existing `verify_path_smoothing_validation.ps1` assertion -`A smoothing candidate above vehicle maximum curvature must be rejected` -remains the independent validator gate. After the estimator implementation, -also add an analyzed over-limit circle check: - -```powershell -$overLimitSegment = New-ConstantCurvatureSegment ` - ($maximumAllowedCurvature + 0.01) ` - $forward -$overLimitAnalysis = Invoke-Analysis @($overLimitSegment) 0.05 -Assert-True ( - $overLimitAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter -gt - $maximumAllowedCurvature + 1e-6 -) 'An actually over-limit circle must remain measurably above the validator tolerance.' -$overLimitValidation = Invoke-AnalyzedValidation ` - $overLimitAnalysis ` - (New-PreparedPathFromSegment $overLimitSegment) ` - $certificationVehicle -Assert-False $overLimitValidation.Accepted ` - 'The existing validator must reject an actually over-limit analyzed circle.' -``` - -- [ ] **Step 3: Run the tests to verify RED** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_integration.ps1 -``` - -Expected: - -- build succeeds; -- geometry fails because the current `deltaHeading / polylineArcLength` estimate exceeds the exact circle by more than `1e-9`; -- integration fails at `RectangleDetour` with `Expected=Success Actual=InvalidInput`. - -- [ ] **Step 4: Implement the shared circle-chord estimator** - -Replace the curvature-assignment loop in `TryAnalyzeSegment` with: - -```csharp -for (int index = 0; index < count; index++) -{ - if (count == 1) - { - geometricCurvatures[index] = 0d; - continue; - } - - int leftIndex = index == 0 ? 0 : index - 1; - int rightIndex = index == count - 1 ? count - 1 : index + 1; - if (!TryEstimateGeometricCurvature( - samples, - unwrappedHeadings, - leftIndex, - rightIndex, - out geometricCurvatures[index], - out reason)) - { - return false; - } -} -``` - -Add this complete private method next to `Distance`: - -```csharp -private static bool TryEstimateGeometricCurvature( - IReadOnlyList samples, - IReadOnlyList unwrappedHeadings, - int leftIndex, - int rightIndex, - out double curvature, - out string reason) -{ - curvature = 0d; - reason = string.Empty; - if (samples == null || unwrappedHeadings == null || - leftIndex < 0 || rightIndex <= leftIndex || - rightIndex >= samples.Count || rightIndex >= unwrappedHeadings.Count) - { - reason = "候选路径曲率采样索引无效。"; - return false; - } - - double chordLength = Distance(samples[leftIndex], samples[rightIndex]); - double deltaHeading = unwrappedHeadings[rightIndex] - unwrappedHeadings[leftIndex]; - if (!NumericGuard.IsPositiveFinite(chordLength) || - !NumericGuard.IsFinite(deltaHeading) || - Math.Abs(deltaHeading) >= Math.PI) - { - reason = "候选路径曲率采样包含退化弦或无法唯一展开的转角。"; - return false; - } - - curvature = 2d * Math.Sin(0.5d * deltaHeading) / chordLength; - if (!NumericGuard.IsFinite(curvature)) - { - reason = "候选路径曲率计算产生了非法数值。"; - return false; - } - return true; -} -``` - -Do not change the direction-sign conversion, start steering-curvature override, -curvature-derivative denominator, validator tolerance, or vehicle limits. - -- [ ] **Step 5: Run the certification and P0 integration gate** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_validation.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_bezier.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_bspline.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_quintic.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_integration.ps1 -``` - -Expected: all commands pass; RectangleDetour raw baseline reports `Success`. - -If the analytic one-sided-underestimation assertion fails, do not change its -`1e-6` allowance or any safety threshold. Also execute the conditional -fallback in Step 6 when the shared-analyzer reconstruction of the raw baseline -is rejected by the unchanged validator specifically for nonfinite or -over-limit reconstructed curvature, while the corresponding trusted coarse -`VehicleCurvature` is finite and within the vehicle limit. - -- [ ] **Step 6: Conditional raw-baseline fallback gate** - -This step is skipped unless either gate above is met. It applies only to the -raw baseline: candidate paths keep the shared analyzer and every safety -threshold, including the `1e-6` validator tolerance, remains unchanged. - -1. retain the shared chord-based estimator even when the raw-baseline - reconstructed curvature invokes this fallback; -2. keep the exact-limit and over-limit analyzer/validator tests; -3. in `RawPathBaselineBuilder.TryCreate`, attempt the shared analysis and - existing full validator first; invoke the trusted raw analysis only when - that validator rejects specifically for nonfinite or over-limit - reconstructed curvature and the trusted coarse curvatures are finite and - within the vehicle limit; -4. implement the trusted raw analysis with: - -```csharp -private static bool TryCreateCoarseAnalysis( - PathSmoothingRequest request, - PreparedPath preparedPath, - out PathGeometryAnalysis analysis, - out string reason) -{ - analysis = null; - reason = string.Empty; - if (request.CoarsePath == null || request.Segments == null || - request.CoarsePath.Count == 0 || - request.Segments.Count != preparedPath.Segments.Count) - { - reason = "原始粗路径基线的点或方向段无效。"; - return false; - } - - var path = new List(request.CoarsePath.Count); - var segments = new List(request.Segments.Count); - double maximumCurvature = 0d; - double maximumDerivative = 0d; - double curvatureSquareSum = 0d; - double totalVariation = 0d; - double variationEnergy = 0d; - double minimumClearance = double.PositiveInfinity; - - for (int segmentIndex = 0; segmentIndex < request.Segments.Count; segmentIndex++) - { - CoarsePath.PathSegment segment = request.Segments[segmentIndex]; - if (segment == null || segment.SegmentIndex != segmentIndex || - segment.StartIndex < 0 || segment.EndIndex < segment.StartIndex || - segment.EndIndex >= request.CoarsePath.Count) - { - reason = "原始粗路径基线的方向段索引无效。"; - return false; - } - - for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) - { - CoarsePath.CoarsePathPoint point = request.CoarsePath[pointIndex]; - int leftIndex = pointIndex == segment.StartIndex ? pointIndex : pointIndex - 1; - int rightIndex = pointIndex == segment.EndIndex ? pointIndex : pointIndex + 1; - double derivative = 0d; - if (leftIndex != rightIndex) - { - double deltaArc = request.CoarsePath[rightIndex].ArcLength - - request.CoarsePath[leftIndex].ArcLength; - double deltaCurvature = request.CoarsePath[rightIndex].VehicleCurvature - - request.CoarsePath[leftIndex].VehicleCurvature; - if (!NumericGuard.IsPositiveFinite(deltaArc) || - !NumericGuard.IsFinite(deltaCurvature)) - { - reason = "原始粗路径基线无法计算有限的曲率导数。"; - return false; - } - derivative = deltaCurvature / deltaArc; - } - - double directionSign = point.Direction == CoarsePath.TravelDirection.Forward ? 1d : -1d; - double geometricCurvature = directionSign * point.VehicleCurvature; - if (!NumericGuard.IsFinite(geometricCurvature) || - !NumericGuard.IsFinite(derivative)) - { - reason = "原始粗路径基线包含非法曲率数值。"; - return false; - } - - path.Add(new SmoothedPathPoint( - point.X, - point.Y, - point.Heading, - point.UnwrappedHeading, - point.ArcLength, - point.Direction, - geometricCurvature, - point.VehicleCurvature, - derivative, - point.BodyClearance, - point.IsGearSwitchPoint, - point.IsGearSwitchPoint - ? SmoothedPathPointSource.GearSwitch - : SmoothedPathPointSource.CoarsePathFallback)); - - maximumCurvature = Math.Max(maximumCurvature, Math.Abs(point.VehicleCurvature)); - maximumDerivative = Math.Max(maximumDerivative, Math.Abs(derivative)); - curvatureSquareSum += point.VehicleCurvature * point.VehicleCurvature; - minimumClearance = Math.Min(minimumClearance, point.BodyClearance); - if (pointIndex > segment.StartIndex) - { - CoarsePath.CoarsePathPoint previous = request.CoarsePath[pointIndex - 1]; - double deltaArc = point.ArcLength - previous.ArcLength; - double deltaCurvature = geometricCurvature - - (previous.Direction == CoarsePath.TravelDirection.Forward ? 1d : -1d) * - previous.VehicleCurvature; - if (!NumericGuard.IsPositiveFinite(deltaArc) || - !NumericGuard.IsFinite(deltaCurvature)) - { - reason = "原始粗路径基线的曲率变化统计无效。"; - return false; - } - totalVariation += Math.Abs(deltaCurvature); - variationEnergy += deltaCurvature * deltaCurvature / deltaArc; - } - } - - segments.Add(new SmoothedPathSegment( - segment.SegmentIndex, - segment.Direction, - segment.StartIndex, - segment.EndIndex, - segment.StartsAtGearSwitch, - segment.EndsAtGearSwitch)); - } - - double pathLength = request.CoarsePath[request.CoarsePath.Count - 1].ArcLength; - double rmsCurvature = Math.Sqrt(curvatureSquareSum / request.CoarsePath.Count); - analysis = new PathGeometryAnalysis( - path, - segments, - pathLength, - maximumCurvature, - maximumDerivative, - rmsCurvature, - totalVariation, - variationEnergy, - minimumClearance); - return true; -} -``` - -Add these `using` directives if the fallback is used: - -```csharp -using System; -using MultiWheelC.TrajectoryPlanning.Utils; -using CoarsePath = MultiWheelC.TrajectoryPlanning.CoarsePath; -``` - -Run the same commands from Step 5. Expected: RectangleDetour and the raw -exact-limit baseline pass; the unchanged candidate analyzer and validator -still reject over-limit candidates. - -- [ ] **Step 7: Commit the successful P0 branch** - -For the shared estimator branch: - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs ` - ClumsyPilot/tests/verify_path_smoothing_geometry.ps1 -git diff --cached --check -git commit -m "fix: certify path curvature estimation" -``` - -For the conditional fallback branch, include -`RawPathBaselineBuilder.cs` in the same `git add` command and use: - -```powershell -git commit -m "fix: preserve trusted raw path curvature" -``` - ---- - -### Task 2: Enforce Total Window Length and Feasible Event Grouping - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1` - -**Interfaces:** - -- Consumes: sorted `CurvatureTransition` values, per-segment local length, - and `LocalG2OptionsSnapshot`. -- Produces: unchanged method contract: - -```csharp -internal bool TryPlan( - PreparedPath originalPath, - IReadOnlyList transitions, - LocalG2OptionsSnapshot options, - out IReadOnlyList regions, - out string reason); -``` - -- Guarantee: every emitted region has at least one `LocalG2WindowVariant`; - every variant satisfies - `EndArcLengthMeters - StartArcLengthMeters <= MaximumWindowLengthMeters + 1e-9`. - -- [ ] **Step 1: Add window-semantics scenarios to the planner test hook** - -Add a nested public `TestHooks` class to `LocalG2WindowPlanner` with this -stable entry point: - -```csharp -public static class TestHooks -{ - public static WindowPlanningTestSnapshot Execute(string scenario) - { - if (string.IsNullOrWhiteSpace(scenario)) - throw new ArgumentException("A scenario is required.", nameof(scenario)); - - IReadOnlyList transitions; - double segmentLength; - switch (scenario) - { - case "SeparatedByOneMeter": - transitions = new[] - { - Transition(0.2d, 0), - Transition(1.2d, 1), - }; - segmentLength = 1.4d; - break; - case "Mergeable": - transitions = new[] - { - Transition(0.4d, 0), - Transition(0.7d, 1), - }; - segmentLength = 1.4d; - break; - case "ThreeEventPartition": - transitions = new[] - { - Transition(0.2d, 0), - Transition(0.6d, 1), - Transition(1.2d, 2), - }; - segmentLength = 1.4d; - break; - case "NearBoundary": - transitions = new[] { Transition(0.1d, 0) }; - segmentLength = 1d; - break; - default: - throw new ArgumentOutOfRangeException(nameof(scenario)); - } - - var planner = new LocalG2WindowPlanner(); - if (!planner.TryPlan( - CreatePreparedPath(segmentLength), - transitions, - new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()), - out IReadOnlyList regions, - out string reason)) - { - throw new InvalidOperationException(reason); - } - - double maximumLength = 0d; - bool exactEnvelope = true; - var counts = new List(regions.Count); - var signature = new List(); - for (int regionIndex = 0; regionIndex < regions.Count; regionIndex++) - { - LocalG2SmoothingRegion region = regions[regionIndex]; - counts.Add(region.Transitions.Count.ToString()); - double minimumStart = double.PositiveInfinity; - double maximumEnd = double.NegativeInfinity; - for (int variantIndex = 0; variantIndex < region.WindowVariants.Count; variantIndex++) - { - LocalG2WindowVariant variant = region.WindowVariants[variantIndex]; - maximumLength = Math.Max( - maximumLength, - variant.EndArcLengthMeters - variant.StartArcLengthMeters); - minimumStart = Math.Min(minimumStart, variant.StartArcLengthMeters); - maximumEnd = Math.Max(maximumEnd, variant.EndArcLengthMeters); - signature.Add( - region.SegmentIndex + ":" + - variant.CandidateIndex + ":" + - variant.StartArcLengthMeters.ToString("R") + ":" + - variant.EndArcLengthMeters.ToString("R")); - } - exactEnvelope &= Math.Abs(region.MaximumStartArcLengthMeters - minimumStart) <= 1e-9d; - exactEnvelope &= Math.Abs(region.MaximumEndArcLengthMeters - maximumEnd) <= 1e-9d; - } - - LocalG2WindowVariant first = regions[0].WindowVariants[0]; - return new WindowPlanningTestSnapshot( - regions.Count, - string.Join(",", counts), - maximumLength, - exactEnvelope, - first.LeftWindowLengthMeters, - first.RightWindowLengthMeters, - string.Join("|", signature)); - } - - public sealed class WindowPlanningTestSnapshot - { - internal WindowPlanningTestSnapshot( - int regionCount, - string transitionCounts, - double maximumWindowLength, - bool exactEnvelope, - double firstLeftLength, - double firstRightLength, - string signature) - { - RegionCount = regionCount; - TransitionCounts = transitionCounts; - MaximumWindowLength = maximumWindowLength; - ExactEnvelope = exactEnvelope; - FirstLeftLength = firstLeftLength; - FirstRightLength = firstRightLength; - Signature = signature; - } - - public int RegionCount { get; } - public string TransitionCounts { get; } - public double MaximumWindowLength { get; } - public bool ExactEnvelope { get; } - public double FirstLeftLength { get; } - public double FirstRightLength { get; } - public string Signature { get; } - } - - private static CurvatureTransition Transition(double arcLength, int index) - { - return new CurvatureTransition( - 0, - index, - index + 1, - arcLength, - arcLength, - 0d, - 0d, - index % 2 == 0 ? 0d : 0.5d, - index % 2 == 0 ? 0.5d : 0d); - } - - private static PreparedPath CreatePreparedPath(double length) - { - var points = new[] - { - new SmoothingPoint2D( - 0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - new SmoothingPoint2D( - length, 0d, length, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), - }; - return new PreparedPath(new[] - { - new PreparedDirectionSegment( - 0, TravelDirection.Forward, points, false, false), - }); - } -} -``` - -The file already imports `System`, collections, processing and utility -namespaces; add `MultiWheelC.TrajectoryPlanning.CoarsePath` because the test -hook names `TravelDirection`. - -- [ ] **Step 2: Add failing reflection assertions** - -Append to `verify_path_smoothing_local_g2_detection.ps1`: - -```powershell -$plannerType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2WindowPlanner' -$plannerHooksType = $plannerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') -Assert-True ($null -ne $plannerHooksType) 'LocalG2WindowPlanner must expose narrow deterministic TestHooks.' -$planScenario = $plannerHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') - -$separated = $planScenario.Invoke($null, @('SeparatedByOneMeter')) -Assert-Equal 2 $separated.RegionCount 'Events 1.0 m apart cannot share a 0.80 m total window.' -Assert-Equal '1,1' $separated.TransitionCounts 'Separated events must remain one event per region.' - -$mergeable = $planScenario.Invoke($null, @('Mergeable')) -Assert-Equal 1 $mergeable.RegionCount 'Events covered by one legal total window must merge.' -Assert-Equal '2' $mergeable.TransitionCounts 'The merged region must retain both events.' - -$partition = $planScenario.Invoke($null, @('ThreeEventPartition')) -Assert-Equal 2 $partition.RegionCount 'Three events must split at the first infeasible joint window.' -Assert-Equal '2,1' $partition.TransitionCounts 'Only a feasible consecutive subgroup may merge.' - -$boundary = $planScenario.Invoke($null, @('NearBoundary')) -Assert-True ($boundary.FirstRightLength -gt $boundary.FirstLeftLength) ` - 'A boundary-clamped total window must transfer missing length to the available side.' -Assert-True ($boundary.MaximumWindowLength -le 0.80 + 1e-9) ` - 'No candidate window may exceed 0.80 m total length.' - -foreach ($snapshot in @($separated, $mergeable, $partition, $boundary)) { - Assert-True $snapshot.ExactEnvelope 'Region envelope must equal the extrema of actual legal variants.' - Assert-True ($snapshot.MaximumWindowLength -le 0.80 + 1e-9) ` - 'MaximumWindowLengthMeters is a total, not a per-side length.' -} - -$repeat = $planScenario.Invoke($null, @('ThreeEventPartition')) -Assert-Equal $partition.Signature $repeat.Signature ` - 'Repeated planning must preserve grouping, candidate numbering and variant order.' -``` - -- [ ] **Step 3: Run the detection script to verify RED** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -``` - -Expected: fail because the current event-`±0.80 m` ranges merge the -`SeparatedByOneMeter` scenario. - -- [ ] **Step 4: Replace overlap grouping with candidate-feasibility grouping** - -Replace the `while (cursor < ordered.Count)` body in `TryPlan` with: - -```csharp -while (cursor < ordered.Count) -{ - CurvatureTransition first = ordered[cursor]; - double segmentLength = segmentLengths[first.SegmentIndex]; - var group = new List { first }; - IReadOnlyList variants = - BuildVariants(group, segmentLength, options); - if (variants.Count == 0) - { - reason = "局部 G2 单事件无法生成满足总长度约束的窗口。"; - return false; - } - cursor++; - - while (cursor < ordered.Count && - ordered[cursor].SegmentIndex == first.SegmentIndex) - { - var tentative = new List(group) - { - ordered[cursor], - }; - IReadOnlyList tentativeVariants = - BuildVariants(tentative, segmentLength, options); - if (tentativeVariants.Count == 0) break; - group = tentative; - variants = tentativeVariants; - cursor++; - } - - double minimumStart = double.PositiveInfinity; - double maximumEnd = double.NegativeInfinity; - for (int variantIndex = 0; variantIndex < variants.Count; variantIndex++) - { - minimumStart = Math.Min( - minimumStart, - variants[variantIndex].StartArcLengthMeters); - maximumEnd = Math.Max( - maximumEnd, - variants[variantIndex].EndArcLengthMeters); - } - - planned.Add(new LocalG2SmoothingRegion( - first.SegmentIndex, - group, - minimumStart, - maximumEnd, - variants)); -} -``` - -Delete `MaximumLegalRange` and `WindowRange`; they encode the erroneous -per-side interpretation. - -At the start of `BuildTargets`, add: - -```csharp -if (segmentLength + MergeToleranceMeters < options.MinimumWindowLengthMeters) - return new ReadOnlyCollection(new List()); -``` - -In `AddIfLegal`, immediately before constructing the variant, add: - -```csharp -double actualLength = end - start; -if (actualLength + MergeToleranceMeters < options.MinimumWindowLengthMeters || - actualLength > options.MaximumWindowLengthMeters + MergeToleranceMeters) -{ - return; -} -``` - -Keep `candidateIndex = variants.Count`, so numbering remains deterministic -after infeasible variants are skipped. - -- [ ] **Step 5: Strengthen the immutable region invariant** - -In the `LocalG2SmoothingRegion` constructor, reject non-finite envelopes and -an empty variant list. Add `using MultiWheelC.TrajectoryPlanning.Utils;` and -use this .NET Standard 2.0-compatible code: - -```csharp -if (segmentIndex < 0 || transitions == null || transitions.Count == 0 || - !NumericGuard.IsFinite(maximumStartArcLengthMeters) || - !NumericGuard.IsFinite(maximumEndArcLengthMeters) || - maximumStartArcLengthMeters < 0d || - maximumEndArcLengthMeters < maximumStartArcLengthMeters || - windowVariants == null || windowVariants.Count == 0) -{ - throw new ArgumentOutOfRangeException(nameof(transitions)); -} -``` - -- [ ] **Step 6: Run focused and downstream tests** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curve.ps1 -``` - -Expected: all scripts pass. - -- [ ] **Step 7: Commit the window fix** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -git diff --cached --check -git commit -m "fix: enforce Local G2 total window length" -``` - ---- - -### Task 3: Add Deterministic Region Work Order and Two-Window Proof - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - -**Interfaces:** - -- Consumes: report-order `IReadOnlyList`. -- Produces: - -```csharp -internal bool TryCreate( - IReadOnlyList reportOrder, - out IReadOnlyList workOrder, - out string reason); -``` - -- Ordering: segment ascending; inside one segment, first transition local arc - descending; exact ties retain original report index. -- The method returns a new read-only collection and never mutates - `reportOrder`. - -- [ ] **Step 1: Add the failing two-region candidate scenario** - -Extend the `LocalG2CandidateBuilder.TestHooks.Execute` switch: - -```csharp -case "TwoRegionWorkOrder": return BuildTwoRegionWorkOrder(); -``` - -Add these optional constructor parameters and public properties to -`CandidateTestSnapshot`: - -```csharp -bool workOrderDescending = false, -bool frontArcPreservedAfterBackReplacement = false, -bool bothReplacementsRetained = false, -bool forwardOrderRejected = false, -bool deterministicWorkOrder = false, -bool invalidWorkOrderRejected = false -``` - -```csharp -WorkOrderDescending = workOrderDescending; -FrontArcPreservedAfterBackReplacement = frontArcPreservedAfterBackReplacement; -BothReplacementsRetained = bothReplacementsRetained; -ForwardOrderRejected = forwardOrderRejected; -DeterministicWorkOrder = deterministicWorkOrder; -InvalidWorkOrderRejected = invalidWorkOrderRejected; -``` - -```csharp -public bool WorkOrderDescending { get; } -public bool FrontArcPreservedAfterBackReplacement { get; } -public bool BothReplacementsRetained { get; } -public bool ForwardOrderRejected { get; } -public bool DeterministicWorkOrder { get; } -public bool InvalidWorkOrderRejected { get; } -``` - -Add this scenario method inside `TestHooks`: - -```csharp -private static CandidateTestSnapshot BuildTwoRegionWorkOrder() -{ - var points = new List(); - for (int index = 0; index <= 12; index++) - { - double arc = index * 0.25d; - points.Add(new SmoothingPoint2D( - arc, - 0d, - arc, - 0d, - 0d, - 1d, - false, - SmoothedPathPointSource.Anchor)); - } - - var segment = new PreparedDirectionSegment( - 0, - TravelDirection.Forward, - points, - false, - false); - var original = new PreparedPath(new[] { segment }); - LocalG2SmoothingRegion frontRegion = CreateOrderedRegion(0.75d, 0.5d, 1.0d, 0); - LocalG2SmoothingRegion backRegion = CreateOrderedRegion(2.25d, 2.0d, 2.5d, 1); - var reportOrder = new[] { frontRegion, backRegion }; - - var orderer = new LocalG2RegionWorkOrder(); - if (!orderer.TryCreate( - reportOrder, - out IReadOnlyList workOrder, - out string orderReason)) - { - throw new InvalidOperationException(orderReason); - } - if (!orderer.TryCreate( - reportOrder, - out IReadOnlyList repeatedOrder, - out string repeatedReason)) - { - throw new InvalidOperationException(repeatedReason); - } - - bool descending = - ReferenceEquals(backRegion, workOrder[0]) && - ReferenceEquals(frontRegion, workOrder[1]); - bool deterministic = - ReferenceEquals(workOrder[0], repeatedOrder[0]) && - ReferenceEquals(workOrder[1], repeatedOrder[1]) && - ReferenceEquals(frontRegion, reportOrder[0]) && - ReferenceEquals(backRegion, reportOrder[1]); - - var wrongSegmentTransition = new CurvatureTransition( - 1, - 0, - 1, - 0.75d, - 0.75d, - 0d, - 0d, - 0d, - 0.4d); - var invalidRegion = new LocalG2SmoothingRegion( - 0, - new[] { wrongSegmentTransition }, - 0.5d, - 1.0d, - new[] - { - new LocalG2WindowVariant(0, 0.5d, 1.0d, 0.25d, 0.25d), - }); - bool invalidWorkOrderRejected = - !orderer.TryCreate( - new[] { invalidRegion }, - out _, - out _) && - !orderer.TryCreate( - new LocalG2SmoothingRegion[] { null }, - out _, - out _); - - LocalG2CandidateGeometry frontCandidate = - CreateLengthChangingCandidate(0, 0.5d, 1.0d); - LocalG2CandidateGeometry backCandidate = - CreateLengthChangingCandidate(1, 2.0d, 2.5d); - var splicer = new LocalG2PathSplicer(); - - if (!splicer.TryReplace( - original, - backCandidate, - out PreparedPath afterBack, - out string backReason)) - { - throw new InvalidOperationException(backReason); - } - - bool frontArcPreserved = - PathReferenceInterpolator.TryInterpolateByArcLength( - afterBack.Segments[0].Points, - 0.5d, - out SmoothingPoint2D frontStart, - out _) && - PathReferenceInterpolator.TryInterpolateByArcLength( - afterBack.Segments[0].Points, - 1.0d, - out SmoothingPoint2D frontEnd, - out _) && - Math.Abs(frontStart.X - 0.5d) <= 1e-12d && - Math.Abs(frontEnd.X - 1.0d) <= 1e-12d; - - if (!splicer.TryReplace( - afterBack, - frontCandidate, - out PreparedPath afterBoth, - out string frontReason)) - { - throw new InvalidOperationException(frontReason); - } - - int localG2PointCount = 0; - for (int index = 0; index < afterBoth.Segments[0].Points.Count; index++) - { - if (afterBoth.Segments[0].Points[index].Source == - SmoothedPathPointSource.LocalG2Transition) - { - localG2PointCount++; - } - } - - if (!splicer.TryReplace( - original, - frontCandidate, - out PreparedPath afterFront, - out string firstReason)) - { - throw new InvalidOperationException(firstReason); - } - bool forwardOrderRejected = !splicer.TryReplace( - afterFront, - backCandidate, - out _, - out _); - - return new CandidateTestSnapshot( - 0, - 0d, - 0d, - 0d, - 0d, - false, - 0, - false, - string.Empty, - false, - false, - false, - false, - false, - false, - false, - descending, - frontArcPreserved, - localG2PointCount >= 2, - forwardOrderRejected, - deterministic, - invalidWorkOrderRejected); -} - -private static LocalG2SmoothingRegion CreateOrderedRegion( - double eventArc, - double startArc, - double endArc, - int index) -{ - var transition = new CurvatureTransition( - 0, - index, - index + 1, - eventArc, - eventArc, - 0d, - 0d, - 0d, - 0.4d); - return new LocalG2SmoothingRegion( - 0, - new[] { transition }, - startArc, - endArc, - new[] - { - new LocalG2WindowVariant( - 0, - startArc, - endArc, - eventArc - startArc, - endArc - eventArc), - }); -} - -private static LocalG2CandidateGeometry CreateLengthChangingCandidate( - int candidateIndex, - double startArc, - double endArc) -{ - double middleArc = 0.5d * (startArc + endArc); - var points = new[] - { - Point(startArc, 0d, startArc, false), - Point(middleArc, 0.20d, middleArc, false), - Point(endArc, 0d, endArc, false), - }; - return new LocalG2CandidateGeometry( - candidateIndex, - 0, - startArc, - endArc, - middleArc - startArc, - endArc - middleArc, - points, - 0d, - 0d, - 0d, - 0d, - true); -} -``` - -Append to `verify_path_smoothing_local_g2_candidates.ps1`: - -```powershell -$twoRegions = Invoke-Scenario 'TwoRegionWorkOrder' -Assert-True $twoRegions.WorkOrderDescending ` - 'Same-segment regions must be processed from larger original local arc to smaller local arc.' -Assert-True $twoRegions.FrontArcPreservedAfterBackReplacement ` - 'Replacing the back region must preserve the front region original arc coordinates.' -Assert-True $twoRegions.BothReplacementsRetained ` - 'Back-then-front replacement must retain both length-changing local replacements.' -Assert-True $twoRegions.ForwardOrderRejected ` - 'The regression fixture must prove that front-first invalidates the original back absolute arc.' -Assert-True $twoRegions.DeterministicWorkOrder ` - 'Work ordering must repeat exactly without mutating report order.' -Assert-True $twoRegions.InvalidWorkOrderRejected ` - 'Work ordering must reject null regions and cross-segment event contents.' -``` - -- [ ] **Step 2: Run the candidate script to verify RED** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -Expected: build fails because `LocalG2RegionWorkOrder` does not exist. - -- [ ] **Step 3: Create the work-order component** - -Create -`ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs` -with: - -```csharp -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using MultiWheelC.TrajectoryPlanning.Utils; - -namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; - -/// 把稳定报告顺序转换为不会使后续原始局部弧长错位的工作顺序。 -internal sealed class LocalG2RegionWorkOrder -{ - internal bool TryCreate( - IReadOnlyList reportOrder, - out IReadOnlyList workOrder, - out string reason) - { - workOrder = Empty(); - reason = string.Empty; - if (reportOrder == null) - { - reason = "局部 G2 区域工作顺序输入无效。"; - return false; - } - - var indexed = new List(reportOrder.Count); - for (int index = 0; index < reportOrder.Count; index++) - { - LocalG2SmoothingRegion region = reportOrder[index]; - if (!TryValidate(region, out double firstArc, out reason)) - return false; - indexed.Add(new IndexedRegion(region, index, firstArc)); - } - - indexed.Sort(Compare); - var ordered = new List(indexed.Count); - for (int index = 0; index < indexed.Count; index++) - ordered.Add(indexed[index].Region); - workOrder = new ReadOnlyCollection(ordered); - return true; - } - - private static bool TryValidate( - LocalG2SmoothingRegion region, - out double firstArc, - out string reason) - { - firstArc = 0d; - reason = string.Empty; - if (region == null || region.SegmentIndex < 0 || - region.Transitions == null || region.Transitions.Count == 0 || - region.WindowVariants == null || region.WindowVariants.Count == 0) - { - reason = "局部 G2 工作顺序包含空区域或空窗口集。"; - return false; - } - - double previousArc = -1d; - for (int index = 0; index < region.Transitions.Count; index++) - { - CurvatureTransition transition = region.Transitions[index]; - if (transition == null || - transition.SegmentIndex != region.SegmentIndex || - !NumericGuard.IsFinite(transition.LocalArcLengthMeters) || - transition.LocalArcLengthMeters < previousArc) - { - reason = "局部 G2 工作顺序要求区域事件有限、同段且按弧长升序。"; - return false; - } - previousArc = transition.LocalArcLengthMeters; - } - - firstArc = region.Transitions[0].LocalArcLengthMeters; - return true; - } - - private static int Compare(IndexedRegion left, IndexedRegion right) - { - int segment = left.Region.SegmentIndex.CompareTo(right.Region.SegmentIndex); - if (segment != 0) return segment; - int descendingArc = right.FirstArc.CompareTo(left.FirstArc); - return descendingArc != 0 - ? descendingArc - : left.ReportIndex.CompareTo(right.ReportIndex); - } - - private static IReadOnlyList Empty() - { - return new ReadOnlyCollection( - new List()); - } - - private sealed class IndexedRegion - { - internal IndexedRegion( - LocalG2SmoothingRegion region, - int reportIndex, - double firstArc) - { - Region = region; - ReportIndex = reportIndex; - FirstArc = firstArc; - } - - internal LocalG2SmoothingRegion Region { get; } - internal int ReportIndex { get; } - internal double FirstArc { get; } - } -} -``` - -- [ ] **Step 4: Run focused determinism and splicing tests** - -Run twice: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -``` - -Expected: every command passes; both candidate runs produce the same -work-order and coordinate assertions. - -- [ ] **Step 5: Commit the work-order component** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git commit -m "fix: order Local G2 regions back to front" -``` - ---- - -### Task 4: Make Task 8 Consume the Stabilized Contracts - -**Files:** - -- Modify: `docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md` - -**Interfaces:** - -- Consumes: - -```csharp -LocalG2RegionWorkOrder.TryCreate( - IReadOnlyList reportOrder, - out IReadOnlyList workOrder, - out string reason); -``` - -- Produces: an updated Task 8 plan in which processing order and report order - are separate, and a real two-region pipeline test is mandatory. - -- [ ] **Step 1: Add the work-order dependency to Task 8** - -In Task 8 `Files`, add: - -```markdown -- Consume unchanged: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs` -``` - -In Task 8 `Interfaces`, add: - -```markdown -- Consumes `LocalG2RegionWorkOrder.TryCreate(...)`; pipeline iteration must not use report-order regions directly. -- Publishes region reports in the detector's original ascending order, independently of processing order. -``` - -- [ ] **Step 2: Replace Task 8 sequential-processing pseudocode** - -Replace the `foreach (LocalG2SmoothingRegion region in regions)` block with: - -```csharp -if (!_workOrder.TryCreate( - regions, - out IReadOnlyList workRegions, - out string orderReason)) -{ - return PathSmoothingResult.Failure( - PathSmoothingStatus.Failed, - new PathSmoothingDiagnostics( - new PathQualityMetrics(), - TimeSpan.Zero, - 0, - 0d, - orderReason)); -} - -PreparedPath current = preparedPath; -var reportsByRegion = new Dictionary(); -int improvedCount = 0; - -foreach (LocalG2SmoothingRegion region in workRegions) -{ - cancellationToken.ThrowIfCancellationRequested(); - IReadOnlyList candidates = - _builder.Build( - preparedPath.Segments[region.SegmentIndex], - region, - outputSpacing, - options, - cancellationToken); - var evaluations = new List(); - foreach (LocalG2CandidateGeometry candidate in candidates) - { - evaluations.Add( - _evaluator.Evaluate( - preparedPath, - current, - region, - candidate, - request, - options, - cancellationToken)); - } - - LocalG2CandidateEvaluation best = - LocalG2CandidateEvaluator.SelectBest(evaluations); - if (best != null && best.Accepted) - { - current = best.SplicedPreparedPath; - improvedCount++; - reportsByRegion.Add(region, CreateImprovedReport(region, best)); - } - else - { - reportsByRegion.Add( - region, - CreateRetainedReport(region, evaluations)); - } -} - -var reports = new List(regions.Count); -for (int reportIndex = 0; reportIndex < regions.Count; reportIndex++) - reports.Add(reportsByRegion[regions[reportIndex]]); -``` - -- [ ] **Step 3: Add a real two-region Task 8 acceptance case** - -Add this exact requirement to Task 8 Step 1: - -```markdown -Create one deterministic same-direction fixture with two disjoint detected -regions. Both accepted candidates must change their local replacement length. -Assert: - -1. the larger-original-arc region is evaluated first; -2. the second processed region still matches the intended original front - window endpoints; -3. both reports are `Improved`; -4. reports are published in ascending original arc order; -5. both Local G2 replacements are present in the final full path; -6. two identical requests produce equal status, candidate indices, report - order, point count and point coordinates. - -The test must fail if the pipeline replaces `workRegions` with `regions`. -``` - -- [ ] **Step 4: Check that Task 8 no longer documents unsafe ascending iteration** - -Run: - -```powershell -rg -n "foreach \\(LocalG2SmoothingRegion region in regions\\)|LocalG2RegionWorkOrder|workRegions|reportsByRegion|two disjoint detected" ` - docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md -``` - -Expected: - -- no match for `foreach (LocalG2SmoothingRegion region in regions)`; -- matches for `LocalG2RegionWorkOrder`, `workRegions`, `reportsByRegion`, and - the two-region acceptance case. - -- [ ] **Step 5: Commit the Task 8 gate update** - -```powershell -git add -- docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md -git diff --cached --check -git commit -m "docs: gate Local G2 pipeline on stable region order" -``` - ---- - -### Task 5: Run the Complete Pre-Task-8 Verification Gate - -**Files:** - -- Verify: `ClumsyPilot/ClumsyPilot.csproj` -- Verify: all `ClumsyPilot/tests/verify_path_smoothing_*.ps1` -- Verify: files committed by Tasks 1–4 - -**Interfaces:** - -- Consumes: the committed P0/P1 fixes and existing regression scripts. -- Produces: evidence that the repository is ready to enter Task 8, without - claiming that the Local G2 service pipeline exists. - -- [ ] **Step 1: Build from the current committed source** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -``` - -Expected: build succeeds with zero errors. Existing obsolete API warnings may -remain only if they were already present before this plan. - -- [ ] **Step 2: Run every PathSmoothing verification script** - -Run: - -```powershell -$tests = @( - 'verify_path_smoothing_algorithm_input.ps1', - 'verify_path_smoothing_bezier.ps1', - 'verify_path_smoothing_bspline.ps1', - 'verify_path_smoothing_comparison.ps1', - 'verify_path_smoothing_contracts.ps1', - 'verify_path_smoothing_documentation.ps1', - 'verify_path_smoothing_fixtures.ps1', - 'verify_path_smoothing_geometry.ps1', - 'verify_path_smoothing_integration.ps1', - 'verify_path_smoothing_local_g2_candidates.ps1', - 'verify_path_smoothing_local_g2_curve.ps1', - 'verify_path_smoothing_local_g2_detection.ps1', - 'verify_path_smoothing_png.ps1', - 'verify_path_smoothing_quintic.ps1', - 'verify_path_smoothing_runner.ps1', - 'verify_path_smoothing_service.ps1', - 'verify_path_smoothing_svg_csv.ps1', - 'verify_path_smoothing_validation.ps1' -) -foreach ($test in $tests) { - & powershell -ExecutionPolicy Bypass -File (Join-Path 'ClumsyPilot/tests' $test) - if ($LASTEXITCODE -ne 0) { throw "$test failed with exit code $LASTEXITCODE" } -} -``` - -Expected: all scripts pass. In particular: - -- exact-limit forward/reverse circle checks pass at endpoints and interiors; -- actually over-limit curvature remains rejected; -- RectangleDetour raw baseline is `Success`; -- `0.80 m` is never treated as a per-side allowance; -- the two-window reverse-order proof passes twice. - -- [ ] **Step 3: Audit scope and staged state** - -Run: - -```powershell -git diff --check -git diff --cached --check -git status --short -git log -4 --oneline -``` - -Expected: - -- no whitespace errors; -- no staged files remain; -- the four task commits are visible; -- unrelated pre-existing worktree changes remain untouched; -- no `LocalG2PreSmoothingPipeline.cs` exists and `PathSmoothingService.cs` was - not changed by this plan. - -- [ ] **Step 4: Record the completion boundary** - -Report exactly: - -```text -任务 8 前稳定化完成:P0 曲率门、0.80 m 总窗口语义和多区域逆序底层证明均已通过。 -当前只具备进入任务 8 的条件;LocalG2PreSmoothingPipeline 和正式服务分派仍未实现。 -``` - -Do not create an empty verification commit. diff --git a/docs/superpowers/plans/2026-07-31-local-g2-soft-anchor-candidate-recovery.md b/docs/superpowers/plans/2026-07-31-local-g2-soft-anchor-candidate-recovery.md deleted file mode 100644 index c97e21c..0000000 --- a/docs/superpowers/plans/2026-07-31-local-g2-soft-anchor-candidate-recovery.md +++ /dev/null @@ -1,971 +0,0 @@ -# Local G2 Soft-Anchor Candidate Recovery Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Prove and implement a bounded soft-anchor Local G2 candidate family that makes the real `SingleTurn` route pass the unchanged safety/quality evaluator, then finish the dedicated Task 8 service pipeline. - -**Architecture:** First preserve the already-proven evaluator seam correction, then make window generation cover representative total lengths before asymmetric variants consume the budget. A disposable-copy feasibility gate evaluates the exact soft-anchor family before the shared production builder changes; only a successful non-zero-offset candidate permits the builder TDD and Task 8 service integration to continue. - -**Tech Stack:** C# 10, .NET Standard 2.0, PowerShell reflection verification, existing `PathGeometryAnalyzer`, `SmoothedPathValidator`, full-body collision checks, Git. - -## Global Constraints - -- `MinimumWindowLengthMeters = 0.20`, `PreferredWindowLengthMeters = 0.50`, and `MaximumWindowLengthMeters = 0.80` are total left-plus-right lengths. -- Path start, path end, gear switches, and outer window endpoints remain hard position anchors. -- Only an internal primitive-boundary position may move; its vehicle heading and distance-weighted shared curvature remain hard boundary values. -- `softOffset = min(0.05 m, 0.5 × MaximumDeviationMeters)`; if it is at most `1e-10 m`, emit no non-zero profile. -- Per selected representative window, profile order is exact anchor, `+softOffset`, `-softOffset`, all with derivative multiplier `1.00`. -- The configured candidate limit remains authoritative and is capped by the existing hard maximum of 12. -- Do not modify vehicle curvature limits, the validator `1e-6` tolerance, raw curvature-range tolerance, collision/clearance gates, `0.10 m` default maximum deviation, 20 percent peak-gradient improvement, or 2 percent variation-cost tolerance. -- Do not change Hybrid A*, SQP, legacy smoothing algorithms, or `PathSmoothingComparisonRequest.DefaultMethods`. -- Detector report order remains immutable ascending order; processing uses `LocalG2RegionWorkOrder`. -- Use TDD. Do not change `LocalG2CandidateBuilder` in the shared tree until the disposable feasibility gate finds an accepted non-zero-offset candidate under the unchanged evaluator. -- Preserve unrelated dirty worktree files. Stage only the exact files listed by each task. -- The untracked `ClumsyPilot/ParkrobTrajplanner/auto_avoidance` tree currently requires unavailable external assemblies. Never edit or delete it; when it blocks the normal build, verify this work from a disposable copy that excludes only that directory. - ---- - -## File Structure - -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` - - Preserve the analyzer-compatible `1e-10 m` boundary-point de-duplication already proven by RED/GREEN evidence. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` - - Expose preferred/minimum/maximum balanced targets before asymmetric variants and keep deterministic target coverage. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` - - Select representative windows, apply the bounded normal soft-anchor profiles, and emit at most 12 deterministic geometries. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` - - Consume work order, evaluate candidates, roll back failed global combinations, and publish raw baseline when no accepted replacement survives. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` - - Dispatch only `LocalG2Quintic` to the dedicated pipeline while preserving legacy routes. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1` - - Prove target coverage, total-window semantics, deterministic order, and configured caps. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - - Prove the evaluator seam, feasibility tuple, soft-anchor geometry, candidate budget, G2, direction, and determinism. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1` - - Prove public service statuses, safe complete-path publication, two-region order, rollback, and cancellation. -- `ClumsyPilot/tests/verify_path_smoothing_service.ps1` - - Prove dedicated Local G2 dispatch without disturbing legacy method registration. - ---- - -### Task 1: Preserve the evaluator window-boundary correction - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` -- Evidence: `.superpowers/sdd/local-g2-task-8-integration-report.md` - -**Interfaces:** - -- Consumes: interpolated window start/end points and source points from one `PreparedDirectionSegment`. -- Produces: `TryExtractWindow(...)` output with exact interpolated endpoints and no consecutive positions within `1e-10 m`. - -- [ ] **Step 1: Separate the completed evaluator regression from the still-RED candidate-family assertion** - -Keep the real SingleTurn seam and this assertion: - -```powershell -Assert-Equal 0 $duplicateFailures.Count ` - 'SingleTurn builder candidates must not fail evaluator window analysis due to duplicate or degenerate points.' -``` - -Remove only the current `$acceptedCandidates` collection and the assertion requiring an accepted candidate. Task 3 reintroduces the accepted non-zero-offset requirement after the feasibility gate. - -- [ ] **Step 2: Confirm the recorded RED/GREEN evidence is complete** - -Read `.superpowers/sdd/local-g2-task-8-integration-report.md` and require both entries: - -```text -RED: Expected=0 Actual=6 duplicate/degenerate evaluator failures -GREEN: Path smoothing Local G2 candidate checks passed. -``` - -The production correction must remain exactly: - -```csharp -private const double WindowPointToleranceMeters = 1e-10d; - -private static bool SamePosition(SmoothingPoint2D left, SmoothingPoint2D right) -{ - if (left == null || right == null) return false; - double x = right.X - left.X; - double y = right.Y - left.Y; - return x * x + y * y <= - WindowPointToleranceMeters * WindowPointToleranceMeters; -} -``` - -and `TryExtractWindow` must preserve the exact interpolated end: - -```csharp -if (point.ArcLength > startArcLength && - point.ArcLength < endArcLength && - !SamePosition(points[points.Count - 1], point)) -{ - points.Add(point); -} - -if (SamePosition(points[points.Count - 1], end)) - points[points.Count - 1] = end; -else - points.Add(end); -``` - -- [ ] **Step 3: Build and run the focused GREEN check** - -Run the normal commands first: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -If the build fails only because of untracked `auto_avoidance` dependencies, create a disposable verification copy: - -```powershell -$verificationRoot = Join-Path $env:TEMP ('parkingrobot-local-g2-' + [Guid]::NewGuid().ToString('N')) -New-Item -ItemType Directory -Path $verificationRoot | Out-Null -robocopy 'ClumsyPilot' (Join-Path $verificationRoot 'ClumsyPilot') /E /XD 'ClumsyPilot\ParkrobTrajplanner\auto_avoidance' -if ($LASTEXITCODE -gt 7) { throw "robocopy failed with $LASTEXITCODE" } -dotnet build (Join-Path $verificationRoot 'ClumsyPilot\ClumsyPilot.csproj') --no-restore -powershell -ExecutionPolicy Bypass -File (Join-Path $verificationRoot 'ClumsyPilot\tests\verify_path_smoothing_local_g2_candidates.ps1') -``` - -Expected: build has zero errors; candidate checks pass without an accepted-candidate requirement. - -- [ ] **Step 4: Commit only the evaluator seam** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "fix: deduplicate Local G2 evaluator windows" -``` - -Expected staged names: exactly the two files above. - ---- - -### Task 2: Guarantee representative window-target coverage - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1` - -**Interfaces:** - -- Consumes: `LocalG2OptionsSnapshot` total-window bounds and `MaximumCandidatesPerRegion`. -- Produces: ordered `WindowVariants` whose balanced target pass precedes 40/60 and 60/40 passes. - -- [ ] **Step 1: Add the failing coverage scenario** - -Add this case to `LocalG2WindowPlanner.TestHooks.Execute`: - -```csharp -case "InteriorCoverage": - transitions = new[] { Transition(1d, 0) }; - segmentLength = 2d; - break; -``` - -Extend `WindowPlanningTestSnapshot` with: - -```csharp -public int FirstRegionVariantCount { get; } -public bool RepresentativeTargetsFirst { get; } -public bool HasAsymmetricVariant { get; } -``` - -Append the three values to its internal constructor and assign them exactly: - -```csharp -internal WindowPlanningTestSnapshot( - int regionCount, - string transitionCounts, - double maximumWindowLength, - bool exactEnvelope, - double firstLeftLength, - double firstRightLength, - string signature, - int firstRegionVariantCount, - bool representativeTargetsFirst, - bool hasAsymmetricVariant) -{ - RegionCount = regionCount; - TransitionCounts = transitionCounts; - MaximumWindowLength = maximumWindowLength; - ExactEnvelope = exactEnvelope; - FirstLeftLength = firstLeftLength; - FirstRightLength = firstRightLength; - Signature = signature; - FirstRegionVariantCount = firstRegionVariantCount; - RepresentativeTargetsFirst = representativeTargetsFirst; - HasAsymmetricVariant = hasAsymmetricVariant; -} -``` - -Compute the values before returning the snapshot: - -```csharp -IReadOnlyList firstVariants = regions[0].WindowVariants; -bool representativeTargetsFirst = - firstVariants.Count >= 3 && - Math.Abs(WindowLength(firstVariants[0]) - 0.50d) <= 1e-9d && - Math.Abs(WindowLength(firstVariants[1]) - 0.20d) <= 1e-9d && - Math.Abs(WindowLength(firstVariants[2]) - 0.80d) <= 1e-9d; -bool hasAsymmetricVariant = false; -for (int index = 0; index < firstVariants.Count; index++) -{ - if (Math.Abs( - firstVariants[index].LeftWindowLengthMeters - - firstVariants[index].RightWindowLengthMeters) > 1e-9d) - { - hasAsymmetricVariant = true; - break; - } -} -``` - -Add the helper: - -```csharp -private static double WindowLength(LocalG2WindowVariant variant) => - variant.EndArcLengthMeters - variant.StartArcLengthMeters; -``` - -Pass the three values through the snapshot constructor. In the PowerShell verifier add: - -```powershell -$coverage = $executeMethod.Invoke($null, @('InteriorCoverage')) -Assert-True $coverage.RepresentativeTargetsFirst ` - 'Preferred, minimum, and maximum balanced targets must precede asymmetric variants.' -Assert-True $coverage.HasAsymmetricVariant ` - 'Default window planning must retain a legal asymmetric variant after balanced coverage.' -Assert-True ($coverage.FirstRegionVariantCount -le 12) ` - 'Window planning must obey the configured default cap.' -``` - -- [ ] **Step 2: Run the detection verifier to prove RED** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -``` - -Use the disposable-copy command from Task 1 only if the normal build is blocked by `auto_avoidance`. - -Expected RED: `RepresentativeTargetsFirst` is false because the current order begins `0.50, 0.50, 0.50` for balanced/asymmetric splits of one target. - -- [ ] **Step 3: Implement coverage-pass enumeration** - -Replace `BuildVariants` with: - -```csharp -private static IReadOnlyList BuildVariants( - IReadOnlyList transitions, - double segmentLength, - LocalG2OptionsSnapshot options) -{ - var variants = new List(); - double firstEvent = transitions[0].LocalArcLengthMeters; - double lastEvent = transitions[transitions.Count - 1].LocalArcLengthMeters; - double anchor = (firstEvent + lastEvent) / 2d; - IReadOnlyList targets = BuildTargets(options, segmentLength); - double[] ratios = { 0.5d, 0.4d, 0.6d }; - for (int ratioIndex = 0; ratioIndex < ratios.Length; ratioIndex++) - { - for (int targetIndex = 0; targetIndex < targets.Count; targetIndex++) - { - if (variants.Count >= options.MaximumCandidatesPerRegion) - return new ReadOnlyCollection(variants); - AddIfLegal( - variants, - targets[targetIndex], - ratios[ratioIndex], - anchor, - firstEvent, - lastEvent, - segmentLength, - ratioIndex == 0, - options); - } - } - return new ReadOnlyCollection(variants); -} -``` - -Replace the `requested` array in `BuildTargets` with: - -```csharp -double[] requested = -{ - options.PreferredWindowLengthMeters, - options.MinimumWindowLengthMeters, - options.MaximumWindowLengthMeters, - 0.75d * options.PreferredWindowLengthMeters, - 1.25d * options.PreferredWindowLengthMeters, -}; -``` - -Do not alter `AddIfLegal`, total-length validation, grouping, or region-envelope calculation. - -- [ ] **Step 4: Verify GREEN and determinism** - -Run twice: - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -Expected: all pass; both detection runs publish identical signatures. - -- [ ] **Step 5: Commit the planner coverage change** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "fix: cover Local G2 window targets before splits" -``` - ---- - -### Task 3: Prove and implement the soft-anchor candidate family - -**Files:** - -- Modify after feasibility GREEN: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` -- Evidence: `.superpowers/sdd/local-g2-soft-anchor-feasibility-report.md` - -**Interfaces:** - -- Consumes: stabilized `WindowVariants`, internal `CurvatureTransition` anchors, travel direction, and `MaximumDeviationMeters`. -- Produces: profile-major deterministic candidates with exact outer states and bounded normal movement at internal anchors. - -- [ ] **Step 1: Reintroduce the permanent RED acceptance seam** - -Before the real-candidate loop initialize: - -```powershell -$acceptedSoftCandidates = @() -$candidateSignatures = @() -``` - -For each real candidate, find its point at the first transition arc within -`1e-9 m`, then compute: - -```powershell -$transition = (Get-InternalProperty $region 'Transitions')[0] -$travelHeading = [double](Get-InternalProperty $transition 'VehicleHeadingRadians') -$normalX = -[Math]::Sin($travelHeading) -$normalY = [Math]::Cos($travelHeading) -$softOffset = 0.0 -foreach ($point in (Get-InternalProperty $realCandidate 'RegionPoints')) { - if ([Math]::Abs($point.ArcLength - (Get-InternalProperty $transition 'LocalArcLengthMeters')) -le 1e-9) { - $softOffset = ($point.X - (Get-InternalProperty $transition 'X')) * $normalX + - ($point.Y - (Get-InternalProperty $transition 'Y')) * $normalY - break - } -} -if ((Get-InternalProperty $evaluation 'Accepted') -and [Math]::Abs($softOffset) -gt 1e-10) { - $acceptedSoftCandidates += $realCandidate -} -``` - -Add: - -```powershell -Assert-True ($acceptedSoftCandidates.Count -gt 0) ` - 'SingleTurn must produce an accepted non-zero soft-anchor candidate under unchanged gates.' -``` - -Run the candidate verifier against the current builder. Expected RED: no non-zero soft-anchor candidate exists. - -- [ ] **Step 2: Run the disposable feasibility gate before shared production edits** - -Create a new disposable verification copy using Task 1's command. In that copy only, apply the complete builder changes from Steps 3 and 4 below with `apply_patch`. Add temporary output inside the candidate evaluation loop: - -```powershell -$candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex' -$startArc = Get-InternalProperty $realCandidate 'StartArcLengthMeters' -$endArc = Get-InternalProperty $realCandidate 'EndArcLengthMeters' -$accepted = Get-InternalProperty $evaluation 'Accepted' -$failure = Get-InternalProperty $evaluation 'FailureReason' -$rawPeak = Get-InternalProperty $evaluation 'RawPeakCurvatureDerivativePerSquareMeter' -$resultPeak = Get-InternalProperty $evaluation 'ResultPeakCurvatureDerivativePerSquareMeter' -$maximumCurvature = Get-InternalProperty $evaluation 'MaximumAbsoluteVehicleCurvaturePerMeter' -$maximumDeviation = Get-InternalProperty $evaluation 'MaximumDeviationMeters' -$minimumClearance = Get-InternalProperty $evaluation 'MinimumBodyClearanceMeters' -Write-Output ("soft-probe candidate=$candidateIndex window=$startArc..$endArc offset=$softOffset accepted=$accepted failure=$failure rawPeak=$rawPeak resultPeak=$resultPeak maxCurvature=$maximumCurvature deviation=$maximumDeviation clearance=$minimumClearance") -``` - -In the disposable verifier, also calculate the raw and candidate curvature -ranges with the evaluator's unchanged private analysis path: - -```powershell -function Get-AnalysisCurvatureRange($Analysis) { - $minimum = [double]::PositiveInfinity - $maximum = [double]::NegativeInfinity - foreach ($point in (Get-InternalProperty $Analysis 'Path')) { - $minimum = [Math]::Min($minimum, [double]$point.VehicleCurvature) - $maximum = [Math]::Max($maximum, [double]$point.VehicleCurvature) - } - return [PSCustomObject]@{ Minimum = $minimum; Maximum = $maximum } -} - -$extractMethod = Get-InternalMethod $evaluatorType 'TryExtractWindow' -$analyzeMethod = Get-InternalMethod $evaluatorType 'TryAnalyzeWindow' -$extractArgs = [object[]]@($preparedSegment, [double]$startArc, [double]$endArc, $null, $null) -Assert-True ($extractMethod.Invoke($realEvaluator, $extractArgs)) ` - 'The feasibility probe must extract the unchanged raw window.' -$rawWindow = $extractArgs[3] -$rawAnalyzeArgs = [object[]]@( - $preparedSegment, $rawWindow, [double]$configuration.OutputSpacingMeters, $null, $null) -Assert-True ($analyzeMethod.Invoke($realEvaluator, $rawAnalyzeArgs)) ` - 'The feasibility probe must analyze the unchanged raw window.' -$candidateAnalyzeArgs = [object[]]@( - $preparedSegment, - (Get-InternalProperty $realCandidate 'RegionPoints'), - [double]$configuration.OutputSpacingMeters, - $null, - $null) -Assert-True ($analyzeMethod.Invoke($realEvaluator, $candidateAnalyzeArgs)) ` - 'The feasibility probe must analyze the soft-anchor candidate window.' -$rawRange = Get-AnalysisCurvatureRange $rawAnalyzeArgs[3] -$candidateRange = Get-AnalysisCurvatureRange $candidateAnalyzeArgs[3] -Write-Output ("soft-probe ranges candidate=$candidateIndex raw=$($rawRange.Minimum)..$($rawRange.Maximum) candidate=$($candidateRange.Minimum)..$($candidateRange.Maximum)") -``` - -Run: - -```powershell -dotnet build (Join-Path $verificationRoot 'ClumsyPilot\ClumsyPilot.csproj') --no-restore -powershell -ExecutionPolicy Bypass -File (Join-Path $verificationRoot 'ClumsyPilot\tests\verify_path_smoothing_local_g2_candidates.ps1') -``` - -Write the exact command, every tuple line, and the final accepted tuple to `.superpowers/sdd/local-g2-soft-anchor-feasibility-report.md` using `apply_patch`. - -Gate result: - -- GREEN: at least one line has `accepted=True`, `abs(offset) > 1e-10`, result peak at most 80 percent of raw peak, and all unchanged evaluator gates pass. Continue. -- RED: no such line exists. Stop this plan, leave the shared builder unchanged, and report the design as blocked. Do not change expected fixture statuses or any threshold. - -- [ ] **Step 3: Implement representative-window selection after feasibility GREEN** - -Replace the current window-first/scale-second loop in `Build` with: - -```csharp -IReadOnlyList windows = - SelectRepresentativeWindows(region.WindowVariants); -double softOffset = Math.Min(0.05d, 0.5d * options.MaximumDeviationMeters); -double[] offsets = softOffset <= 1e-10d - ? new[] { 0d } - : new[] { 0d, softOffset, -softOffset }; -var candidates = new List(); -int limit = Math.Min(options.MaximumCandidatesPerRegion, 12); -for (int profileIndex = 0; profileIndex < offsets.Length; profileIndex++) -{ - for (int windowIndex = 0; windowIndex < windows.Count; windowIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - if (candidates.Count >= limit) return ReadOnly(candidates); - if (TryBuildCandidate( - candidates.Count, - originalSegment, - region, - windows[windowIndex], - 1d, - offsets[profileIndex], - outputSpacingMeters, - cancellationToken, - out LocalG2CandidateGeometry candidate)) - { - candidates.Add(candidate); - } - } -} -return ReadOnly(candidates); -``` - -Add these helpers next to `SameArc`: - -```csharp -private static IReadOnlyList SelectRepresentativeWindows( - IReadOnlyList variants) -{ - var selected = new List(4); - if (variants == null || variants.Count == 0) - return new ReadOnlyCollection(selected); - - AddDistinctWindow(selected, variants[0]); - LocalG2WindowVariant minimum = variants[0]; - LocalG2WindowVariant maximum = variants[0]; - LocalG2WindowVariant asymmetric = variants[0]; - for (int index = 1; index < variants.Count; index++) - { - LocalG2WindowVariant candidate = variants[index]; - if (IsShorter(candidate, minimum)) minimum = candidate; - if (IsLonger(candidate, maximum)) maximum = candidate; - if (IsMoreAsymmetric(candidate, asymmetric)) asymmetric = candidate; - } - AddDistinctWindow(selected, minimum); - AddDistinctWindow(selected, maximum); - AddDistinctWindow(selected, asymmetric); - return new ReadOnlyCollection(selected); -} - -private static bool IsShorter( - LocalG2WindowVariant candidate, - LocalG2WindowVariant current) -{ - double candidateLength = - candidate.EndArcLengthMeters - candidate.StartArcLengthMeters; - double currentLength = - current.EndArcLengthMeters - current.StartArcLengthMeters; - return candidateLength < currentLength - 1e-10d || - (Math.Abs(candidateLength - currentLength) <= 1e-10d && - candidate.CandidateIndex < current.CandidateIndex); -} - -private static bool IsLonger( - LocalG2WindowVariant candidate, - LocalG2WindowVariant current) -{ - double candidateLength = - candidate.EndArcLengthMeters - candidate.StartArcLengthMeters; - double currentLength = - current.EndArcLengthMeters - current.StartArcLengthMeters; - return candidateLength > currentLength + 1e-10d || - (Math.Abs(candidateLength - currentLength) <= 1e-10d && - candidate.CandidateIndex < current.CandidateIndex); -} - -private static bool IsMoreAsymmetric( - LocalG2WindowVariant candidate, - LocalG2WindowVariant current) -{ - double candidateValue = Math.Abs( - candidate.LeftWindowLengthMeters - candidate.RightWindowLengthMeters); - double currentValue = Math.Abs( - current.LeftWindowLengthMeters - current.RightWindowLengthMeters); - return candidateValue > currentValue + 1e-10d || - (Math.Abs(candidateValue - currentValue) <= 1e-10d && - candidate.CandidateIndex < current.CandidateIndex); -} - -private static void AddDistinctWindow( - List selected, - LocalG2WindowVariant candidate) -{ - for (int index = 0; index < selected.Count; index++) - { - if (SameArc(selected[index].StartArcLengthMeters, candidate.StartArcLengthMeters) && - SameArc(selected[index].EndArcLengthMeters, candidate.EndArcLengthMeters)) - { - return; - } - } - selected.Add(candidate); -} -``` - -- [ ] **Step 4: Implement bounded internal-anchor movement** - -Add `double normalOffsetMeters` to `TryBuildCandidate` immediately after `double multiplier`. - -Replace internal node construction with: - -```csharp -double travelHeading = segment.Direction == TravelDirection.Forward - ? transition.VehicleHeadingRadians - : transition.VehicleHeadingRadians - Math.PI; -double normalX = -Math.Sin(travelHeading); -double normalY = Math.Cos(travelHeading); -double nodeX = transition.X + normalOffsetMeters * normalX; -double nodeY = transition.Y + normalOffsetMeters * normalY; -if (!NumericGuard.IsFinite(nodeX) || !NumericGuard.IsFinite(nodeY)) - return false; -nodes.Add(new BoundaryNode( - transition.LocalArcLengthMeters, - nodeX, - nodeY, - transition.VehicleHeadingRadians, - sharedCurvature)); -``` - -Declare this immediately before the transition loop: - -```csharp -int internalNodeCount = 0; -``` - -Increment it immediately after adding each moved internal node. Before -`TryAssignDerivativeScales` add: - -```csharp -if (internalNodeCount == 0 && Math.Abs(normalOffsetMeters) > 1e-10d) - return false; -``` - -Keep the original interpolated outer nodes, distance-weighted `sharedCurvature`, derivative-scale calculation, derivative certification, sampling, direction sign, and candidate endpoint fields unchanged. -Delete the now-unused `DerivativeScaleMultipliers` field. - -- [ ] **Step 5: Complete permanent soft-anchor and budget assertions** - -Add assertions that: - -```powershell -Assert-True ($acceptedSoftCandidates.Count -gt 0) ` - 'SingleTurn must publish an accepted non-zero soft-anchor candidate.' -Assert-True ($realCandidates.Count -le $configuration.LocalG2Quintic.MaximumCandidatesPerRegion) ` - 'Builder output must obey the configured candidate cap.' -``` - -Run the real builder twice and compare, for every candidate, candidate index, start/end arc, point count, every X/Y/Heading/Source value, and the inferred signed internal offset. Loop candidate limits from `1` through `12`; rebuild options and assert output count never exceeds the configured limit. Set `MaximumDeviationMeters = 0.02` in one request and assert every inferred anchor offset is at most `0.010000001 m`. - -Extend the existing cluster and reverse TestHook cases so that one non-zero profile proves: - -```text -outer endpoint position error <= 1e-9 m -outer endpoint heading error <= 1e-9 rad -outer endpoint curvature error <= 1e-8 1/m -internal left/right tangent error <= 1e-8 -internal left/right curvature error <= 1e-8 1/m -``` - -- [ ] **Step 6: Verify candidate GREEN and integration progress** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -``` - -Use the disposable copy when the normal build is blocked only by `auto_avoidance`. - -Expected: detection and candidate scripts pass; service integration advances past `single-turn must publish its required Local G2 status`. Any later Task 8 failure is handled in Task 4 without modifying candidate gates. - -- [ ] **Step 7: Commit the soft-anchor candidate family** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "fix: generate bounded Local G2 soft anchors" -``` - ---- - -### Task 4: Finish the dedicated Task 8 pipeline and service publication - -**Files:** - -- Create from the existing untracked prototype: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` -- Create from the existing untracked prototype: `ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_service.ps1` - -**Interfaces:** - -- Consumes: verified `RawPathBaseline`, immutable prepared path, detector `reportOrder`, `LocalG2RegionWorkOrder`, candidate builder/evaluator. -- Produces: - -```csharp -internal PathSmoothingResult Smooth( - PathSmoothingRequest request, - PreparedPath preparedPath, - RawPathBaseline rawBaseline, - CancellationToken cancellationToken); -``` - -- Guarantees: every valid non-cancelled request publishes a complete verified path; invalid raw input and cancellation publish no partial path. - -- [ ] **Step 1: Run the public integration and service scripts as RED** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1 -``` - -Expected before the final pipeline correction: at least one mandatory fixture/status, raw-baseline publication, rollback, or report assertion fails. Record the first expected failure in `.superpowers/sdd/local-g2-task-8-integration-report.md`. - -- [ ] **Step 2: Preserve immutable report order and work-order processing** - -Keep this exact flow in `Smooth`: - -```csharp -IReadOnlyList reportOrder = - new ReadOnlyCollection( - new List(regions)); -if (!_workOrder.TryCreate( - reportOrder, - out IReadOnlyList workRegions, - out string orderReason)) -{ - return Failure(PathSmoothingStatus.Failed, stopwatch, orderReason); -} - -foreach (LocalG2SmoothingRegion region in workRegions) -{ - cancellationToken.ThrowIfCancellationRequested(); - // Build from preparedPath; evaluate against current. -} - -var reports = new List(reportOrder.Count); -for (int reportIndex = 0; reportIndex < reportOrder.Count; reportIndex++) - reports.Add(reportsByRegion[reportOrder[reportIndex]]); -``` - -Candidate construction must use `preparedPath.Segments[region.SegmentIndex]`; evaluation must receive both `preparedPath` and the evolving `current` path. - -- [ ] **Step 3: Publish the verified raw baseline without reconstructing it** - -Before regional processing, if `transitions.Count == 0`, publish: - -```csharp -return Publish( - PathSmoothingStatus.NotNeeded, - rawBaseline, - stopwatch, - new List()); -``` - -Add: - -```csharp -private static PathSmoothingResult Publish( - PathSmoothingStatus status, - RawPathBaseline baseline, - Stopwatch stopwatch, - IReadOnlyList reports) => - PathSmoothingResult.PublishLocalG2( - status, - baseline.Path, - baseline.Segments, - new PathSmoothingDiagnostics( - baseline.Metrics, - stopwatch.Elapsed, - 0, - 0d, - string.Empty), - reports); -``` - -After regional evaluation and global rollback, if `improvedCount == 0`, publish the same `rawBaseline` as `Unchanged` with detector-order reports. Do not call `PathGeometryAnalyzer` again for this raw fallback; the pre-Task-8 trusted-curvature gate already produced and fully validated it. - -- [ ] **Step 4: Make accepted and retained reports describe the actual candidate** - -For every region retain: - -```csharp -IReadOnlyList candidates -LocalG2CandidateEvaluation best -LocalG2CandidateGeometry selectedCandidate -``` - -Find the selected geometry deterministically: - -```csharp -LocalG2CandidateGeometry selectedCandidate = null; -for (int index = 0; index < candidates.Count; index++) -{ - if (candidates[index].CandidateIndex == best.CandidateIndex) - { - selectedCandidate = candidates[index]; - break; - } -} -``` - -When `best.Accepted`, require `selectedCandidate != null`, store it in `AcceptedRegion`, and report its start/end/left/right lengths. When no candidate is accepted, use candidate index `-1`, the first window only as planned-window diagnostics, and the stable failure reason from `best`. - -Extend `AcceptedRegion` with: - -```csharp -internal int CandidateCount { get; } -internal LocalG2CandidateGeometry Candidate { get; } -``` - -Rollback reports use the stored candidate count and selected candidate rather than `region.WindowVariants.Count`. - -- [ ] **Step 5: Preserve global rollback and exact status derivation** - -After candidate processing, validate `current`. If it fails, roll accepted regions back in reverse acceptance order. After each rollback, re-run full validation. Mark every removed region `GlobalValidationRollback`. - -Derive status only after rollback: - -```csharp -if (improvedCount == 0) - status = PathSmoothingStatus.Unchanged; -else if (improvedCount == regions.Count) - status = PathSmoothingStatus.Complete; -else - status = PathSmoothingStatus.PartialImprovement; -``` - -`Complete` and `PartialImprovement` must have at least one `Improved` report. If every accepted region is rolled back, publish the verified `rawBaseline` as `Unchanged`. - -- [ ] **Step 6: Keep service dispatch isolated** - -In `PathSmoothingService` retain the dedicated branch after common validation, preparation, and raw-baseline creation: - -```csharp -if (configuration.Method == SmoothingMethod.LocalG2Quintic) - return _localG2Pipeline.Smooth( - request, - preparedPath, - rawBaseline, - cancellationToken); -``` - -Validate `LocalG2QuinticOptions` only for that method; legacy methods continue through `Resolve(...)` and `_runner`. Do not add `LocalG2Quintic` to comparison defaults. - -- [ ] **Step 7: Complete integration assertions** - -The integration script must run every fixture twice and require: - -```text -Straight -> NotNeeded -SingleTurn -> Complete -LargeHeadingChange -> Complete -SBend -> Complete or PartialImprovement -RectangleDetour -> Complete or PartialImprovement -MultiObstacleDetour -> Complete or PartialImprovement -ReverseGearSwitch -> Complete, PartialImprovement, or NotNeeded -``` - -For every published path require complete segment coverage, feasible diagnostics, configured clearance, deterministic status/coordinates/reports, and at least one `Improved` report for `Complete`/`PartialImprovement`. Keep the strict `0.99` improvement request as `Unchanged`, cancellation as empty-path `Cancelled`, and the same-direction two-region case as back-to-front processing with detector-order reports. - -- [ ] **Step 8: Run focused and legacy GREEN checks** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -``` - -Expected: all pass. Use the disposable copy only for the unrelated `auto_avoidance` build blocker. - -- [ ] **Step 9: Commit only Task 8 publication files** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_service.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "feat: publish Local G2 presmoothing results" -``` - -Expected staged names: exactly the four files above. If the service file contains unrelated user hunks, stage the Task 8 patch into the index without staging those hunks and verify with `git diff --cached` before committing. - ---- - -### Task 5: Run the complete recovery and Task 8 verification gate - -**Files:** - -- Verify: all files committed by Tasks 1–4 -- Verify: all `ClumsyPilot/tests/verify_path_smoothing_*.ps1` -- Record: `.superpowers/sdd/local-g2-soft-anchor-task-8-final-report.md` - -**Interfaces:** - -- Consumes: the evaluator seam, window scheduling, proven soft-anchor family, and dedicated pipeline. -- Produces: evidence that Task 8 is complete without modifying Task 9 documentation. - -- [ ] **Step 1: Build the final source** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -``` - -Expected: zero errors. The two pre-existing obsolete API warnings may remain. If unrelated `auto_avoidance` dependencies still block the shared build, also record that exact failure and run the same build in the disposable copy. - -- [ ] **Step 2: Run every PathSmoothing verification script** - -```powershell -$tests = @( - 'verify_path_smoothing_algorithm_input.ps1', - 'verify_path_smoothing_bezier.ps1', - 'verify_path_smoothing_bspline.ps1', - 'verify_path_smoothing_comparison.ps1', - 'verify_path_smoothing_contracts.ps1', - 'verify_path_smoothing_documentation.ps1', - 'verify_path_smoothing_fixtures.ps1', - 'verify_path_smoothing_geometry.ps1', - 'verify_path_smoothing_integration.ps1', - 'verify_path_smoothing_local_g2_candidates.ps1', - 'verify_path_smoothing_local_g2_curve.ps1', - 'verify_path_smoothing_local_g2_detection.ps1', - 'verify_path_smoothing_local_g2_integration.ps1', - 'verify_path_smoothing_png.ps1', - 'verify_path_smoothing_quintic.ps1', - 'verify_path_smoothing_runner.ps1', - 'verify_path_smoothing_service.ps1', - 'verify_path_smoothing_svg_csv.ps1', - 'verify_path_smoothing_validation.ps1' -) -foreach ($test in $tests) { - & powershell -ExecutionPolicy Bypass -File (Join-Path 'ClumsyPilot/tests' $test) - if ($LASTEXITCODE -ne 0) { throw "$test failed with exit code $LASTEXITCODE" } -} -``` - -Expected: all 19 scripts pass. - -- [ ] **Step 3: Audit scope, safety thresholds, and Task 9 boundary** - -```powershell -git diff --check -git diff --cached --check -git status --short -git log -6 --oneline -rg -n "MaximumCurvaturePerMeter|CurvatureRangeTolerance|MinimumClearanceReserveMeters|MinimumPeakGradientImprovementRatio|MaximumVariationCostRegressionRatio" ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2 ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs -``` - -Confirm no safety value changed, no staged files remain, unrelated dirty files are untouched, comparison defaults exclude Local G2, and Task 9 README/documentation work has not started. - -- [ ] **Step 4: Record final evidence without an empty commit** - -Write `.superpowers/sdd/local-g2-soft-anchor-task-8-final-report.md` with: - -```text -Soft-anchor feasibility gate: PASS with exact accepted tuple and unchanged evaluator metrics. -Window target coverage: PASS for preferred/minimum/maximum before asymmetric variants. -SingleTurn: Complete with at least one accepted non-zero soft anchor. -Task 8 service publication: PASS including raw fallback, rollback, work/report order, and cancellation. -Full PathSmoothing verification: 19/19 scripts PASS (or shared-build limitation plus isolated-copy evidence explicitly recorded). -Task 9 documentation remains pending. -``` - -Do not create a verification-only commit. diff --git a/docs/superpowers/plans/2026-08-01-local-g2-minimum-curvature-excursion-feasibility.md b/docs/superpowers/plans/2026-08-01-local-g2-minimum-curvature-excursion-feasibility.md deleted file mode 100644 index e05f3c3..0000000 --- a/docs/superpowers/plans/2026-08-01-local-g2-minimum-curvature-excursion-feasibility.md +++ /dev/null @@ -1,1584 +0,0 @@ -# Local G2 Minimum Curvature Excursion Feasibility Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** In two disposable roots, determine whether a hard-anchor, arc-length curvature-Bézier Local G2 candidate can pass every existing `SingleTurn` safety and quality gate except the strict raw-curvature-range early return, and report the smallest excursion found within the fixed search budget. - -**Architecture:** A disposable `probe-core` Git repository owns the analytic curvature curve, the bounded two-variable pose-closure solver, the temporary candidate builder, deterministic diagnostics, and the shared probe script. Task 4 clones the same frozen core commit into a strict root whose evaluator is byte-identical to the shared baseline and a measurement root whose evaluator changes only the raw-range early return plus non-behavioral diagnostics; candidate SHA-256 records bind the two results atomically. No probe source is merged back: the shared branch receives only this plan and the final evidence report. - -**Tech Stack:** C# 10, .NET Standard 2.0, PowerShell 5.1 reflection tests, `System.Security.Cryptography.SHA256`, existing Local G2 preprocessing/evaluation/validation components, local disposable Git repositories. - -## Global Constraints - -- This plan implements only the isolated feasibility probe authorized by `docs/superpowers/specs/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-design.md`; it does not authorize production candidate/evaluator/configuration changes. -- Path start, path end, gear switches, outer window endpoints, and every internal primitive boundary remain hard position anchors. -- Internal primitive boundaries retain the original vehicle heading and the existing distance-weighted shared vehicle curvature. -- Work in travel geometry: `geometricCurvature = directionSign * vehicleCurvature`, with forward `directionSign=+1` and reverse `directionSign=-1`. -- Per interval, `kappa(u)` is cubic Bézier; `c2 = 4*deltaHeading/L - c0 - c1 - c3`; the only solver variables are `[L,c1]`. -- All `c0,c1,c2,c3` must be finite and within `[-Kvehicle,+Kvehicle]`; analyzer-discrete vehicle curvature must still pass the unchanged vehicle maximum-curvature gate. -- Before exact-anchor output overwrite, Euclidean position closure is `<=1e-9 m`; analytic heading error is `<=1e-8 rad`; endpoint/seam curvature error is `<=1e-8 1/m`. -- Candidate deviation remains `<=0.10 m`; full-body collision, map boundary, clearance, 20 percent peak-gradient improvement, and 2 percent variation-cost tolerance remain unchanged. -- The strict root evaluator is byte-identical to the recorded shared baseline. The measurement root may only record-and-continue at the raw-range early return and add diagnostics that do not reorder or change any other gate. -- Use the ordered states `PROBE_INVALID`, `BUDGET_RED`, `STRICT_GREEN`, `NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET`, `QUALITY_RED`, and `FEASIBLE_WITH_EXCURSION`; exactly one must result. -- Search limits are 12 windows, 36 window/seed combinations, 8 intervals per combination, 96 total solver invocations including failures, 16 outer LM iterations, 4 damping trials per iteration, and integration depth 10. -- Performance protocol is 5 warm-ups plus 30 measured runs. Solver-phase median/worst must be `<=10/25 ms`; measurement-root region median/worst must be `<=25/50 ms`. -- Identical measured runs must have identical candidate keys, candidate SHA-256 values, evaluator results, state, and diagnostic selection. -- Use TDD in the disposable repository. Every implementation task commits there and receives an independent task review before the next task. -- Preserve the shared dirty worktree. On the shared branch, stage only this plan or the final report named in Task 5. -- Never read, modify, copy, delete, or commit `ClumsyPilot/ParkrobTrajplanner/auto_avoidance`; verify it is absent from every disposable root before building. -- A cancellation is `PROBE_INVALID`, never a geometry RED. Do not use wall-clock time as a solver stop condition. - ---- - -## Execution workspace contract - -Task 1 creates one disposable Git repository under the system temp directory and records its absolute path and baseline hashes in `.superpowers/sdd/local-g2-excursion-task-1-report.md`. The controller passes that exact path to Tasks 2 and 3. Task 4 clones the frozen Task 3 core commit twice and records strict/measurement paths in `.superpowers/sdd/local-g2-excursion-task-4-report.md`. - -All `Create` and `Modify` paths in Tasks 1–4 are relative to the disposable root named in the task report, except `.superpowers/sdd/*`, which always resolves against the original shared repository. Those report files match the existing `.superpowers/sdd/.gitignore`, are never written inside strict/measurement clones, and must never be staged; the surrounding `.superpowers/` directory may remain unrelated untracked user state. - -## File Structure - -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs` - - Owns cubic curvature coefficients, analytic heading, adaptive 8/16 Gauss–Legendre coordinate integration, exact analytic curvature range/gradient diagnostics, and curve sampling. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs` - - Owns the joint physical-curvature domain, three seeds, finite differences, projected two-variable LM, fixed failure exits, and per-interval diagnostics. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs` - - Owns immutable interval/attempt/candidate diagnostics used only by the disposable probe. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` - - Temporarily attaches one `LocalG2ExcursionCandidateDiagnostic` without changing existing geometry semantics. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` - - Temporarily replaces the Hermite candidate family with bounded curvature-domain interval solving and enforces all attempt limits. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` - - Passes the already-computed vehicle maximum curvature into the temporary builder; no evaluator gate changes. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` - - Unchanged in core/strict roots. Measurement-root-only commit records the raw range and continues, while preserving every later gate. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1` - - Verifies curve math, integration, analytic extrema, finite inputs, and fixed integration boundaries. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1` - - Verifies analytic heading elimination, joint domain, exact LM constants/exits, cancellation, limits, and deterministic solutions. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1` - - Runs the real fixture, emits stable candidate keys/digests and gate/audit JSON, runs 5+30 timing, and applies the exhaustive state machine. -- `docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md` - - The only probe output copied to and committed on the shared branch. - ---- - -### Task 1: Create the disposable core and implement the analytic curvature curve - -**Files:** - -- Create in disposable core: `.gitignore` -- Create in disposable core: `.gitattributes` -- Create in disposable core: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs` -- Create in disposable core: `ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1` -- Record without committing: `.superpowers/sdd/local-g2-excursion-task-1-report.md` - -**Interfaces:** - -- Produces: - -```csharp -internal sealed class ArcLengthCurvatureBezierCurve2D -{ - internal static bool TryCreate( - double startX, double startY, double startTravelHeadingRadians, - double lengthMeters, double c0, double c1, double c2, double c3, - out ArcLengthCurvatureBezierCurve2D curve, out string reason); - - internal double LengthMeters { get; } - internal double StartCurvaturePerMeter { get; } - internal double EndCurvaturePerMeter { get; } - internal double EvaluateCurvature(double u); - internal double EvaluateCurvatureDerivativePerSquareMeter(double u); - internal double EvaluateTravelHeading(double u); - internal bool TryEvaluate(double u, CancellationToken cancellationToken, - out CurvatureCurveSample sample, out CurvatureIntegrationDiagnostic diagnostic, - out string reason); - internal bool TryGetAnalyticExtrema(out double minimumCurvature, - out double maximumCurvature, out double maximumAbsoluteGradient); - - public sealed class CurvatureCurveTestSnapshot - { - public double EndX { get; } - public double EndY { get; } - public double EndHeading { get; } - public double MinimumCurvature { get; } - public double MaximumCurvature { get; } - public double MaximumEndpointCurvature { get; } - public bool AllValuesFinite { get; } - public bool AnalyticMatchesDenseReference { get; } - public bool GradientMatchesDenseReference { get; } - public bool EqualToleranceAccepted { get; } - public bool DepthTenFailureIsStable { get; } - public bool CancellationPropagates { get; } - public bool InvalidInputsRejected { get; } - } - - public static class TestHooks - { - public static CurvatureCurveTestSnapshot Execute(string scenario); - } -} - -internal readonly struct CurvatureCurveSample -{ - internal CurvatureCurveSample(double parameter, double x, double y, - double travelHeadingRadians, double curvaturePerMeter); - internal double Parameter { get; } - internal double X { get; } - internal double Y { get; } - internal double TravelHeadingRadians { get; } - internal double CurvaturePerMeter { get; } -} - -internal readonly struct CurvatureIntegrationDiagnostic -{ - internal CurvatureIntegrationDiagnostic(int evaluationCount, int maximumDepth); - internal int EvaluationCount { get; } - internal int MaximumDepth { get; } -} - -``` - -- Consumers: Task 2 uses curve endpoint integration as the position residual; Task 3 uses `TryEvaluate` for candidate sampling. - -- [ ] **Step 1: Create and fingerprint the disposable core before changing code** - -Run from the shared repository root: - -```powershell -$sourceProject = (Resolve-Path 'ClumsyPilot').Path -$excludedAuto = [IO.Path]::GetFullPath((Join-Path $sourceProject 'ParkrobTrajplanner\auto_avoidance')) -$probeRoot = Join-Path ([IO.Path]::GetTempPath()) ('local-g2-excursion-' + [Guid]::NewGuid().ToString('N')) -$coreRoot = Join-Path $probeRoot 'probe-core' -$coreProject = Join-Path $coreRoot 'ClumsyPilot' -$probeEvidenceRoot = Join-Path $probeRoot 'evidence' -New-Item -ItemType Directory -Path $coreProject -Force | Out-Null -New-Item -ItemType Directory -Path $probeEvidenceRoot -Force | Out-Null -robocopy $sourceProject $coreProject /E /XD $excludedAuto bin obj .task8-sweep | Out-Null -if ($LASTEXITCODE -gt 7) { throw "robocopy failed with exit code $LASTEXITCODE" } -if (Test-Path (Join-Path $coreProject 'ParkrobTrajplanner\auto_avoidance')) { - throw 'auto_avoidance was copied into the disposable core.' -} -``` - -Use `apply_patch` in `$coreRoot` to create `.gitignore` with exactly: - -```gitignore -**/bin/ -**/obj/ -``` - -Create `.gitattributes` with exactly: - -```gitattributes -* -text -``` - -This disposable-only policy preserves every copied file's raw bytes and prevents the machine-level `core.autocrlf` setting from changing LF/CRLF bytes between the core and later clones. It does not modify the shared repository's attributes. - -Then initialize and commit the snapshot: - -```powershell -git -C $coreRoot init -git -C $coreRoot config core.autocrlf false -if ((git -C $coreRoot config --get core.autocrlf).Trim() -ne 'false') { - throw 'Disposable core must pin core.autocrlf=false.' -} -git -C $coreRoot add -- .gitattributes .gitignore ClumsyPilot -git -C $coreRoot commit -m 'chore: capture Local G2 excursion probe baseline' -``` - -Record in the task report: - -```text -shared HEAD -shared dirty-worktree status -absolute probeRoot/coreRoot -core baseline commit -SHA-256 of shared and core LocalG2CandidateEvaluator.cs -SHA-256 of path-smoothing-fixtures.json -sorted path + SHA-256 manifest for ClumsyPilot.csproj, -every .cs file below ParkrobTrajplanner/PathSmoothing, -and ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json -confirmation that auto_avoidance is absent -``` - -Build the manifest by enumerating only those explicit paths; never enumerate `ParkrobTrajplanner/auto_avoidance`. Map each shared relative path to the copied core path and require identical file counts, paths, and hashes before continuing. The two evaluator hashes must match before continuing. Store the machine-readable manifest under `$probeRoot/evidence/shared-baseline-manifest.json` and its SHA-256 in the Task 1 scratch report. - -- [ ] **Step 2: Write the failing curve test** - -Create `verify_path_smoothing_local_g2_curvature_probe_curve.ps1`. It must load the disposable `ClumsyPilot.dll`, reflect `ArcLengthCurvatureBezierCurve2D.TestHooks.Execute(string)`, and assert these exact scenarios: - -```powershell -$straight = Invoke-Scenario 'Straight' -Assert-Near 2.0 $straight.EndX 1e-11 'Straight X endpoint must integrate exactly.' -Assert-Near 0.0 $straight.EndY 1e-11 'Straight Y endpoint must remain zero.' -Assert-Near 0.0 $straight.EndHeading 1e-12 'Straight heading must remain zero.' - -$circle = Invoke-Scenario 'ConstantCurvature' -Assert-Near ([Math]::Sin(0.4) / 0.4) $circle.EndX 1e-11 'Circle X must match.' -Assert-Near ((1.0 - [Math]::Cos(0.4)) / 0.4) $circle.EndY 1e-11 'Circle Y must match.' -Assert-Near 0.4 $circle.EndHeading 1e-12 'Circle heading integral must match.' - -$counter = Invoke-Scenario 'ZeroHeadingCounterCurvature' -Assert-Near 0.0 $counter.EndHeading 1e-12 'Bezier heading elimination identity must hold.' -Assert-True ($counter.MinimumCurvature -lt 0.0) 'Counter-curvature scenario must go negative.' -Assert-Near 0.4 $counter.MaximumCurvature 1e-12 'Counter-curvature endpoint must remain 0.4 /m.' - -$overshoot = Invoke-Scenario 'PositiveInteriorOvershoot' -Assert-True ($overshoot.MaximumCurvature -gt $overshoot.MaximumEndpointCurvature) ` - 'A non-monotone control polygon must expose positive interior overshoot.' -Assert-True $overshoot.AllValuesFinite 'Positive-overshoot diagnostics must remain finite.' - -$extrema = Invoke-Scenario 'AnalyticExtrema' -Assert-True $extrema.AnalyticMatchesDenseReference 'Analytic extrema must match a dense reference.' -Assert-True $extrema.GradientMatchesDenseReference 'Analytic gradient peak must match a dense reference.' - -$limits = Invoke-Scenario 'IntegrationLimits' -Assert-True $limits.EqualToleranceAccepted 'Error equal to 1e-12 m must be accepted.' -Assert-True $limits.DepthTenFailureIsStable 'Depth 10 above tolerance must fail closed.' -Assert-True $limits.CancellationPropagates 'Cancellation must propagate.' -Assert-True $limits.InvalidInputsRejected 'Non-finite/illegal inputs must be rejected.' -``` - -- [ ] **Step 3: Run RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 -``` - -Expected: build or reflection FAIL because `ArcLengthCurvatureBezierCurve2D` does not exist. A pass means the seam is not exercising the new component. - -- [ ] **Step 4: Implement the cubic curvature and analytic heading** - -Use the polynomial coefficients exactly: - -```csharp -_a0 = c0; -_a1 = 3d * (c1 - c0); -_a2 = 3d * (c0 - 2d * c1 + c2); -_a3 = -c0 + 3d * c1 - 3d * c2 + c3; - -internal double EvaluateCurvature(double u) => - ((_a3 * u + _a2) * u + _a1) * u + _a0; - -internal double EvaluateTravelHeading(double u) => _startHeading + _length * - (_a0 * u + _a1 * u * u / 2d + _a2 * u * u * u / 3d + - _a3 * u * u * u * u / 4d); - -internal double EvaluateCurvatureDerivativePerSquareMeter(double u) => - (_a1 + 2d * _a2 * u + 3d * _a3 * u * u) / _length; -``` - -`TryGetAnalyticExtrema` evaluates curvature at `u=0`, `u=1`, and every finite derivative root in `[0,1]`. It evaluates absolute gradient at `u=0`, `u=1`, and the quadratic-gradient vertex `u=-_a2/(3*_a3)` when finite and in range. - -- [ ] **Step 5: Implement fixed adaptive 8/16 Gauss–Legendre integration** - -Use these positive nodes/weights and mirror them around zero: - -```csharp -private static readonly double[] Nodes8 = -{ 0.1834346424956498d, 0.5255324099163290d, 0.7966664774136267d, 0.9602898564975363d }; -private static readonly double[] Weights8 = -{ 0.3626837833783620d, 0.3137066458778873d, 0.2223810344533745d, 0.1012285362903763d }; -private static readonly double[] Nodes16 = -{ 0.09501250983763744d, 0.2816035507792589d, 0.4580167776572274d, - 0.6178762444026438d, 0.7554044083550030d, 0.8656312023878318d, - 0.9445750230732326d, 0.9894009349916499d }; -private static readonly double[] Weights16 = -{ 0.1894506104550685d, 0.1826034150449236d, 0.1691565193950025d, - 0.1495959888165767d, 0.1246289712555339d, 0.0951585116824928d, - 0.06225352393864789d, 0.02715245941175409d }; -``` - -For `cos(theta(u))` and `sin(theta(u))`, compute the 8- and 16-point normalized vector integrals over the current `u` subinterval. Accept when `L * EuclideanNorm(vector16-vector8) <= 1e-12 m`; otherwise bisect while `depth<10`; at `depth==10` return `IntegrationDepthLimit`. Multiply the accepted normalized integral by `L` exactly once when producing coordinates. - -- [ ] **Step 6: Run GREEN twice and commit in the disposable core** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 -git commit -m 'feat: add curvature-domain Local G2 probe curve' -``` - -Expected: build exit 0 and both script runs print `Local G2 curvature probe curve checks passed.` Record commands, exact output, baseline/head commits, and concerns in the Task 1 report. - ---- - -### Task 2: Implement the deterministic two-variable closure solver - -**Files:** - -- Create in disposable core: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs` -- Create in disposable core: `ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1` -- Record without committing: `.superpowers/sdd/local-g2-excursion-task-2-report.md` - -**Interfaces:** - -```csharp -internal readonly struct LocalG2HardBoundaryState -{ - internal LocalG2HardBoundaryState(double x, double y, double travelUnwrappedHeadingRadians, - double geometricCurvaturePerMeter, double referenceArcLengthMeters); - internal double X { get; } - internal double Y { get; } - internal double TravelUnwrappedHeadingRadians { get; } - internal double GeometricCurvaturePerMeter { get; } - internal double ReferenceArcLengthMeters { get; } -} - -internal enum LocalG2ClosureFailure -{ - None, InvalidInput, EmptyJointCurvatureDomain, IntegrationDepthLimit, - JacobianBoundaryDegenerate, SingularJacobian, NoDescentStep, - StalledAtBoundary, IterationLimit -} - -internal sealed class LocalG2PoseClosureSolution -{ - internal ArcLengthCurvatureBezierCurve2D Curve { get; } - internal int SeedIndex { get; } - internal int IterationCount { get; } - internal int DampingTrialCount { get; } - internal double LengthMeters { get; } - internal double C0 { get; } - internal double C1 { get; } - internal double C2 { get; } - internal double C3 { get; } - internal double PreOverwritePositionErrorMeters { get; } - internal double HeadingErrorRadians { get; } -} - -internal sealed class LocalG2PoseClosureDiagnostic -{ - internal int SeedIndex { get; } - internal int OuterIterationCount { get; } - internal int DampingTrialCount { get; } - internal int IntegrationEvaluationCount { get; } - internal int MaximumIntegrationDepth { get; } - internal double? LastLengthMeters { get; } - internal double? LastC1 { get; } - internal double? LastC2 { get; } - internal LocalG2ClosureFailure Failure { get; } - internal string Reason { get; } -} - -internal sealed class LocalG2PoseClosureSolver -{ - internal bool TrySolve(LocalG2HardBoundaryState start, LocalG2HardBoundaryState end, - double maximumDeviationMeters, double maximumVehicleCurvaturePerMeter, - int seedIndex, CancellationToken cancellationToken, - out LocalG2PoseClosureSolution solution, - out LocalG2PoseClosureDiagnostic diagnostic, - out LocalG2ClosureFailure failure, out string reason); - - public sealed class PoseClosureTestSnapshot - { - public bool Accepted { get; } - public double PositionError { get; } - public double HeadingError { get; } - public double C1 { get; } - public double C2 { get; } - public bool ExactBounds { get; } - public bool EmptyDomainRejected { get; } - public bool C2NeverClamped { get; } - public bool LengthBoundsExact { get; } - public bool ChordAboveMaximumRejected { get; } - public bool DifferenceRulesExact { get; } - public bool ProjectionOrderExact { get; } - public bool FourTrialLimitExact { get; } - public bool DampingScheduleExact { get; } - public bool PivotThresholdExact { get; } - public bool EqualStepStalls { get; } - public bool AboveStepContinues { get; } - public bool IterationLimitExact { get; } - public bool FinalTrialClosureAccepted { get; } - public bool AllFailureExitsStable { get; } - public IReadOnlyList FailureExitNames { get; } - public bool AllSeedsOrdered { get; } - public double SeedLowerBound { get; } - public double SeedUpperBound { get; } - public double Seed0C1 { get; } - public double Seed1C1 { get; } - public double Seed2C1 { get; } - public bool RepeatedSolutionsEqual { get; } - public bool CancellationPropagates { get; } - } - - public static class TestHooks - { - public static PoseClosureTestSnapshot Execute(string scenario); - } -} -``` - -- Consumes Task 1 curve. -- Produces one interval solution; Task 3 owns window/seed scheduling and total invocation budgets. -- Every non-cancelled `TrySolve` call returns a non-null immutable diagnostic, including `InvalidInput` and all other failures. Fields that were never evaluated remain nullable; cancellation throws and does not fabricate a diagnostic. - -- [ ] **Step 1: Write solver RED tests** - -The reflection script invokes `LocalG2PoseClosureSolver.TestHooks.Execute` and checks: - -```powershell -$known = Invoke-Scenario 'KnownCurveRecovery' -Assert-True $known.Accepted 'A known cubic-curvature endpoint must be recovered.' -Assert-True ($known.PositionError -le 1e-9) 'Known curve position must close.' -Assert-True ($known.HeadingError -le 1e-8) 'Known curve heading must close.' - -$counter = Invoke-Scenario 'SingleTurnLeftCounterCurvature' -Assert-True $counter.Accepted 'The zero-heading left interval must close within physical bounds.' -Assert-True ($counter.C1 -lt 0.0 -or $counter.C2 -lt 0.0) 'Closure must expose counter-curvature.' - -$domain = Invoke-Scenario 'JointDomain' -Assert-True $domain.ExactBounds 'c1 bounds must equal max(-K,S-K)..min(K,S+K).' -Assert-True $domain.EmptyDomainRejected 'An empty joint domain must be rejected.' -Assert-True $domain.C2NeverClamped 'c2 must be derived, never independently clamped.' - -$length = Invoke-Scenario 'LengthBounds' -Assert-True $length.LengthBoundsExact 'Length must be projected to [chord,rawSpan+2*deviation].' -Assert-True $length.ChordAboveMaximumRejected 'An empty length interval must reject InvalidInput.' - -$algorithm = Invoke-Scenario 'AlgorithmConstants' -Assert-True $algorithm.DifferenceRulesExact 'Central/one-sided finite differences must match the design.' -Assert-True $algorithm.ProjectionOrderExact 'Projection order must match the design.' -Assert-True $algorithm.FourTrialLimitExact 'Each outer iteration must permit at most four damping trials.' -Assert-True $algorithm.DampingScheduleExact 'Lambda must start at 1e-3 and change by factors of ten.' -Assert-True $algorithm.PivotThresholdExact 'The 1e-14 pivot threshold must be enforced.' -Assert-True $algorithm.EqualStepStalls 'normalizedStep exactly 1e-12 must stall when not closed.' -Assert-True $algorithm.AboveStepContinues 'normalizedStep above 1e-12 must not take the stall exit.' -Assert-True $algorithm.IterationLimitExact 'The outer iteration cap must be 16.' -Assert-True $algorithm.FinalTrialClosureAccepted ` - 'A closing fourth trial in outer iteration 16 must succeed, not return IterationLimit.' -Assert-True $algorithm.AllFailureExitsStable ` - 'Every failure enum must be triggered by a deterministic fixture and retain its exact reason.' -Assert-Sequence @('InvalidInput','EmptyJointCurvatureDomain','IntegrationDepthLimit', ` - 'JacobianBoundaryDegenerate','SingularJacobian','NoDescentStep', ` - 'StalledAtBoundary','IterationLimit') $algorithm.FailureExitNames ` - 'Every non-cancellation solver failure exit must be covered exactly once.' - -$stable = Invoke-Scenario 'DeterminismAndCancellation' -Assert-True $stable.AllSeedsOrdered 'Seeds must be mid, lower-quarter, upper-quarter.' -Assert-Near (($stable.SeedLowerBound + $stable.SeedUpperBound) / 2.0) ` - $stable.Seed0C1 1e-15 'Seed 0 must be the exact midpoint.' -Assert-Near (($stable.SeedLowerBound + $stable.Seed0C1) / 2.0) ` - $stable.Seed1C1 1e-15 'Seed 1 must be the exact lower quarter.' -Assert-Near (($stable.Seed0C1 + $stable.SeedUpperBound) / 2.0) ` - $stable.Seed2C1 1e-15 'Seed 2 must be the exact upper quarter.' -Assert-True $stable.RepeatedSolutionsEqual 'Repeated solution fields must match exactly.' -Assert-True $stable.CancellationPropagates 'Cancellation must throw OperationCanceledException.' - -``` - -- [ ] **Step 2: Run RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1 -``` - -Expected: reflection FAIL because the solver type does not exist. - -- [ ] **Step 3: Implement joint domain and analytic heading elimination** - -Use exactly: - -```csharp -double chord = Distance(start.X, start.Y, end.X, end.Y); -double rawSpan = end.ReferenceArcLengthMeters - start.ReferenceArcLengthMeters; -double maximumLength = rawSpan + 2d * maximumDeviationMeters; -double deltaHeading = end.TravelUnwrappedHeadingRadians - - start.TravelUnwrappedHeadingRadians; - -double SumForLength(double length) => - 4d * deltaHeading / length - start.GeometricCurvaturePerMeter - - end.GeometricCurvaturePerMeter; - -void GetJointBounds(double length, out double low, out double high) -{ - double sum = SumForLength(length); - low = Math.Max(-maximumVehicleCurvaturePerMeter, - sum - maximumVehicleCurvaturePerMeter); - high = Math.Min(maximumVehicleCurvaturePerMeter, - sum + maximumVehicleCurvaturePerMeter); -} -``` - -After projecting `L`, recompute bounds, project `c1`, then derive `c2=SumForLength(L)-c1`. Never clamp `c2`. - -- [ ] **Step 4: Implement the exact finite-difference and LM contract** - -Bind these constants in one place: - -```csharp -private const int MaximumOuterIterations = 16; -private const int MaximumDampingTrials = 4; -private const double InitialDamping = 1e-3d; -private const double MinimumDamping = 1e-12d; -private const double MaximumDamping = 1e12d; -private const double PivotTolerance = 1e-14d; -private const double StalledStepTolerance = 1e-12d; -private const double PositionToleranceMeters = 1e-9d; -private const double HeadingToleranceRadians = 1e-8d; -``` - -Use: - -```csharp -double positionScale = Math.Max(rawSpan, 1e-3d); -double hL = Math.Max(1e-7d, 1e-6d * positionScale); -double hC = Math.Max(1e-8d, 1e-6d * maximumVehicleCurvaturePerMeter); -double stepL = Math.Abs(trialL - currentL) / positionScale; -double stepC = Math.Abs(trialC1 - currentC1) / - Math.Max(maximumVehicleCurvaturePerMeter, 1e-8d); -double normalizedStep = Math.Max(stepL, stepC); -``` - -`J` is the Jacobian of `[dx/positionScale,dy/positionScale]`. Prefer central differences; use the actual projected variable delta in the denominator; fall back to one-sided when only one side is legal. Solve `(J^T J + lambda * diag(max(diag(J^T J),1))) * delta = -J^T rNormalized` as a damped `2x2` normal equation with partial pivoting. Accept only a finite strict norm decrease. Four failed trials return `NoDescentStep`; do not hide trials inside the outer-iteration counter. - -Implement `TrySolve` with this complete control flow: - -1. Check cancellation; validate all inputs, `seedIndex in [0,2]`, `rawSpan>0`, `maximumDeviation>=0`, and `Kvehicle>0`. Compute `minimumLength=chord`, `maximumLength=rawSpan+2*maximumDeviation`; return `InvalidInput` when the interval is empty or non-finite. -2. Project `rawSpan` to the length interval, compute the joint `c1` bounds, and return `EmptyJointCurvatureDomain` when empty. Initialize `c1` as `mid`, `(low+mid)/2`, or `(mid+high)/2` for seed 0, 1, or 2. Derive `c2`; evaluate the curve and normalized residual. Propagate cancellation and map only integration depth exhaustion to `IntegrationDepthLimit`. -3. At the start of each of 16 outer iterations, check cancellation and all success conditions: Euclidean position `<=1e-9`, analytic heading `<=1e-8`, endpoint curvatures `<=1e-8`, finite values, and all four controls inside the physical domain. Return the immutable solution immediately only when every condition passes. -4. For `L` then `c1`, form projected plus/minus perturbations in that order. Evaluate both distinct legal sides for a central column; otherwise evaluate the one distinct legal side against the current residual. Divide by the actual projected physical-variable delta. If neither side is distinct/legal, return `JacobianBoundaryDegenerate`. -5. Carry `lambda` across outer iterations, initially `1e-3`. For damping trials 0 through 3: check cancellation; form the exact damped normal equation; solve it with partial pivoting; return `SingularJacobian` when either selected absolute pivot is `<1e-14`; project the proposed `L`, recompute/project the joint `c1` domain, derive `c2`, and evaluate the trial. -6. Immediately after every trial evaluation and before testing step size or descent, run the complete hard-success predicate from item 3 and return the immutable solution if it passes. Otherwise, if projected `normalizedStep<=1e-12`, return `StalledAtBoundary`. Accept only a finite strict residual-norm decrease, set `lambda=max(1e-12,lambda/10)`, store the trial, and begin the next outer iteration. On rejection set `lambda=min(1e12,lambda*10)` and try again. Four rejected trials return `NoDescentStep`. This post-trial success check also applies to the fourth damping trial of the 16th outer iteration; a closed final trial must never fall through to `IterationLimit`. -7. After the 16th unsuccessful outer iteration return `IterationLimit`. Never catch `OperationCanceledException`, randomize seeds, use wall-clock termination, or reuse damping/history across calls. - -- [ ] **Step 5: Run GREEN twice, run Task 1 regression, and commit** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1 -git commit -m 'feat: add bounded Local G2 pose closure probe' -``` - -Expected: all scripts exit 0 with their named pass messages. Record the exact solver status counts from synthetic cases. - ---- - -### Task 3: Integrate bounded candidates and emit strict candidate audit data - -**Files:** - -- Create in disposable core: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs` -- Modify in disposable core: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` -- Modify in disposable core: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` -- Modify in disposable core: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` -- Create in disposable core: `ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1` -- Record without committing: `.superpowers/sdd/local-g2-excursion-task-3-report.md` - -**Interfaces:** - -```csharp -internal sealed class LocalG2ExcursionIntervalDiagnostic -{ - internal int IntervalIndex { get; } - internal bool Accepted { get; } - internal ArcLengthCurvatureBezierCurve2D Curve { get; } - internal LocalG2ClosureFailure Failure { get; } - internal string Reason { get; } - internal double? LengthMeters { get; } - internal double? GeometricC0 { get; } - internal double? GeometricC1 { get; } - internal double? GeometricC2 { get; } - internal double? GeometricC3 { get; } - internal double? VehicleC0 { get; } - internal double? VehicleC1 { get; } - internal double? VehicleC2 { get; } - internal double? VehicleC3 { get; } - internal int OuterIterationCount { get; } - internal int DampingTrialCount { get; } - internal int IntegrationEvaluationCount { get; } - internal int MaximumIntegrationDepth { get; } - internal long SolverElapsedStopwatchTicks { get; } - internal long StopwatchFrequency { get; } - internal double? PreOverwritePositionErrorMeters { get; } - internal double? AnalyticHeadingErrorRadians { get; } - internal double? AnalyticMinimumCurvaturePerMeter { get; } - internal double? AnalyticMaximumCurvaturePerMeter { get; } - internal double? AnalyticMaximumGradientPerSquareMeter { get; } -} - -internal sealed class LocalG2ExcursionCandidateDiagnostic -{ - internal int WindowIndex { get; } - internal int SeedIndex { get; } - internal int CombinationAttemptIndex { get; } - internal int SolverInvocationCount { get; } - internal int DirectionSign { get; } - internal IReadOnlyList HardNodes { get; } - internal IReadOnlyList Intervals { get; } - internal double MaximumPreOverwritePositionErrorMeters { get; } - internal double MaximumPostOverwritePositionErrorMeters { get; } - internal double MaximumTangentSeamErrorRadians { get; } - internal double MaximumCurvatureSeamErrorPerMeter { get; } - internal double MinimumAdjacentPointDistanceMeters { get; } -} - -internal sealed class LocalG2ExcursionCandidateLedgerEntry -{ - internal int WindowIndex { get; } - internal int SeedIndex { get; } - internal int CombinationAttemptIndex { get; } - internal LocalG2CandidateGeometry Candidate { get; } - internal bool IsCanonical { get; } - internal int CanonicalCandidateIndex { get; } - internal IReadOnlyList EquivalenceParameters { get; } - internal double MaximumEquivalenceCoordinateDifferenceMeters { get; } -} - -internal enum LocalG2ExcursionAttemptOutcome -{ - CandidateConstructed, - NumericallyDeduplicated, - HardNodeInvalid, - IntervalSolveFailed, - CandidateAssemblyFailed, - CombinationBudgetExhausted, - SolverInvocationBudgetExhausted, - CandidateLimitReached, - ProbeInvalid -} - -internal enum LocalG2ExcursionSearchStop -{ - None, - CombinationBudgetExhausted, - SolverInvocationBudgetExhausted, - CandidateLimitReached, - ProbeInvalid -} - -internal sealed class LocalG2ExcursionAttemptDiagnostic -{ - internal int WindowIndex { get; } - internal int SeedIndex { get; } - internal int CombinationAttemptIndex { get; } - internal int IntervalIndex { get; } - internal int EquivalentCandidateIndex { get; } - internal LocalG2ExcursionAttemptOutcome Outcome { get; } - internal LocalG2ClosureFailure ClosureFailure { get; } - internal string Reason { get; } - internal IReadOnlyList Intervals { get; } -} - -internal sealed class LocalG2ExcursionProbeRunDiagnostic -{ - internal int WindowCount { get; } - internal int CombinationAttemptCount { get; } - internal int FailedCombinationCount { get; } - internal int SolverInvocationCount { get; } - internal int CandidateCount { get; } - internal int MaximumIntervalsPerCombination { get; } - internal LocalG2ExcursionSearchStop SearchStop { get; } - internal int SearchStopWindowIndex { get; } - internal int SearchStopSeedIndex { get; } - internal bool ProbeInvalid { get; } - internal string ProbeInvalidReason { get; } - internal bool HasCandidateGeometryMetrics { get; } - internal double? MaximumPreOverwritePositionErrorMeters { get; } - internal double? MaximumPostOverwritePositionErrorMeters { get; } - internal double? MaximumTangentSeamErrorRadians { get; } - internal double? MaximumCurvatureSeamErrorPerMeter { get; } - internal double? MinimumAdjacentPointDistanceMeters { get; } - internal IReadOnlyList Attempts { get; } - internal IReadOnlyList CandidateLedger { get; } -} - -internal sealed class LocalG2CandidateBuilder -{ - private static readonly double[] EquivalenceParameters = - { 0d, 0.25d, 0.5d, 0.75d, 1d }; - internal LocalG2ExcursionProbeRunDiagnostic LastProbeDiagnostic { get; } - - public sealed class DirectionRoundTripSnapshot - { - public double ForwardVehicleCurvature { get; } - public double ForwardGeometricCurvature { get; } - public double ReverseVehicleCurvature { get; } - public double ReverseGeometricCurvature { get; } - public bool ForwardHeadingRoundTrips { get; } - public bool ReverseHeadingRoundTrips { get; } - } - - public sealed class EquivalenceToleranceSnapshot - { - public bool ExactThresholdsEquivalent { get; } - public bool AboveAnyThresholdDistinct { get; } - public bool HardNodeMismatchDistinct { get; } - public bool IntervalCountMismatchDistinct { get; } - public bool FixedEquivalenceGridExact { get; } - public bool AliasRetainsIndependentDigestInput { get; } - } - - public static class TestHooks - { - public static DirectionRoundTripSnapshot ExecuteDirectionRoundTrip(); - public static EquivalenceToleranceSnapshot ExecuteEquivalenceTolerance(); - } -} - -internal IReadOnlyList Build( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - double maximumVehicleCurvaturePerMeter, - LocalG2OptionsSnapshot options, - CancellationToken cancellationToken); - -private bool TryCreateHardBoundaryStates( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - LocalG2WindowVariant window, - double maximumVehicleCurvaturePerMeter, - out IReadOnlyList hardNodes, - out string reason); - -private bool TryCreateCandidateFromSolutions( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - LocalG2WindowVariant window, - int windowIndex, - int seedIndex, - int combinationAttemptIndex, - int candidateIndex, - IReadOnlyList hardNodes, - IReadOnlyList solutions, - IReadOnlyList intervalDiagnostics, - double outputSpacingMeters, - int solverInvocationCount, - out LocalG2CandidateGeometry candidate, - out LocalG2ExcursionCandidateDiagnostic diagnostic, - out string reason); - -private void RecordAttemptFailure(int windowIndex, int seedIndex, - int combinationAttemptIndex, LocalG2ClosureFailure failure, string reason); -private void RecordIntervalFailure(int windowIndex, int seedIndex, - int combinationAttemptIndex, int intervalIndex, - LocalG2ClosureFailure failure, string reason, - IReadOnlyList intervalDiagnostics); -private void RecordCandidateFailure(int windowIndex, int seedIndex, - int combinationAttemptIndex, LocalG2ExcursionCandidateDiagnostic diagnostic, - string reason); -private void MarkProbeInvalid(int windowIndex, int seedIndex, string reason); -private void MarkSearchStopped(int windowIndex, int seedIndex, - LocalG2ExcursionSearchStop stop, string reason); -private bool TryAddIfNumericallyNew(List candidates, - LocalG2CandidateGeometry candidate, - LocalG2ExcursionCandidateDiagnostic diagnostic, - CancellationToken cancellationToken, out string reason); -private static bool TryAreNumericallyEquivalent( - LocalG2ExcursionCandidateDiagnostic left, - LocalG2ExcursionCandidateDiagnostic right, - CancellationToken cancellationToken, out bool equivalent, - out double maximumCoordinateDifferenceMeters, out string reason); -private static LocalG2ExcursionIntervalDiagnostic CreateIntervalDiagnostic( - int intervalIndex, int directionSign, - LocalG2HardBoundaryState start, LocalG2HardBoundaryState end, - LocalG2PoseClosureSolution solution, - LocalG2PoseClosureDiagnostic solverDiagnostic, - LocalG2ClosureFailure failure, string reason, - long solverElapsedStopwatchTicks, long stopwatchFrequency); -private static int DirectionSign(TravelDirection direction); -``` - -The displayed builder `TestHooks` methods are added to the existing nested `TestHooks` class; do not create a second class, replace the existing `Execute(string): CandidateTestSnapshot`, or overload by return type. - -- The `LocalG2CandidateGeometry` constructor adds one final optional parameter: - -```csharp -LocalG2ExcursionCandidateDiagnostic excursionDiagnostic = null -``` - -- The probe script command-line contract is: - -```powershell -param( - [ValidateSet('Strict','Measurement','FinalGate')] - [string]$Mode = 'Strict', - [string]$StrictRoot, - [string]$MeasurementRoot, - [string]$EvidenceRoot, - [string]$RunLabel = 'single', - [string]$CoreCommit, - [switch]$Batch -) -``` - -`Strict` and non-batch `Measurement` use the assembly under the current root and emit one run at the path returned by `Join-Path $EvidenceRoot ($RunLabel + '.json')`; when `EvidenceRoot` is omitted for a one-root developer run, create a unique directory under `[IO.Path]::GetTempPath()` and print its full path. `Strict` may omit `CoreCommit`, in which case both identity commits are the current root `HEAD`; when supplied it must equal `HEAD`. `Measurement` requires `CoreCommit`, records its current `HEAD` as `RootCommit`, and requires every committed path in `CoreCommit..HEAD` to be exactly the evaluator path. `Measurement -Batch` loads the assembly and fixture once, performs five warm-up runs followed immediately by 30 measured runs serially in that same process, and emits one batch JSON containing both arrays. `Batch` is invalid with `Strict` or `FinalGate`. `FinalGate` requires both absolute root paths and an explicit `EvidenceRoot`, derives the expected core commit from strict-root `HEAD`, launches one strict child and one measurement-batch child with that exact `CoreCommit`, pairs their evidence, writes timing/state/report artifacts, and exits nonzero only for `PROBE_INVALID` or a broken test contract; valid RED/STRICT_GREEN/FEASIBLE states are evidence outcomes and exit zero. - -- [ ] **Step 1: Write the strict-root candidate/audit RED seam** - -The new script must use the real `single-turn` fixture and existing preprocessor/detector/window planner. Its `Invoke-ProbeHook` helper maps `DirectionRoundTrip` to `LocalG2CandidateBuilder.TestHooks.ExecuteDirectionRoundTrip()` and `EquivalenceTolerance` to `ExecuteEquivalenceTolerance()`, and throws for any other name or absent snapshot. Before evaluation it asserts exact attempt limits and hard geometry diagnostics: - -```powershell -$runDiagnostic = Get-InternalProperty $builder 'LastProbeDiagnostic' -$attempts = @(Get-InternalProperty $runDiagnostic 'Attempts') -$candidateLedger = @(Get-InternalProperty $runDiagnostic 'CandidateLedger') -$allIntervals = @($attempts | ForEach-Object { - @(Get-InternalProperty $_ 'Intervals') -}) -$hasCandidateGeometryMetrics = [bool](Get-InternalProperty $runDiagnostic 'HasCandidateGeometryMetrics') -$probe = [pscustomobject]@{ - WindowCount = [int](Get-InternalProperty $runDiagnostic 'WindowCount') - CombinationAttemptCount = [int](Get-InternalProperty $runDiagnostic 'CombinationAttemptCount') - SolverInvocationCount = [int](Get-InternalProperty $runDiagnostic 'SolverInvocationCount') - MaximumIntervalsPerCombination = [int](Get-InternalProperty $runDiagnostic 'MaximumIntervalsPerCombination') - CandidateCount = [int](Get-InternalProperty $runDiagnostic 'CandidateCount') - CandidateLedgerCount = $candidateLedger.Count - CanonicalLedgerCount = @($candidateLedger | Where-Object { - [bool](Get-InternalProperty $_ 'IsCanonical') - }).Count - DeduplicatedLedgerCount = @($candidateLedger | Where-Object { - -not [bool](Get-InternalProperty $_ 'IsCanonical') - }).Count - ProbeInvalid = [bool](Get-InternalProperty $runDiagnostic 'ProbeInvalid') - SearchStop = (Get-InternalProperty $runDiagnostic 'SearchStop').ToString() - HasCandidateGeometryMetrics = $hasCandidateGeometryMetrics - MaximumPreOverwritePositionError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumPreOverwritePositionErrorMeters') } else { $null }) - MaximumPostOverwritePositionError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumPostOverwritePositionErrorMeters') } else { $null }) - MaximumTangentError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumTangentSeamErrorRadians') } else { $null }) - MaximumCurvatureError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumCurvatureSeamErrorPerMeter') } else { $null }) - MinimumAdjacentDistance = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MinimumAdjacentPointDistanceMeters') } else { $null }) - AllAttemptsCategorized = Test-AllAttemptsCategorized $attempts ` - ([int](Get-InternalProperty $runDiagnostic 'CombinationAttemptCount')) - IntervalDiagnosticCount = $allIntervals.Count - AllSolverTicksNonnegative = -not @($allIntervals | Where-Object { - [long](Get-InternalProperty $_ 'SolverElapsedStopwatchTicks') -lt 0 - }).Count - AllStopwatchFrequenciesPositive = -not @($allIntervals | Where-Object { - [long](Get-InternalProperty $_ 'StopwatchFrequency') -le 0 - }).Count - StrictEvaluatorSourceMatchesBaseline = Test-StrictEvaluatorHash - CandidateLedgerAuditValid = - Test-CandidateDigestDeduplication $candidateLedger $attempts -} - -Assert-True ($probe.WindowCount -le 12) 'Window attempts must be capped at 12.' -Assert-True ($probe.CombinationAttemptCount -le 36) 'Combinations must be capped at 36.' -Assert-True ($probe.SolverInvocationCount -le 96) 'Failed solver calls must count toward 96.' -Assert-True ($probe.IntervalDiagnosticCount -eq $probe.SolverInvocationCount) ` - 'Every completed solver invocation must emit one timing diagnostic.' -Assert-True $probe.AllSolverTicksNonnegative 'Solver elapsed ticks must be nonnegative.' -Assert-True $probe.AllStopwatchFrequenciesPositive 'Stopwatch frequencies must be positive.' -if ($probe.MaximumIntervalsPerCombination -gt 8) { - Assert-True $probe.ProbeInvalid 'More than 8 intervals must produce PROBE_INVALID.' - Assert-True ($probe.SearchStop -eq 'ProbeInvalid') 'Invalid interval count must stop all scheduling.' -} else { - Assert-True ($probe.MaximumIntervalsPerCombination -le 8) 'A scheduled combination must contain at most 8 intervals.' -} -Assert-True ($probe.CandidateCount -le 12) 'Constructed candidates must be capped at 12.' -Assert-True ($probe.CandidateCount -eq $probe.CanonicalLedgerCount) ` - 'CandidateCount must count canonical ledger entries only.' -Assert-True ($probe.CandidateLedgerCount -eq - ($probe.CanonicalLedgerCount + $probe.DeduplicatedLedgerCount)) ` - 'Every ledger record must be canonical or a deduplicated alias.' -Assert-True $probe.AllAttemptsCategorized 'Every failed, exhausted, invalid, constructed, or deduplicated attempt must be retained.' -if ($probe.CandidateCount -gt 0) { - Assert-True ($probe.MaximumPreOverwritePositionError -le 1e-9) 'Pre-overwrite closure must pass.' - Assert-True ($probe.MaximumPostOverwritePositionError -le 1e-9) 'Post-overwrite anchor must pass.' - Assert-True ($probe.MaximumTangentError -le 1e-8) 'Travel tangent seams must be G2.' - Assert-True ($probe.MaximumCurvatureError -le 1e-8) 'Curvature seams must be G2.' - Assert-True ($probe.MinimumAdjacentDistance -gt 1e-10) 'Candidate points must not repeat.' -} else { - Assert-True (-not $probe.HasCandidateGeometryMetrics) ` - 'No-candidate runs must not synthesize hard-geometry metrics.' -} -Assert-True $probe.StrictEvaluatorSourceMatchesBaseline 'Strict evaluator must remain unchanged.' -Assert-True $probe.CandidateLedgerAuditValid ` - 'Candidate ledger keys, digests, canonical links, and deduplication evidence must be valid.' - -$direction = Invoke-ProbeHook 'DirectionRoundTrip' -Assert-Near $direction.ForwardVehicleCurvature $direction.ForwardGeometricCurvature 1e-12 ` - 'Forward geometric curvature must preserve the vehicle sign.' -Assert-Near (-$direction.ReverseVehicleCurvature) $direction.ReverseGeometricCurvature 1e-12 ` - 'Reverse geometric curvature must negate the vehicle sign.' -Assert-True $direction.ForwardHeadingRoundTrips 'Forward travel/vehicle heading conversion must round-trip.' -Assert-True $direction.ReverseHeadingRoundTrips 'Reverse travel/vehicle heading conversion must round-trip.' - -$equivalence = Invoke-ProbeHook 'EquivalenceTolerance' -Assert-True $equivalence.ExactThresholdsEquivalent ` - 'Equal 1e-9 L/c1/c2 and coordinate differences must deduplicate.' -Assert-True $equivalence.AboveAnyThresholdDistinct ` - 'A difference above any 1e-9 threshold must remain distinct.' -Assert-True $equivalence.HardNodeMismatchDistinct ` - 'Different hard-node sequences must remain distinct.' -Assert-True $equivalence.IntervalCountMismatchDistinct ` - 'Different interval counts must remain distinct.' -Assert-True $equivalence.FixedEquivalenceGridExact ` - 'The common grid must be exactly 0, 0.25, 0.5, 0.75, 1 per interval.' -Assert-True $equivalence.AliasRetainsIndependentDigestInput ` - 'A deduplicated key must retain its own hard nodes and candidate points.' -``` - -`Test-AllAttemptsCategorized` requires exactly one terminal attempt record for every started combination index `1..CombinationAttemptCount`; permitted terminal outcomes are constructed, deduplicated, hard-node invalid, interval failure, candidate assembly failure, started-combination budget exhaustion, or probe invalid. `Test-StrictEvaluatorHash` compares the current evaluator SHA-256 with the Task 1 manifest. `Test-CandidateDigestDeduplication($candidateLedger,$attempts)` recomputes a digest from every entry's own retained candidate, requires one ledger entry for every `CandidateConstructed` or `NumericallyDeduplicated` terminal attempt and none for failed attempts, requires stable keys to be unique and canonical indices to be unique/dense, and requires each duplicate attempt and alias to name the same preserved canonical index. Equal content SHA-256 values on two canonical records do not themselves deduplicate or invalidate them; only the fixed continuous-curve equivalence contract may deduplicate. Each alias must keep a distinct stable key, its own digest input/digest, the exact fixed grid and measured proof, and a resolvable canonical evaluation source; the script separately constructs evaluator-call, eligibility, state-count, performance-count, and selection input arrays and asserts no alias stable key occurs in any of them. - -The script must emit one invariant JSON object per attempt/candidate, including stable key, digest, interval values, failures, strict result, actual strict stop gate, raw/candidate ranges when evaluated, and `not evaluated` for gates after strict early return. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -Mode Strict -``` - -Expected: FAIL because the builder signature, diagnostics, and curvature-domain scheduling do not exist. - -- [ ] **Step 3: Add immutable probe diagnostics** - -Diagnostics validate all finite/nonnegative fields in constructors and copy input lists into `ReadOnlyCollection`. Store both geometric and recovered vehicle `c0..c3`, `L`, seed, iteration/damping/integration counts, failure enum, pre/post errors, analytic extrema, and seam errors. `EquivalentCandidateIndex` is the dense emitted index only for `NumericallyDeduplicated` and is `-1` for every other outcome. Do not add public production contracts; these files exist only in the disposable repository. - -- [ ] **Step 4: Replace candidate scheduling in the disposable builder** - -Add `using System.Diagnostics;` and use this exact outer scheduling shape: - -```csharp -int maximumWindows = Math.Min(region.WindowVariants.Count, 12); -int combinationAttempts = 0; -int solverInvocations = 0; -bool stopScheduling = false; -for (int windowIndex = 0; windowIndex < maximumWindows && !stopScheduling; windowIndex++) -{ - LocalG2WindowVariant window = region.WindowVariants[windowIndex]; - for (int seedIndex = 0; seedIndex < 3; seedIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - if (candidates.Count >= Math.Min(options.MaximumCandidatesPerRegion, 12)) - { - MarkSearchStopped(windowIndex, seedIndex, - LocalG2ExcursionSearchStop.CandidateLimitReached, - "The constructed-candidate limit was reached."); - stopScheduling = true; - break; - } - if (combinationAttempts >= 36) - { - MarkSearchStopped(windowIndex, seedIndex, - LocalG2ExcursionSearchStop.CombinationBudgetExhausted, - "The window/seed combination limit was reached."); - stopScheduling = true; - break; - } - if (solverInvocations >= 96) - { - MarkSearchStopped(windowIndex, seedIndex, - LocalG2ExcursionSearchStop.SolverInvocationBudgetExhausted, - "The solver invocation limit was reached."); - stopScheduling = true; - break; - } - combinationAttempts++; - - if (!TryCreateHardBoundaryStates(originalSegment, region, window, - maximumVehicleCurvaturePerMeter, - out IReadOnlyList hardNodes, - out string hardNodeReason)) - { - RecordAttemptFailure(windowIndex, seedIndex, combinationAttempts, - LocalG2ClosureFailure.InvalidInput, hardNodeReason); - continue; - } - - int intervalCount = hardNodes.Count - 1; - if (intervalCount < 1 || intervalCount > 8) - { - MarkProbeInvalid(windowIndex, seedIndex, - "Hard-node interval count must be in [1,8]."); - stopScheduling = true; - break; - } - if (solverInvocations + intervalCount > 96) - { - MarkSearchStopped(windowIndex, seedIndex, - LocalG2ExcursionSearchStop.SolverInvocationBudgetExhausted, - "The remaining solver budget cannot cover this combination."); - stopScheduling = true; - break; - } - - var solutions = new List(intervalCount); - var intervalDiagnostics = new List(intervalCount); - bool solved = true; - for (int intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++) - { - solverInvocations++; - long solverStarted = Stopwatch.GetTimestamp(); - bool intervalAccepted = _poseClosureSolver.TrySolve( - hardNodes[intervalIndex], hardNodes[intervalIndex + 1], - options.MaximumDeviationMeters, maximumVehicleCurvaturePerMeter, - seedIndex, cancellationToken, out LocalG2PoseClosureSolution solution, - out LocalG2PoseClosureDiagnostic solverDiagnostic, - out LocalG2ClosureFailure failure, out string solveReason); - long solverElapsed = Stopwatch.GetTimestamp() - solverStarted; - LocalG2ExcursionIntervalDiagnostic intervalDiagnostic = - CreateIntervalDiagnostic(intervalIndex, DirectionSign(originalSegment.Direction), - hardNodes[intervalIndex], hardNodes[intervalIndex + 1], - solution, solverDiagnostic, failure, solveReason, - solverElapsed, Stopwatch.Frequency); - intervalDiagnostics.Add(intervalDiagnostic); - if (!intervalAccepted) - { - RecordIntervalFailure(windowIndex, seedIndex, combinationAttempts, - intervalIndex, failure, solveReason, intervalDiagnostics); - solved = false; - break; - } - solutions.Add(solution); - } - if (!solved) continue; - - if (TryCreateCandidateFromSolutions(originalSegment, region, window, - windowIndex, seedIndex, combinationAttempts, candidates.Count, - hardNodes, solutions, intervalDiagnostics, - outputSpacingMeters, solverInvocations, - out LocalG2CandidateGeometry candidate, - out LocalG2ExcursionCandidateDiagnostic diagnostic, - out string candidateReason)) - { - if (!TryAddIfNumericallyNew(candidates, candidate, diagnostic, - cancellationToken, out string equivalenceReason)) - { - MarkProbeInvalid(windowIndex, seedIndex, equivalenceReason); - stopScheduling = true; - break; - } - } - else - { - RecordCandidateFailure(windowIndex, seedIndex, combinationAttempts, - diagnostic, candidateReason); - } - } -} -``` - -The method publishes `LastProbeDiagnostic` from one `finally`-style exit path after both loops. `RecordAttemptFailure`, `MarkProbeInvalid`, `MarkSearchStopped`, `RecordIntervalFailure`, `RecordCandidateFailure`, and `TryAddIfNumericallyNew` populate that immutable snapshot; none may silently discard a failed attempt. `TryAddIfNumericallyNew` applies the Task 3 equivalence contract below, preserves the first numerical solution, records later equivalent stable keys as deduplicated ledger entries, and keeps emitted `candidateIndex` values dense in combination order. - -`DirectionSign` returns `+1` for `TravelDirection.Forward`, `-1` for `TravelDirection.Reverse`, and throws `ArgumentOutOfRangeException` otherwise. Every completed solver invocation appends its interval diagnostic before success/failure branching; a failed combination therefore retains all successful prefix intervals plus the failed interval, including elapsed ticks and exact exit diagnostics. `SolverPhaseElapsedMilliseconds` is the sum of `SolverElapsedStopwatchTicks * 1000.0 / StopwatchFrequency` over all invocation diagnostics, not a second independently rounded measurement. - -Implement numerical equivalence here with the immutable common grid `u={0,0.25,0.5,0.75,1}` for every corresponding interval. Require identical hard-node count/content and interval count; for every interval require `|delta L|`, `|delta c1|`, and `|delta c2|` each `<=1e-9`; then evaluate both stored immutable curves at all five identical `u` values and require maximum Euclidean coordinate difference `<=1e-9 m`. Check cancellation before every interval and grid point. Equality at the threshold deduplicates; any field above it remains distinct. A curve-evaluation/integration failure returns `false` with a reason and makes the run `PROBE_INVALID`, not “distinct.” The Task 3 `EquivalenceTolerance` hook asserts the exact grid and boundary behavior. - -Every successfully assembled combination is appended to `CandidateLedger` before canonical/deduplicated branching. A canonical entry retains its full candidate and has `IsCanonical=true`, `CanonicalCandidateIndex=candidate.CandidateIndex`, grid `{0,0.25,0.5,0.75,1}`, and maximum equivalence coordinate difference `0`. A deduplicated entry retains its own full candidate points/hard nodes and independent digest input, has `IsCanonical=false`, points to the preserved canonical dense index, and stores the measured maximum grid difference. The builder returns only canonical candidates to the pipeline; ledger aliases do not increment `CandidateCount`. - -Keep the current hard-node construction, interpolation, direction-sign handling, distance-weighted shared curvature, reference body-clearance lookup, source marker, and dense candidate indices. Convert each normalized vehicle heading to normalized travel heading. Set the first unwrapped travel heading to that normalized value; for every later node use `currentUnwrapped = previousUnwrapped + AngleMath.ShortestSignedDifference(AngleMath.NormalizeRadians(previousUnwrapped), currentTravelNormalized)`. Pass those unwrapped travel headings to the solver. For each solved interval, sample `count=max(1,ceil(L/outputSpacingMeters))`; emit the shared hard node once; use exact anchors only after pre-overwrite closure passes. - -- [ ] **Step 5: Pass the physical curvature limit from the pipeline** - -Change the sole production-path call in the disposable `LocalG2PreSmoothingPipeline` to: - -```csharp -IReadOnlyList candidates = _builder.Build( - preparedPath.Segments[region.SegmentIndex], region, - request.Configuration.OutputSpacingMeters, maximumCurvature, - options, cancellationToken); -``` - -Update disposable test-hook/reflection calls with explicit finite positive curvature limits. Do not modify `LocalG2CandidateEvaluator.cs` in the core. - -- [ ] **Step 6: Implement the complete audit/final-gate script in the disposable core** - -Implement all three command modes, the Task 4 child JSON schema, child-process pairing, timing capture, state resolver/table tests, and report generation now; Task 3 requires only `Strict` to pass, while `Measurement` intentionally remains RED until the isolated evaluator patch. The candidate digest input uses UTF-8 invariant round-trip text, one field per line, in this exact order: - -The script owns one `Get-ProbeSourceManifest -Root ` helper. It enumerates the current filesystem only beneath explicit safe paths: root `.gitattributes`, `ClumsyPilot/ClumsyPilot.csproj`, every `.cs` file below `ClumsyPilot/ParkrobTrajplanner/PathSmoothing` except `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs`, the fixture `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json`, and exactly the three probe verifier scripts created by Tasks 1–3. This allows Task 3's pre-commit GREEN run to hash its TDD worktree; the frozen post-commit run and both Task 4 roots additionally require clean Git status, so their manifest bytes are bound to the recorded commits. The helper rejects any path containing a tab/newline or `auto_avoidance`, requires all seven categories to be nonempty, requires `.gitattributes` raw text to be exactly `* -text` plus one LF, normalizes separators to `/`, sorts with `StringComparer.Ordinal`, and hashes each file's raw bytes with lowercase SHA-256. The canonical manifest text is one `relative/pathlowercase-sha256` line per entry including a final LF, encoded as UTF-8 without BOM; `ProbeSourceManifestSha256` is the lowercase SHA-256 of those exact bytes. The evaluator is intentionally excluded because its strict/measurement hashes and complete diff are audited separately. Every single/batch child recomputes this manifest from its own root; when `EvidenceRoot` is supplied it also writes the canonical text to `-probe-source-manifest.txt` and records that full path beside the hash. - -Use this exact canonicalization core inside the helper: - -```powershell -$paths = [string[]]@($sourcePaths | ForEach-Object { $_.Replace('\','/') }) -[Array]::Sort($paths, [StringComparer]::Ordinal) -$entries = foreach ($relativePath in $paths) { - if ($relativePath.IndexOf("`t", [StringComparison]::Ordinal) -ge 0 -or - $relativePath.IndexOf("`n", [StringComparison]::Ordinal) -ge 0 -or - $relativePath.IndexOf('auto_avoidance', [StringComparison]::Ordinal) -ge 0) { - throw "Illegal probe-manifest path: $relativePath" - } - $absolutePath = Join-Path $Root ($relativePath.Replace('/', [IO.Path]::DirectorySeparatorChar)) - $fileSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $absolutePath).Hash.ToLowerInvariant() - $relativePath + "`t" + $fileSha -} -$canonicalText = [string]::Join("`n", [string[]]$entries) + "`n" -$utf8NoBom = New-Object Text.UTF8Encoding($false) -$manifestBytes = $utf8NoBom.GetBytes($canonicalText) -$sha256 = [Security.Cryptography.SHA256]::Create() -try { $manifestSha256 = -join ($sha256.ComputeHash($manifestBytes) | ForEach-Object { $_.ToString('x2') }) } -finally { $sha256.Dispose() } -``` - -`$sourcePaths` is built from the exact six explicit files plus a recursive `*.cs` enumeration rooted only at `ClumsyPilot/ParkrobTrajplanner/PathSmoothing`; it never enumerates the `ParkrobTrajplanner` parent. Convert every absolute result back to a root-relative path and apply the exact evaluator exclusion before canonicalization. A missing file, duplicate normalized path, empty manifest, category-count failure, EOL-policy mismatch, `core.autocrlf` other than `false`, or final-evidence dirty Git status aborts the run as `PROBE_INVALID` evidence. - -```powershell -function Get-CandidateLedgerDigest { -param($ledgerEntry, $preparedSegment) -$candidate = Get-InternalProperty $ledgerEntry 'Candidate' -$culture = [Globalization.CultureInfo]::InvariantCulture -$builder = [Text.StringBuilder]::new() -[void]$builder.AppendLine(([int]$preparedSegment.Direction).ToString($culture)) -[void]$builder.AppendLine(([int](Get-InternalProperty $candidate 'SegmentIndex')).ToString($culture)) -[void]$builder.AppendLine(([double](Get-InternalProperty $candidate 'StartArcLengthMeters')).ToString('R', $culture)) -[void]$builder.AppendLine(([double](Get-InternalProperty $candidate 'EndArcLengthMeters')).ToString('R', $culture)) -$diagnostic = Get-InternalProperty $candidate 'ExcursionDiagnostic' -$directionSign = [int](Get-InternalProperty $diagnostic 'DirectionSign') -if ($directionSign -ne 1 -and $directionSign -ne -1) { throw 'Invalid direction sign.' } -$hardNodes = @(Get-InternalProperty $diagnostic 'HardNodes') -[void]$builder.AppendLine($hardNodes.Count.ToString($culture)) -foreach ($node in $hardNodes) { - [void]$builder.AppendLine(([double](Get-InternalProperty $node 'X')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $node 'Y')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $node 'ReferenceArcLengthMeters')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $node 'TravelUnwrappedHeadingRadians')).ToString('R', $culture)) - $geometricCurvature = [double](Get-InternalProperty $node 'GeometricCurvaturePerMeter') - [void]$builder.AppendLine($geometricCurvature.ToString('R', $culture)) - [void]$builder.AppendLine(($directionSign * $geometricCurvature).ToString('R', $culture)) -} -$regionPoints = @(Get-InternalProperty $candidate 'RegionPoints') -[void]$builder.AppendLine($regionPoints.Count.ToString($culture)) -foreach ($point in $regionPoints) { - [void]$builder.AppendLine(([double](Get-InternalProperty $point 'X')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $point 'Y')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $point 'ArcLength')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $point 'Heading')).ToString('R', $culture)) - [void]$builder.AppendLine(([double](Get-InternalProperty $point 'UnwrappedHeading')).ToString('R', $culture)) - [void]$builder.AppendLine(([int](Get-InternalProperty $point 'Source')).ToString($culture)) -} -$bytes = [Text.Encoding]::UTF8.GetBytes($builder.ToString()) -$hash = [Security.Cryptography.SHA256]::Create().ComputeHash($bytes) -$digest = -join ($hash | ForEach-Object { $_.ToString('x2') }) -return $digest -} -``` - -Iterate `LastProbeDiagnostic.CandidateLedger`, not only the builder's returned canonical list, call `Get-CandidateLedgerDigest $ledgerEntry $preparedSegment`, and compute this digest independently from every ledger entry's own retained candidate. The stable key is `single-turn/s/r/w/seed` with every index zero-based. The atomic record also stores each hard node's recovered vehicle curvature (`directionSign * geometricCurvature`) and fails closed if either geometric or recovered value is non-finite. - -Canonical records use `RecordKind=Canonical`, run the evaluator directly, and may enter state counts/diagnostic selection. Deduplicated records use `RecordKind=DeduplicatedAlias`, retain their independent stable key/content digest in both roots, store the exact equivalence grid/proof and `EvaluationSourceStableKey` of the canonical record, and reuse that canonical evaluator result only as annotated evidence. Aliases are never counted in `CandidateCount`, `StrictQualifiedCount`, `PhysicalCandidateCount`, `ExcursionQualifiedRecords`, performance evaluator-call counts, or minimum-excursion selection. Missing canonical references, alias cycles, cross-root alias digest/proof mismatch, or a canonical source that was not directly evaluated makes the probe invalid. - -Every child atomic record therefore contains `StableKey`, `CandidateSha256`, `RecordKind`, `CanonicalCandidateIndex`, `EvaluationSourceStableKey`, `EquivalenceParameters`, `MaximumEquivalenceCoordinateDifferenceMeters`, `EvaluatorExecutedDirectly`, evaluator result/stop-gate fields, and the interval/candidate diagnostics. For canonical records, `EvaluationSourceStableKey=StableKey`, `EvaluatorExecutedDirectly=true`, and the equivalence difference is `0`. For aliases, `EvaluationSourceStableKey` resolves to a canonical record, `EvaluatorExecutedDirectly=false`, and evaluator result/stop-gate fields are copied from that source only after the reference audit succeeds. - -- [ ] **Step 7: Run strict GREEN twice and commit the frozen core** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -Mode Strict -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -Mode Strict -git diff --check -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -git commit -m 'feat: integrate bounded Local G2 excursion candidates' -$coreCommit = (git rev-parse HEAD).Trim() -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 ` - -Mode Strict -EvidenceRoot $probeEvidenceRoot -RunLabel frozen-core -$frozenCoreJsonPath = Join-Path $probeEvidenceRoot 'frozen-core.json' -$frozenCoreManifestPath = Join-Path $probeEvidenceRoot 'frozen-core-probe-source-manifest.txt' -if (!(Test-Path -LiteralPath $frozenCoreJsonPath) -or !(Test-Path -LiteralPath $frozenCoreManifestPath)) { - throw 'Frozen core evidence is incomplete.' -} -$frozenCoreJson = Get-Content -Raw -Encoding UTF8 -LiteralPath $frozenCoreJsonPath | ConvertFrom-Json -$manifestFileSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $frozenCoreManifestPath).Hash.ToLowerInvariant() -if ($frozenCoreJson.Identity.CoreCommit -ne $coreCommit -or - $frozenCoreJson.Identity.RootCommit -ne $coreCommit -or - $frozenCoreJson.Identity.ProbeSourceManifestSha256 -ne $manifestFileSha) { - throw 'Frozen core identity or probe-source manifest hash mismatch.' -} -``` - -Expected: all structural assertions pass. This task does not require a particular feasibility outcome; strict candidate results are evidence for Task 4. Record the frozen core commit, evaluator hash, canonical probe-source manifest path/hash, and frozen-core JSON path. Do not edit the core after review. - ---- - -### Task 4: Build strict/measurement roots and execute the exhaustive feasibility gate - -**Files:** - -- Modify only in measurement clone: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` -- Use unchanged in both clones: `ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1` -- Generate without committing: `.superpowers/sdd/local-g2-minimum-curvature-excursion-feasibility-report.md` -- Record without committing: `.superpowers/sdd/local-g2-excursion-task-4-report.md` - -**Interfaces:** - -- Consumes the frozen Task 3 core commit. -- Produces two roots with a shared parent commit, one measurement-only evaluator commit, per-candidate `CandidateAuditRecord` JSON, 5+30 timing data, one exhaustive state, and a generated Markdown report. - -The measurement evaluator adds only private trace storage plus this disposable reflection snapshot: - -```csharp -public sealed class MeasurementTraceSnapshot -{ - public int CandidateIndex { get; } - public double? RawMinimumCurvature { get; } - public double? RawMaximumCurvature { get; } - public double? CandidateMinimumCurvature { get; } - public double? CandidateMaximumCurvature { get; } - public double? Eminus { get; } - public double? Eplus { get; } - public bool? RawRangeExceeded { get; } - public double? VehicleMaximumCurvatureLimitPerMeter { get; } - public double? CandidateMaximumAbsoluteVehicleCurvaturePerMeter { get; } - public double? MaximumDeviationMeters { get; } - public double? MaximumDeviationLimitMeters { get; } - public bool? CollisionAndMapBoundaryPassed { get; } - public double? MinimumBodyClearanceMeters { get; } - public double? MinimumClearanceReserveMeters { get; } - public double? RawPeakCurvatureDerivativePerSquareMeter { get; } - public double? CandidatePeakCurvatureDerivativePerSquareMeter { get; } - public double? MinimumPeakGradientImprovementRatio { get; } - public double? RawCurvatureVariationCost { get; } - public double? CandidateCurvatureVariationCost { get; } - public double? MaximumVariationCostRegressionRatio { get; } - public double? AbsolutePathLengthChangeMeters { get; } - public IReadOnlyList ExecutedGates { get; } - public IReadOnlyList PassedGates { get; } - public string StopGate { get; } - public string Result { get; } - public string Reason { get; } -} - -private static class LocalG2ExcursionMeasurementTrace -{ - internal static void Begin(int candidateIndex); - internal static void RecordGate(string gate, bool passed); - internal static void RecordRawRange(int candidateIndex, - double rawMinimumCurvature, double rawMaximumCurvature, - double candidateMinimumCurvature, double candidateMaximumCurvature, - bool rawRangeExceeded); - internal static void RecordVehicleCurvature(double? vehicleLimit, - double? candidateMaximumAbsoluteVehicleCurvature, bool passed); - internal static void RecordDeviation(double? maximumDeviation, - double maximumDeviationLimit, bool passed); - internal static void RecordCollisionAndMapBoundary(bool passed, - double? minimumBodyClearance, string reason); - internal static void RecordClearance(double? minimumBodyClearance, - double minimumClearanceReserve, bool passed); - internal static void RecordPeakGradient(double rawPeak, double candidatePeak, - double minimumImprovementRatio, bool passed); - internal static void RecordVariationCost(double rawCost, double candidateCost, - double maximumRegressionRatio, bool passed); - internal static void RecordRawFullPathAnalysis( - double? absolutePathLengthChangeMeters, bool passed); - internal static void Complete(string stopGate, - PathSmoothingRegionFailureReason result, bool accepted, string reason); - internal static MeasurementTraceSnapshot Capture(); -} - -// Add this method to the existing public static TestHooks class. -public static MeasurementTraceSnapshot CaptureLastMeasurementTrace(); -``` - -`Begin` replaces the previous candidate trace at the first line of `Evaluate`. Store the trace in a `[ThreadStatic]` field because the probe is sequential. Use these gate names in this order: `InputAnalysis`, `VehicleCurvature`, `RawCurvatureRange`, `Deviation`, `FullPathAnalysis`, `CollisionAndMapBoundary`, `Clearance`, `PeakGradientImprovement`, `VariationCost`, `RawFullPathAnalysis`, `Accepted`. Each metric-specific recorder atomically stores its operands and appends its named gate to `ExecutedGates`; it appends the gate to `PassedGates` only when `passed=true`. `RecordRawRange` similarly always appends `RawCurvatureRange` to executed and appends it to passed only when `rawRangeExceeded=false`; when exceeded, the measurement clone records a failed raw-range gate and continues. Nullable recorder operands are `null` only when the underlying computation was unavailable or non-finite; never serialize a non-finite double or substitute zero. On validator failure, `RecordCollisionAndMapBoundary(false,null,reason)` records the exact existing reason and leaves clearance `null`; on validator success it records the returned finite clearance, then `RecordClearance` records the reserve and pass/fail result. `RecordPeakGradient` and `RecordVariationCost` preserve both raw/candidate operands and configured ratios even on their rejection branches; the script derives improvement/regression percentages and leaves a ratio `null` when its denominator is zero. `RecordRawFullPathAnalysis(null,false)` is used when raw full-path analysis fails; on success it records the absolute path-length change. Use plain `RecordGate` only for `InputAnalysis`, `FullPathAnalysis`, and `Accepted`. Call the appropriate recorder immediately before every rejection and after each success, and call `Complete` before every return. `Capture` returns an immutable copy, and `CaptureLastMeasurementTrace` only forwards that copy. The probe script must call `Evaluate` for one candidate, immediately capture its trace before evaluating the next candidate, and store the copy in that candidate's atomic record. It then separately invokes the full pipeline once per run to record the probe `Complete` status; it must not infer per-candidate traces from the last full-pipeline evaluation. `Eminus=max(0,rawMinimum-candidateMinimum)` and `Eplus=max(0,candidateMaximum-rawMaximum)`. - -`Complete` sets `Result` to the exact string `Accepted` when `accepted=true`; otherwise it sets `Result=result.ToString()`. This mapping is invariant across processes and is part of the determinism signature. -Nullable range/excursion fields remain `null` until `RecordRawRange`; JSON preserves `null`, and the Markdown renderer writes it as `not evaluated`, never `0`. - -- [ ] **Step 1: Clone the frozen core twice and prove source identity** - -```powershell -$strictRoot = Join-Path $probeRoot 'strict-root' -$measureRoot = Join-Path $probeRoot 'measurement-root' -$evidenceRoot = Join-Path $probeRoot 'evidence' -New-Item -ItemType Directory -Path $evidenceRoot -Force | Out-Null -git -c core.autocrlf=false clone $coreRoot $strictRoot -git -c core.autocrlf=false clone $coreRoot $measureRoot -git -C $strictRoot config core.autocrlf false -git -C $measureRoot config core.autocrlf false -$coreCommit = (git -C $coreRoot rev-parse HEAD).Trim() -if ((git -C $strictRoot rev-parse HEAD).Trim() -ne $coreCommit) { throw 'Strict clone drift.' } -if ((git -C $measureRoot rev-parse HEAD).Trim() -ne $coreCommit) { throw 'Measurement clone drift.' } -if (Test-Path (Join-Path $strictRoot 'ClumsyPilot\ParkrobTrajplanner\auto_avoidance')) { throw 'Strict root contains excluded source.' } -if (Test-Path (Join-Path $measureRoot 'ClumsyPilot\ParkrobTrajplanner\auto_avoidance')) { throw 'Measurement root contains excluded source.' } -if ((Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $strictRoot '.gitattributes')) -ne "* -text`n") { throw 'Strict root EOL policy drift.' } -if ((Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $measureRoot '.gitattributes')) -ne "* -text`n") { throw 'Measurement root EOL policy drift.' } -if ((git -C $strictRoot config --get core.autocrlf).Trim() -ne 'false') { throw 'Strict root autocrlf drift.' } -if ((git -C $measureRoot config --get core.autocrlf).Trim() -ne 'false') { throw 'Measurement root autocrlf drift.' } -``` - -Hash shared snapshot, core, strict, and measurement evaluators before the measurement patch. All four hashes must match. - -Load `$probeEvidenceRoot/frozen-core.json` and `$probeEvidenceRoot/frozen-core-probe-source-manifest.txt` from Task 3. Require the JSON core/root commit to equal `$coreCommit`, require the canonical file's raw-byte SHA-256 to equal `Identity.ProbeSourceManifestSha256`, and retain that value as `$expectedProbeSourceManifestSha256`. Later strict and measurement children must each recompute exactly this value from their own clean filesystem root; because the evaluator is excluded, the measurement evaluator commit does not change it. - -- [ ] **Step 2: Write the measurement RED assertion before changing the evaluator** - -Run the unchanged probe in measurement mode: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 ` - -Mode Measurement -CoreCommit $coreCommit -``` - -Expected: FAIL with exactly `Measurement evaluator hook is unavailable`. It must not silently treat the strict early return as measurement success. - -- [ ] **Step 3: Add the only permitted measurement behavior** - -In measurement-root evaluator, preserve the existing vehicle gate, compute `rawRangeExceeded`, record it, and reject only when not in the measurement-only clone. Because this source exists only in that clone, bind it as: - -```csharp -private const bool MeasureRawCurvatureExcursion = true; -``` - -Replace only the existing raw-range return with: - -```csharp -bool rawRangeExceeded = ExceedsRawCurvatureRange( - candidateAnalysis.Path, rawMinimumCurvature, rawMaximumCurvature); -GetCurvatureRange(candidateAnalysis.Path, - out double candidateMinimumCurvature, out double candidateMaximumCurvature); -LocalG2ExcursionMeasurementTrace.RecordRawRange( - candidateIndex, rawMinimumCurvature, rawMaximumCurvature, - candidateMinimumCurvature, candidateMaximumCurvature, rawRangeExceeded); -if (rawRangeExceeded && !MeasureRawCurvatureExcursion) - return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureOvershoot, - "局部 G2 候选超出原始区域曲率范围。"); -``` - -Add the exact trace calls above immediately after each metric becomes available, after each later gate is actually passed, and at each later failure return. In particular, do not read quality metrics from `Rejected(...)`, because that helper replaces unavailable values with zeros: copy the live evaluator locals into the metric recorders before returning. Trace state is reset for every candidate and is only exposed through the existing nested reflection `TestHooks`; it must not change return values, ordering, analysis inputs, validator calls, or selection. - -- [ ] **Step 4: Run measurement tests and commit only the evaluator in the measurement clone** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 ` - -Mode Measurement -CoreCommit $coreCommit -git diff --check -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs -git commit -m 'test: measure Local G2 raw curvature excursion' -``` - -Expected: the script reaches every later gate for candidates that pass earlier gates and emits execution flags. The commit must contain exactly one file. - -The measurement-mode script must also exercise the existing deterministic evaluator scenarios by calling `LocalG2CandidateEvaluator.TestHooks.Execute(scenario)` and immediately calling `CaptureLastMeasurementTrace()` after each call. Assert: `TooFar` records finite deviation and its `0.10 m` limit before rejecting `Deviation`; `LowClearance` records finite checked clearance and the `0.02 m` reserve before rejecting `Clearance`; `NoOp` records raw/candidate peak-gradient operands plus the `0.20` ratio before rejecting `PeakGradientImprovement`; `Oscillating` records raw/candidate variation costs plus the `0.02` ratio before rejecting `VariationCost`; `Improved` records every metric field, all named gates, and a finite absolute path-length change before `Accepted`; and `Overshoot` records `RawCurvatureRange` as executed-but-not-passed while proving at least the next `Deviation` gate executed. For each early rejection, all later metric fields remain `null` and render `not evaluated`, not zero. - -- [ ] **Step 5: Produce atomic strict/measurement candidate records** - -Run strict and measurement roots sequentially and compare by stable key. For every key require identical candidate SHA-256; pair strict result, measurement result, stop gate, execution flags, raw/candidate ranges, `Eminus/Eplus`, improvement, variation, deviation, clearance, and elapsed metrics in one record. - -The strict side qualifies an excursion pair only when its result is exactly `CurvatureOvershoot`. `CurvatureExceeded`, `CandidateGenerationFailed`, any earlier result, digest mismatch, or missing gate flag cannot support `FEASIBLE_WITH_EXCURSION`. - -Derive `StrictStopGate` without modifying the strict evaluator: `Accepted -> Accepted`, `CurvatureExceeded -> VehicleCurvature`, `CurvatureOvershoot -> RawCurvatureRange`, `DeviationExceeded -> Deviation`, `Collision -> CollisionAndMapBoundary`, `InsufficientClearance -> Clearance`, `InsufficientImprovement -> PeakGradientImprovement`, and `VariationCostRegression -> VariationCost`. For `CandidateGenerationFailed`, use the paired measurement trace's `InputAnalysis`, `VehicleCurvature`, `FullPathAnalysis`, or `RawFullPathAnalysis` stop gate only when the trace either stopped before raw-range evaluation or recorded `RawRangeExceeded=false`; if it recorded an exceeded raw range, the unchanged strict result should have been `CurvatureOvershoot`, so the pair is invalid. No reason-string guessing is permitted. - -Every single-run `Strict` or non-batch `Measurement` JSON uses schema version 1 and contains these exact top-level fields: - -```text -SchemaVersion -Mode -RunLabel -Environment { MachineName, CpuName, LogicalProcessorCount, TotalMemoryBytes, OsDescription, - PowerShellVersion, DotNetSdkVersion, BuildConfiguration, TargetFramework } -Identity { SharedBaselineCommit, CoreCommit, RootCommit, FixtureSha256, - EvaluatorSha256, ProbeSourceManifestSha256 } -ProbeRunDiagnostic -CandidateRecords -PipelineStatus -PipelineSelectedStableKey -Timings { IntervalSolverElapsedMilliseconds[], SolverPhaseElapsedMilliseconds, - CandidateEvaluationElapsedMilliseconds[], CandidateEvaluationPhaseElapsedMilliseconds, - RegionElapsedMilliseconds, FixtureElapsedMilliseconds } -``` - -A `Measurement -Batch` JSON has exact top-level fields `SchemaVersion`, `Mode`, `RunLabel`, `Environment`, `Identity`, `WarmupRuns`, and `MeasuredRuns`. `WarmupRuns` has exactly five single-run payloads and `MeasuredRuns` exactly 30, both in execution order; nested payloads omit duplicate `Environment`/`Identity` but otherwise use the same `ProbeRunDiagnostic`, `CandidateRecords`, `PipelineStatus`, `PipelineSelectedStableKey`, and `Timings` fields. - -Before timing, compare the batch environment with the recorded baseline in the shared scratch file `.superpowers/sdd/local-g2-split-scale-feasibility-report.md`: machine `DESKTOP-428CCNK`, CPU `11th Gen Intel Core i5-1135G7 @ 2.40 GHz`, 8 logical processors, 16,863,318,016 RAM bytes, Windows 10 Pro `10.0.19045`, PowerShell `5.1.19041.7548`, .NET SDK `10.0.302`, and `Debug/netstandard2.0`. A mismatch sets `InvalidEvidence=true`; record both observed and expected metadata rather than silently changing the performance baseline. - -`IntervalSolverElapsedMilliseconds` records every successful or failed invocation in order. `SolverPhaseElapsedMilliseconds` covers all closure calls in the region. Candidate evaluation timings record every evaluator call and their sum. `RegionElapsedMilliseconds` covers candidate construction, all candidate evaluations, selection, and region result. `FixtureElapsedMilliseconds` covers the full `SingleTurn` probe run after assembly/fixture setup. All timing fields use `Stopwatch.GetTimestamp`, are finite and nonnegative, and exclude process launch/build. FinalGate additionally reports strict-child, measurement-child, pairing, and two-root total diagnostic wall time separately; these diagnostic totals never enter the four performance gates. - -For every paired atomic record compute: - -```text -denominator = max(rawMaximum - rawMinimum, - AbsoluteCurvatureJumpFloorPerMeter) -Rminus = Eminus / denominator -Rplus = Eplus / denominator -analyticVehicleCurvatureUtilization = max(abs(c0),abs(c1),abs(c2),abs(c3)) / Kvehicle -discreteVehicleCurvatureUtilization = candidateMaximumAbsoluteVehicleCurvature / Kvehicle -``` - -The record contains `HardPhysicalQualified`, which is true only when every interval passes hard closure/seam/nondegeneracy checks, all four continuous controls pass the physical curvature range, and the unchanged evaluator actually passes its discrete vehicle-curvature gate. Missing or non-finite operands make the record invalid, never zero. - -`New-PairedObservation -StrictAudit -MeasurementRun ` performs this deterministic join: validate schema/identity/source hashes, including requiring both `ProbeSourceManifestSha256` values to equal `$expectedProbeSourceManifestSha256` and requiring each written canonical manifest file to be raw-byte identical to the frozen-core manifest; require the same ordered stable-key set; for each key require equal candidate digest, `RecordKind`, canonical index, evaluation-source key, fixed equivalence grid, and equivalence proof before copying both child records into one `CandidateAuditRecord`; compute excursion/utilization fields for canonical records only; verify every required executed-gate flag; derive `HardPhysicalQualified`; retain both roots' pipeline statuses and selected keys; sort atomic records by stable key; then populate `InvalidEvidence`, `StrictQualifiedCount`, `PhysicalCandidateCount`, and `ExcursionQualifiedRecords` from canonical records only. It returns a new object and never mutates either child JSON. It also proves each alias points to one directly evaluated canonical record, rejects self-reference/cycles, and proves aliases never enter evaluator-call/performance/eligibility/selection counts. A missing/duplicate key, digest or alias-proof mismatch, probe-manifest mismatch, non-finite required number, missing flag, or `Complete` pipeline selection that names no canonical atomic record sets `InvalidEvidence=true` and records the exact reason list. Strict and measurement selected keys may differ because measurement can admit additional raw-range-excursion candidates; both keys remain determinism evidence but do not filter otherwise accepted per-candidate canonical records. - -- [ ] **Step 6: Execute the fixed 5+30 performance and determinism protocol** - -```powershell -function Invoke-ProbeChild { - param( - [Parameter(Mandatory=$true)][string]$Root, - [Parameter(Mandatory=$true)][ValidateSet('Strict','Measurement')][string]$ChildMode, - [Parameter(Mandatory=$true)][string]$ChildRunLabel, - [Parameter(Mandatory=$true)][string]$CoreCommit, - [switch]$ChildBatch - ) - $scriptPath = Join-Path $Root 'ClumsyPilot\tests\verify_path_smoothing_local_g2_excursion_probe.ps1' - $jsonPath = Join-Path $EvidenceRoot ($ChildRunLabel + '.json') - $childArgs = @('-ExecutionPolicy','Bypass','-File',$scriptPath,'-Mode',$ChildMode, - '-EvidenceRoot',$EvidenceRoot,'-RunLabel',$ChildRunLabel,'-CoreCommit',$CoreCommit) - if ($ChildBatch) { $childArgs += '-Batch' } - & powershell @childArgs - if ($LASTEXITCODE -ne 0) { throw "$ChildMode child failed for $ChildRunLabel." } - if (!(Test-Path -LiteralPath $jsonPath)) { throw "Missing child JSON: $jsonPath" } - return Get-Content -Raw -Encoding UTF8 -LiteralPath $jsonPath | ConvertFrom-Json -} - -$strictAudit = Invoke-ProbeChild -Root $StrictRoot -ChildMode Strict ` - -ChildRunLabel 'strict-audit' -CoreCommit $coreCommit -$measurementBatch = Invoke-ProbeChild -Root $MeasurementRoot -ChildMode Measurement ` - -ChildRunLabel 'measurement-batch' -CoreCommit $coreCommit -ChildBatch -if (@($measurementBatch.WarmupRuns).Count -ne 5) { throw 'Measurement batch must contain 5 warm-ups.' } -if (@($measurementBatch.MeasuredRuns).Count -ne 30) { throw 'Measurement batch must contain 30 measured runs.' } -$warmups = @($measurementBatch.WarmupRuns | ForEach-Object { - New-PairedObservation -StrictAudit $strictAudit -MeasurementRun $_ -}) -$runs = @($measurementBatch.MeasuredRuns | ForEach-Object { - New-PairedObservation -StrictAudit $strictAudit -MeasurementRun $_ -}) -$measurementAudit = $runs[0] -``` - -The measurement batch child loads its assembly and fixture exactly once, then runs 5+30 serial iterations with the same immutable `SingleTurn` request and `CancellationToken.None` in that process; each iteration constructs fresh builder/evaluator/pipeline instances so only JIT/runtime warming persists. The strict child is a separate process so same-name assemblies never share a loader. `New-PairedObservation` creates atomic records, marks `InvalidEvidence` on identity/key/digest mismatch, and determines `HardPhysicalQualified`. When evidence is valid it runs the Step 7 underlying-state function with the fixed strict audit plus that measurement run; when invalid it sets paired state to `PROBE_INVALID` and leaves selection `not evaluated`. It returns `PairedState`, `UnderlyingStateWithoutPerformanceGate`, and `SelectedStableKey` beside the measurement timings. Thus a Measurement child never invents a final state by itself. - -Build each measured determinism signature by sorting paired atomic records by stable key and hashing invariant JSON containing only key, candidate SHA-256, `RecordKind`, canonical index, evaluation-source key, equivalence grid/proof, direct-evaluation flag, strict/measurement results, stop gates, execution flags, interval diagnostics, `PairedState`, `UnderlyingStateWithoutPerformanceGate`, and `SelectedStableKey`—never elapsed values or file paths. Assert all 30 signatures match and retain all five warm-up records plus all 30 measured records in evidence. Sort the 30 `SolverPhaseElapsedMilliseconds` values and the 30 `RegionElapsedMilliseconds` values separately; for each, use the mean of elements 14 and 15 (zero-based) as the median and `Measure-Object -Maximum` as worst. Apply all four gates: solver median/worst `<=10/25 ms`, region median/worst `<=25/50 ms`. Also report every per-invocation, evaluator-phase, fixture, and two-root diagnostic timing without applying additional pass/fail thresholds. - -- [ ] **Step 7: Apply the frozen, exhaustively tested ordered state machine** - -Create one observation object after pairing. `InvalidEvidence` is true for any design section 11.1 condition: unknown commits/root/fixture, source-diff violation, strict evaluator mismatch, key/digest mismatch, missing or non-finite required data, missing gate flags/diffs/repeat evidence, infrastructure failure, cancellation, or unequal 30-run signatures. `PerformanceExceeded` is true when any of the four fixed median/worst limits fails. `StrictQualifiedCount` counts atomic records whose hard/physical gates pass, whose strict evaluator and measurement evaluator both accept all unchanged gates, and whose strict and measurement full pipelines both report probe `Complete`; per-candidate direct acceptance is the qualification, while each pipeline's selected key is retained only as audit/determinism evidence. `PhysicalCandidateCount` counts `HardPhysicalQualified` records. `ExcursionQualifiedRecords` contains all hard/physical records with strict result exactly `CurvatureOvershoot`, measurement result `Accepted`, every later gate executed and passed, measurement pipeline status `Complete`, valid source identity, and `Eminus>0` or `Eplus>0`; do not discard an accepted candidate merely because the pipeline selected another accepted candidate. - -The unchanged Task 3 script already contains and table-tests these exact functions; execute them against the paired evidence: - -```powershell -function Resolve-UnderlyingStateWithoutPerformanceGate { - param($Observation) - if ($Observation.StrictQualifiedCount -gt 0) { return 'STRICT_GREEN' } - if ($Observation.PhysicalCandidateCount -eq 0) { - return 'NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET' - } - if (@($Observation.ExcursionQualifiedRecords).Count -eq 0) { return 'QUALITY_RED' } - return 'FEASIBLE_WITH_EXCURSION' -} - -function Resolve-FinalState { - param($Observation) - if ($Observation.InvalidEvidence) { return 'PROBE_INVALID' } - $underlying = Resolve-UnderlyingStateWithoutPerformanceGate $Observation - if ($Observation.PerformanceExceeded) { return 'BUDGET_RED' } - return $underlying -} -``` - -Before using real observations, table-test: each of the six states alone; `InvalidEvidence+PerformanceExceeded -> PROBE_INVALID`; performance failure plus a strict candidate `-> BUDGET_RED`; strict-qualified plus excursion-qualified `-> STRICT_GREEN`; physical zero `-> NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET`; physical positive plus zero excursion `-> QUALITY_RED`; digest mismatch `-> PROBE_INVALID`. Enumerate every logically legal combination of the four booleans/count predicates, assert the return belongs to the six-state set, and assert exactly one string is returned. Any failure exits nonzero as a broken test contract. - -Always record `UnderlyingStateWithoutPerformanceGate`; it is especially mandatory when final state is `BUDGET_RED`. For `FEASIBLE_WITH_EXCURSION`, select only `ExcursionQualifiedRecords` by `(max(Eminus,Eplus), Eminus+Eplus, maximumDeviation, candidateIndex)`. For `STRICT_GREEN`, report that no raw-range relaxation is needed. For the budget/no-candidate/quality states, retain every categorized failure count. Do not turn `not evaluated` into zero. - -The generated report must contain every field required by spec sections 12–15 plus full paths to: - -```text -strict-vs-shared evaluator hash proof -measurement evaluator commit and diff -strict/measurement candidate audit JSON -30-run timing JSON -state decision trace -``` - -- [ ] **Step 8: Independently verify root diffs and rerun the final gate** - -```powershell -git -C $strictRoot diff --quiet $coreCommit HEAD -if ($LASTEXITCODE -ne 0) { throw 'Strict root has uncommitted drift.' } -$strictStatus = @(git -C $strictRoot status --porcelain) -if ($strictStatus.Count -ne 0) { throw ('Strict root working tree drift: ' + ($strictStatus -join ',')) } -$measureFiles = @(git -C $measureRoot diff --name-only $coreCommit HEAD) -if ($measureFiles.Count -ne 1 -or $measureFiles[0] -ne 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs') { - throw ('Unexpected measurement changes: ' + ($measureFiles -join ',')) -} -$measureStatus = @(git -C $measureRoot status --porcelain) -if ($measureStatus.Count -ne 0) { throw ('Measurement root working tree drift: ' + ($measureStatus -join ',')) } -$finalGateScript = Join-Path $measureRoot 'ClumsyPilot\tests\verify_path_smoothing_local_g2_excursion_probe.ps1' -powershell -ExecutionPolicy Bypass -File $finalGateScript -Mode FinalGate ` - -StrictRoot $strictRoot -MeasurementRoot $measureRoot -EvidenceRoot $evidenceRoot -``` - -The controller reruns the same final-gate command independently after the task reviewer approves. Task 4 is complete when evidence is valid and exactly one state is produced; it does not require the state to be positive. - ---- - -### Task 5: Publish only the verified feasibility report on the shared branch - -**Files:** - -- Create on shared branch: `docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md` -- Record without committing: `.superpowers/sdd/local-g2-excursion-task-5-report.md` - -**Interfaces:** - -- Consumes the Task 4 generated report and controller rerun evidence. -- Produces one immutable shared report; no probe `.cs`, `.ps1`, configuration, fixture, or evaluator file is copied back. - -- [ ] **Step 1: Verify the shared branch has no staged files and no probe source drift** - -```powershell -git diff --cached --quiet -if ($LASTEXITCODE -ne 0) { throw 'Shared index is not empty.' } -git diff --no-ext-diff a4e116a -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs -``` - -The diff is recorded as context and is not compared with `a4e116a`, because the shared tree already contained user changes at Task 1. The authoritative preservation check is the Task 1 machine-readable manifest: regenerate it from the same explicit `ClumsyPilot.csproj`, `ParkrobTrajplanner/PathSmoothing/**/*.cs`, and fixture paths, then require identical path count, sorted paths, per-file SHA-256 values, and manifest SHA-256. This includes the currently untracked `LocalG2PreSmoothingPipeline.cs`. - -- [ ] **Step 2: Add the generated report with `apply_patch`** - -Use the exact Task 4 generated Markdown as the new report. It must name the final state, core/strict/measurement commits and paths, all source hashes/diffs, candidate atomic records, timing samples, decision trace, and the matching authorization boundary: - -```text -PROBE_INVALID -> repair and rerun; no geometry conclusion -BUDGET_RED -> performance-only optimization and rerun -STRICT_GREEN -> next design keeps the strict raw-range gate -NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET or QUALITY_RED -> no evaluator change -FEASIBLE_WITH_EXCURSION -> report minimum found excursion; request a new production-envelope decision -``` - -- [ ] **Step 3: Verify and commit only the report** - -```powershell -git add -- docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md -git diff --cached --check -$staged = @(git diff --cached --name-only) -if ($staged.Count -ne 1 -or $staged[0] -ne 'docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md') { - throw ('Unexpected staged files: ' + ($staged -join ',')) -} -git commit -m 'docs: report Local G2 curvature excursion feasibility' -``` - -- [ ] **Step 4: Controller final verification** - -Freshly run: - -```powershell -git show --check --stat HEAD -git diff --cached --quiet -git status --short -- docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md -``` - -Then compare the committed report SHA-256 with the Task 4 generated report SHA-256. Only after equality and the independent final-gate rerun may the controller state the probe result to the user. - ---- - -## Stop Rules - -- Stop immediately with `PROBE_INVALID` on source identity drift, unauthorized evaluator differences, digest mismatch, missing evidence, nondeterminism, cancellation, or test infrastructure failure. -- `BUDGET_RED`, `NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET`, and `QUALITY_RED` are valid terminal evidence outcomes, not permission to expand search or relax gates. -- `STRICT_GREEN` authorizes only a new production design that keeps the strict raw-range evaluator unchanged. -- `FEASIBLE_WITH_EXCURSION` authorizes only a user decision and a new production-envelope design; it does not authorize copying measurement behavior or probe source into production. -- Do not execute any old split-scale plan task after this plan begins. - -## Subagent-Driven Review Gates - -- Record the disposable core baseline commit before Task 1 implementation and use it as the review-package base for Task 1. -- For Tasks 2 and 3, use the previous approved disposable-core commit as the review-package base; never use `HEAD~1` when a task creates multiple commits. -- Task 4 review receives the core commit, measurement commit, strict/measurement evaluator diff, candidate-audit JSON, timing JSON, state trace, and generated report. -- Resolve every task reviewer Critical/Important before continuing. A `⚠️ Cannot verify` item must be resolved by the controller before marking the task complete. -- After Task 5, perform a broad final review of the design commit, plan commit, final report, and all disposable evidence paths. The final reviewer must confirm that no probe source reached the shared branch. diff --git a/docs/superpowers/plans/2026-08-01-local-g2-split-derivative-scale-recovery.md b/docs/superpowers/plans/2026-08-01-local-g2-split-derivative-scale-recovery.md deleted file mode 100644 index 2c00a5c..0000000 --- a/docs/superpowers/plans/2026-08-01-local-g2-split-derivative-scale-recovery.md +++ /dev/null @@ -1,1618 +0,0 @@ -# Local G2 Split-Derivative-Scale Recovery Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace the infeasible Local G2 soft-position-anchor family with a bounded hard-position, split incoming/outgoing derivative-scale family, then complete and verify the dedicated Task 8 publication path without relaxing any safety or quality gate. - -**Architecture:** A stateful candidate-build session owns deterministic representative-window/profile scheduling so production constructs Tier 1 first and constructs Tier 2 only after Tier 1 has no accepted candidate. Every primitive-boundary coordinate remains fixed; independent positive incoming/outgoing Hermite derivative scales supply the finite geometric freedom, while a per-region evaluator session caches only raw-window analysis and still runs every candidate-specific geometry, collision, clearance, deviation, and quality check. - -**Tech Stack:** C# 10, .NET Standard 2.0, PowerShell reflection verification, existing quintic Hermite/path analysis/vehicle validation components, Git. - -## Global Constraints - -- `MinimumWindowLengthMeters = 0.20`, `PreferredWindowLengthMeters = 0.50`, and `MaximumWindowLengthMeters = 0.80` are left-plus-right total lengths. -- Path start, path end, gear switches, outer window endpoints, and every internal primitive boundary remain hard position anchors. -- Internal vehicle heading and distance-weighted shared vehicle curvature remain hard boundary values. -- Segment `node[i] -> node[i+1]` uses `node[i].OutgoingDerivativeScale` and `node[i+1].IncomingDerivativeScale`. -- Boundary derivatives remain `r' = lambda * T` and `r'' = lambda^2 * kappa_geometric * N`; geometric G2 requires common position, unit tangent, and geometric curvature, not equal parameter speeds. -- Internal profiles are exactly `P0=(1.00,1.00)`, `P1=(0.75,1.25)`, and `P2=(1.25,0.75)`; outer endpoint factors are always `1.00`. -- A multi-event region applies one profile to every internal node; a window without an internal node emits only P0. -- Representative windows are, in order, planner-first, shortest, longest, and most asymmetric, de-duplicated by start/end arc and tie-broken by planner candidate index. -- Tier 1 is all representative P0 attempts followed by first-window P1 and P2, with at most six attempts. Tier 2 is remaining-window P1 followed by remaining-window P2. -- The configured limit truncates the global attempt order and the hard limit is `12`; failed constructions consume attempt budget, while successful geometries receive dense candidate indices. -- `PathSmoothingRegionReport.CandidateCount` is the number of candidates actually passed to the evaluator, not window count or build-attempt count. -- Maximum vehicle curvature, raw curvature range tolerance `1e-6`, full-body collision, extra clearance, maximum deviation, 20 percent peak-gradient improvement, and 2 percent variation-cost tolerance remain unchanged. -- Do not change Hybrid A*, vehicle parameters, SQP, legacy smoothers, comparison defaults, fixtures, expected status thresholds, or collision/clearance behavior. -- Preserve existing cancellation checks, derivative certification depth `40`, derivative certification interval cap `8192`, and adaptive sampling depth `32`. -- Identical requests must produce identical attempt order, candidate indices, coordinates, status, and region reports. -- Use TDD. The disposable `SingleTurn` feasibility gate must be GREEN before the shared `LocalG2CandidateBuilder.cs` is edited. -- Preserve unrelated dirty worktree files. Stage only the exact files named by each commit step. -- Never edit or delete `ClumsyPilot/ParkrobTrajplanner/auto_avoidance`; when its unavailable assemblies block the normal build, use an isolated copy excluding only that directory. - ---- - -## Completed prerequisites - -- Commit `144a088` already makes evaluator window extraction de-duplicate coincident boundary points. -- Commit `bd08a9b` already makes the window planner cover preferred/minimum/maximum total lengths before asymmetric variants consume the budget. -- The old soft-anchor Task 3 is intentionally abandoned: its `0.05 m` coordinate offsets caused real `SingleTurn` curvature up to approximately `14.86 1/m` against the approximately `0.833333 1/m` vehicle limit. -- This plan supersedes `docs/superpowers/plans/2026-07-31-local-g2-soft-anchor-candidate-recovery.md` from its Task 3 onward. Do not revert or reimplement its completed Tasks 1 and 2. - -## File Structure - -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` - - Owns internal-only scale-profile, tier, and internal-node scale diagnostics attached to a successfully constructed geometry. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` - - Owns representative-window selection, split incoming/outgoing scales, global attempt order, dense candidate indices, and lazy Tier 2 construction. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` - - Owns a request/region-scoped evaluation session and its raw-window analysis cache; candidate safety checks remain here. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` - - Owns Tier 1 evaluation, conditional Tier 2 expansion, selected-candidate reporting, global validation, and rollback. -- `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` - - Dispatches only `LocalG2Quintic` to the dedicated pipeline and preserves all legacy routes. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - - Proves fixed coordinates, split-scale G2, deterministic scheduling, caps, cache count, forward/reverse behavior, and the real accepted `SingleTurn` tuple. -- `ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1` - - Proves Task 8 statuses, Tier 1 early stop/Tier 2 expansion, safe publication, report order, rollback, and cancellation. -- `ClumsyPilot/tests/measure_path_smoothing_local_g2_performance.ps1` - - Runs the fixed 5-warmup/30-measurement benchmark and emits machine-readable P50/P95 metrics. -- `ClumsyPilot/tests/verify_path_smoothing_service.ps1` - - Proves dedicated Local G2 dispatch and unchanged legacy method routing. - ---- - -### Task 1: Prove and implement the hard-anchor split-scale candidate family - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` -- Record without committing: `.superpowers/sdd/local-g2-split-scale-feasibility-report.md` - -**Interfaces:** - -- Consumes: - -```csharp -internal LocalG2CandidateBuildSession BeginBuild( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - LocalG2OptionsSnapshot options); -``` - -- Produces: - -```csharp -internal IReadOnlyList BuildPrimary( - CancellationToken cancellationToken); - -internal IReadOnlyList BuildFallback( - CancellationToken cancellationToken); - -internal int AttemptCount { get; } -internal int SuccessfulCandidateCount { get; } -internal IReadOnlyList AttemptDiagnostics { get; } -``` - -- `BuildFallback` throws `InvalidOperationException` if called before `BuildPrimary`, and production does not call it after a Tier 1 acceptance. - -- [ ] **Step 1: Replace the obsolete soft-anchor RED assertion** - -In `verify_path_smoothing_local_g2_candidates.ps1`, replace `$acceptedSoftCandidates`, normal-offset inference, and the final non-zero-soft-anchor assertion with these checks: - -```powershell -$acceptedSplitCandidates = @() -$maximumAnchorError = 0.0 -$maximumConnectionPositionError = 0.0 -$maximumTangentDirectionError = 0.0 -$maximumCurvatureError = 0.0 -foreach ($realCandidate in $realCandidates) { - $evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @( - $preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options, - [Threading.CancellationToken]::None)) - $failureReason = Get-InternalProperty $evaluation 'FailureReason' - if ($failureReason -eq 2) { $duplicateFailures += (Get-InternalProperty $realCandidate 'CandidateIndex') } - - $hasDifferentScale = $false - foreach ($diagnostic in (Get-InternalProperty $realCandidate 'InternalScaleDiagnostics')) { - $anchorError = [double](Get-InternalProperty $diagnostic 'AnchorPositionErrorMeters') - $maximumAnchorError = [Math]::Max($maximumAnchorError, $anchorError) - $maximumConnectionPositionError = [Math]::Max( - $maximumConnectionPositionError, - [double](Get-InternalProperty $diagnostic 'ConnectionPositionErrorMeters')) - $maximumTangentDirectionError = [Math]::Max( - $maximumTangentDirectionError, - [double](Get-InternalProperty $diagnostic 'TangentDirectionErrorRadians')) - $maximumCurvatureError = [Math]::Max( - $maximumCurvatureError, - [double](Get-InternalProperty $diagnostic 'CurvatureErrorPerMeter')) - $incoming = [double](Get-InternalProperty $diagnostic 'IncomingDerivativeScale') - $outgoing = [double](Get-InternalProperty $diagnostic 'OutgoingDerivativeScale') - if ([Math]::Abs($incoming - $outgoing) -gt 1e-10) { $hasDifferentScale = $true } - } - if ((Get-InternalProperty $evaluation 'Accepted') -and $hasDifferentScale) { - $acceptedSplitCandidates += $realCandidate - } -} -Assert-Equal 0 $duplicateFailures.Count ` - 'SingleTurn candidates must not fail raw-window analysis on coincident boundary points.' -Assert-True ($maximumAnchorError -le 1e-9) ` - 'Every internal primitive boundary must remain at its original coordinate.' -Assert-True ($maximumConnectionPositionError -le 1e-9) ` - 'Every real split-scale connection must be position continuous.' -Assert-True ($maximumTangentDirectionError -le 1e-8) ` - 'Every real split-scale connection must preserve unit tangent direction.' -Assert-True ($maximumCurvatureError -le 1e-8) ` - 'Every real split-scale connection must preserve geometric curvature.' -Assert-True ($acceptedSplitCandidates.Count -gt 0) ` - 'SingleTurn must accept a zero-coordinate-offset candidate with different incoming/outgoing scales.' -``` - -- [ ] **Step 2: Run the permanent test against the old shared builder and confirm RED** - -Run: - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -Expected: FAIL because the real candidate diagnostics do not expose the fixed-anchor/split-scale G2 measurements, or because no accepted zero-offset split-scale candidate exists. A pass at this point means the test did not exercise the new contract; correct the test before continuing. - -- [ ] **Step 3: Create an isolated feasibility copy and capture the `bd08a9b` baseline** - -Use a disposable directory whose resolved path is under the system temp directory: - -```powershell -$probeRoot = Join-Path ([IO.Path]::GetTempPath()) ('local-g2-split-' + [Guid]::NewGuid().ToString('N')) -New-Item -ItemType Directory -Path $probeRoot | Out-Null -$sourceRoot = (Resolve-Path '.').Path -$probeProject = Join-Path $probeRoot 'ParkingRobot' -robocopy $sourceRoot $probeProject /E /XD .git bin obj auto_avoidance .task8-sweep | Out-Null -if ($LASTEXITCODE -gt 7) { throw "robocopy failed with exit code $LASTEXITCODE" } -``` - -Before applying the candidate patch in the disposable copy, run the real `SingleTurn` builder/evaluator path 5 warmups plus 30 measurements. Record the elapsed milliseconds as `baselineSamples`, then compute: - -```powershell -function Get-Percentile([double[]]$Values, [double]$Fraction) { - $ordered = @($Values | Sort-Object) - $index = [Math]::Ceiling($Fraction * $ordered.Count) - 1 - return [double]$ordered[[Math]::Max(0, [Math]::Min($ordered.Count - 1, $index))] -} -$baselineP50 = Get-Percentile $baselineSamples 0.50 -$baselineP95 = Get-Percentile $baselineSamples 0.95 -``` - -The report must identify commit `bd08a9b`, `Debug/netstandard2.0`, the `SingleTurn` fixture/configuration, the machine, 5 warmups, 30 measurements, P50, and P95. If the candidate/evaluator sources differ from `bd08a9b`, overwrite only these three files in the disposable copy from a `git archive bd08a9b` extraction before measuring: - -```text -LocalG2CandidateBuilder.cs -LocalG2CandidateGeometry.cs -LocalG2CandidateEvaluator.cs -``` - -- [ ] **Step 4: Add scale metadata in the disposable copy** - -Add this enum and diagnostic type to `LocalG2CandidateGeometry.cs`: - -```csharp -internal enum LocalG2DerivativeScaleProfile -{ - P0 = 0, - P1 = 1, - P2 = 2, -} - -internal sealed class LocalG2InternalScaleDiagnostic -{ - internal LocalG2InternalScaleDiagnostic( - double arcLengthMeters, - double incomingDerivativeScale, - double outgoingDerivativeScale, - double anchorPositionErrorMeters, - double connectionPositionErrorMeters, - double tangentDirectionErrorRadians, - double curvatureErrorPerMeter) - { - if (!NumericGuard.IsFinite(arcLengthMeters) || - !NumericGuard.IsPositiveFinite(incomingDerivativeScale) || - !NumericGuard.IsPositiveFinite(outgoingDerivativeScale) || - !NumericGuard.IsFinite(anchorPositionErrorMeters) || anchorPositionErrorMeters < 0d || - !NumericGuard.IsFinite(connectionPositionErrorMeters) || connectionPositionErrorMeters < 0d || - !NumericGuard.IsFinite(tangentDirectionErrorRadians) || tangentDirectionErrorRadians < 0d || - !NumericGuard.IsFinite(curvatureErrorPerMeter) || curvatureErrorPerMeter < 0d) - throw new ArgumentOutOfRangeException(nameof(arcLengthMeters)); - ArcLengthMeters = arcLengthMeters; - IncomingDerivativeScale = incomingDerivativeScale; - OutgoingDerivativeScale = outgoingDerivativeScale; - AnchorPositionErrorMeters = anchorPositionErrorMeters; - ConnectionPositionErrorMeters = connectionPositionErrorMeters; - TangentDirectionErrorRadians = tangentDirectionErrorRadians; - CurvatureErrorPerMeter = curvatureErrorPerMeter; - } - - internal double ArcLengthMeters { get; } - internal double IncomingDerivativeScale { get; } - internal double OutgoingDerivativeScale { get; } - internal double AnchorPositionErrorMeters { get; } - internal double ConnectionPositionErrorMeters { get; } - internal double TangentDirectionErrorRadians { get; } - internal double CurvatureErrorPerMeter { get; } -} -``` - -Append these optional parameters to the `LocalG2CandidateGeometry` constructor so existing evaluator TestHooks remain source-compatible: - -```csharp -LocalG2DerivativeScaleProfile scaleProfile = LocalG2DerivativeScaleProfile.P0, -int candidateTier = 1, -IReadOnlyList internalScaleDiagnostics = null -``` - -Validate `candidateTier` is `1` or `2`, copy diagnostics into a `ReadOnlyCollection`, and expose: - -```csharp -if (candidateTier != 1 && candidateTier != 2) - throw new ArgumentOutOfRangeException(nameof(candidateTier)); -ScaleProfile = scaleProfile; -CandidateTier = candidateTier; -var diagnosticCopy = new List(); -if (internalScaleDiagnostics != null) -{ - for (int index = 0; index < internalScaleDiagnostics.Count; index++) - { - if (internalScaleDiagnostics[index] == null) - throw new ArgumentOutOfRangeException(nameof(internalScaleDiagnostics)); - diagnosticCopy.Add(internalScaleDiagnostics[index]); - } -} -InternalScaleDiagnostics = - new ReadOnlyCollection(diagnosticCopy); - -internal LocalG2DerivativeScaleProfile ScaleProfile { get; } -internal int CandidateTier { get; } -internal IReadOnlyList InternalScaleDiagnostics { get; } -``` - -- [ ] **Step 5: Replace the single-scale boundary model in the disposable copy** - -Replace `BoundaryNode.DerivativeScale` and `WithDerivativeScale` with: - -```csharp -internal double IncomingDerivativeScale { get; } -internal double OutgoingDerivativeScale { get; } - -internal BoundaryNode WithDerivativeScales(double incoming, double outgoing) => - new BoundaryNode( - ArcLengthMeters, - X, - Y, - VehicleHeadingRadians, - VehicleCurvaturePerMeter, - incoming, - outgoing); -``` - -The public-shape constructor initializes both scales to `0d`; the private constructor receives both values. Replace `TryAssignDerivativeScales` with: - -```csharp -private static bool TryAssignDerivativeScales( - List nodes, - LocalG2DerivativeScaleProfile profile) -{ - if (nodes == null || nodes.Count < 2) return false; - GetProfileFactors(profile, out double incomingFactor, out double outgoingFactor); - for (int index = 0; index < nodes.Count; index++) - { - bool internalNode = index > 0 && index < nodes.Count - 1; - double incoming = index == 0 - ? 0d - : nodes[index].ArcLengthMeters - nodes[index - 1].ArcLengthMeters; - double outgoing = index == nodes.Count - 1 - ? 0d - : nodes[index + 1].ArcLengthMeters - nodes[index].ArcLengthMeters; - if (internalNode) - { - incoming *= incomingFactor; - outgoing *= outgoingFactor; - } - if ((index > 0 && !NumericGuard.IsPositiveFinite(incoming)) || - (index < nodes.Count - 1 && !NumericGuard.IsPositiveFinite(outgoing))) - return false; - nodes[index] = nodes[index].WithDerivativeScales(incoming, outgoing); - } - return true; -} - -private static void GetProfileFactors( - LocalG2DerivativeScaleProfile profile, - out double incoming, - out double outgoing) -{ - switch (profile) - { - case LocalG2DerivativeScaleProfile.P0: incoming = 1d; outgoing = 1d; return; - case LocalG2DerivativeScaleProfile.P1: incoming = 0.75d; outgoing = 1.25d; return; - case LocalG2DerivativeScaleProfile.P2: incoming = 1.25d; outgoing = 0.75d; return; - default: throw new ArgumentOutOfRangeException(nameof(profile)); - } -} -``` - -Change curve construction to select the side-specific scale: - -```csharp -private static bool TryCreateCurve( - BoundaryNode left, - BoundaryNode right, - double directionSign, - out QuinticHermiteCurve2D curve) -{ - curve = null; - if (!TryGetDerivatives(left, left.OutgoingDerivativeScale, directionSign, - out double ldx, out double ldy, out double lddx, out double lddy) || - !TryGetDerivatives(right, right.IncomingDerivativeScale, directionSign, - out double rdx, out double rdy, out double rddx, out double rddy)) - return false; - return QuinticHermiteCurve2D.TryCreate( - left.X, left.Y, ldx, ldy, lddx, lddy, - right.X, right.Y, rdx, rdy, rddx, rddy, - out curve, out _); -} - -private static bool TryGetDerivatives( - BoundaryNode node, - double derivativeScale, - double directionSign, - out double dx, - out double dy, - out double ddx, - out double ddy) -{ - dx = dy = ddx = ddy = 0d; - if (!NumericGuard.IsPositiveFinite(derivativeScale)) return false; - double travelHeading = directionSign > 0d - ? node.VehicleHeadingRadians - : node.VehicleHeadingRadians - Math.PI; - double geometricCurvature = directionSign * node.VehicleCurvaturePerMeter; - double tx = Math.Cos(travelHeading); - double ty = Math.Sin(travelHeading); - double nx = -ty; - double ny = tx; - dx = derivativeScale * tx; - dy = derivativeScale * ty; - ddx = derivativeScale * derivativeScale * geometricCurvature * nx; - ddy = derivativeScale * derivativeScale * geometricCurvature * ny; - return NumericGuard.IsFinite(dx) && NumericGuard.IsFinite(dy) && - NumericGuard.IsFinite(ddx) && NumericGuard.IsFinite(ddy) && - Math.Sqrt(dx * dx + dy * dy) >= MinimumDerivativeNorm; -} -``` - -Do not alter transition X/Y construction. Step 7 builds one `LocalG2InternalScaleDiagnostic` for every real internal connection from the actual incoming/outgoing curve endpoint derivatives. - -- [ ] **Step 6: Add deterministic representative scheduling and lazy Tier 2 construction in the disposable copy** - -Delete `DerivativeScaleMultipliers`. Add `BeginBuild`; keep `Build` only as a test/backward-compatibility wrapper that concatenates `BuildPrimary` and `BuildFallback` from one session: - -```csharp -internal LocalG2CandidateBuildSession BeginBuild( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - LocalG2OptionsSnapshot options) => - new LocalG2CandidateBuildSession( - originalSegment, - region, - outputSpacingMeters, - options); - -internal IReadOnlyList Build( - PreparedDirectionSegment originalSegment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - LocalG2OptionsSnapshot options, - CancellationToken cancellationToken) -{ - LocalG2CandidateBuildSession session = BeginBuild( - originalSegment, region, outputSpacingMeters, options); - var all = new List(); - all.AddRange(session.BuildPrimary(cancellationToken)); - all.AddRange(session.BuildFallback(cancellationToken)); - return ReadOnly(all); -} -``` - -Use these attempt records and exact global order: - -```csharp -private readonly struct CandidateAttempt -{ - internal CandidateAttempt( - LocalG2WindowVariant window, - LocalG2DerivativeScaleProfile profile, - int tier) - { - Window = window; - Profile = profile; - Tier = tier; - } - internal LocalG2WindowVariant Window { get; } - internal LocalG2DerivativeScaleProfile Profile { get; } - internal int Tier { get; } -} - -internal sealed class LocalG2CandidateAttemptDiagnostic -{ - internal LocalG2CandidateAttemptDiagnostic( - int attemptIndex, - int tier, - LocalG2DerivativeScaleProfile profile, - int plannerWindowIndex, - double startArcLengthMeters, - double endArcLengthMeters, - bool succeeded, - int candidateIndex, - string reason) - { - AttemptIndex = attemptIndex; - Tier = tier; - Profile = profile; - PlannerWindowIndex = plannerWindowIndex; - StartArcLengthMeters = startArcLengthMeters; - EndArcLengthMeters = endArcLengthMeters; - Succeeded = succeeded; - CandidateIndex = candidateIndex; - Reason = reason ?? string.Empty; - } - internal int AttemptIndex { get; } - internal int Tier { get; } - internal LocalG2DerivativeScaleProfile Profile { get; } - internal int PlannerWindowIndex { get; } - internal double StartArcLengthMeters { get; } - internal double EndArcLengthMeters { get; } - internal bool Succeeded { get; } - internal int CandidateIndex { get; } - internal string Reason { get; } -} - -private static List CreateAttempts( - LocalG2SmoothingRegion region, - IReadOnlyList windows, - out int primaryAttemptCount) -{ - var attempts = new List(12); - for (int index = 0; index < windows.Count; index++) - attempts.Add(new CandidateAttempt(windows[index], LocalG2DerivativeScaleProfile.P0, 1)); - if (windows.Count > 0 && HasInternalNode(region, windows[0])) - { - attempts.Add(new CandidateAttempt(windows[0], LocalG2DerivativeScaleProfile.P1, 1)); - attempts.Add(new CandidateAttempt(windows[0], LocalG2DerivativeScaleProfile.P2, 1)); - } - primaryAttemptCount = Math.Min(attempts.Count, 6); - for (int index = 1; index < windows.Count; index++) - if (HasInternalNode(region, windows[index])) - attempts.Add(new CandidateAttempt(windows[index], LocalG2DerivativeScaleProfile.P1, 2)); - for (int index = 1; index < windows.Count; index++) - if (HasInternalNode(region, windows[index])) - attempts.Add(new CandidateAttempt(windows[index], LocalG2DerivativeScaleProfile.P2, 2)); - return attempts; -} - -private static bool HasInternalNode( - LocalG2SmoothingRegion region, - LocalG2WindowVariant window) -{ - for (int index = 0; index < region.Transitions.Count; index++) - { - double arc = region.Transitions[index].LocalArcLengthMeters; - if (arc > window.StartArcLengthMeters + 1e-10d && - arc < window.EndArcLengthMeters - 1e-10d) - return true; - } - return false; -} -``` - -Add the representative selectors exactly as follows: - -```csharp -private static IReadOnlyList SelectRepresentativeWindows( - IReadOnlyList variants) -{ - var selected = new List(4); - if (variants == null || variants.Count == 0) - return new ReadOnlyCollection(selected); - AddDistinctWindow(selected, variants[0]); - LocalG2WindowVariant shortest = variants[0]; - LocalG2WindowVariant longest = variants[0]; - LocalG2WindowVariant asymmetric = variants[0]; - for (int index = 1; index < variants.Count; index++) - { - LocalG2WindowVariant candidate = variants[index]; - if (IsShorter(candidate, shortest)) shortest = candidate; - if (IsLonger(candidate, longest)) longest = candidate; - if (IsMoreAsymmetric(candidate, asymmetric)) asymmetric = candidate; - } - AddDistinctWindow(selected, shortest); - AddDistinctWindow(selected, longest); - AddDistinctWindow(selected, asymmetric); - return new ReadOnlyCollection(selected); -} - -private static bool IsShorter(LocalG2WindowVariant candidate, LocalG2WindowVariant current) -{ - double candidateLength = candidate.EndArcLengthMeters - candidate.StartArcLengthMeters; - double currentLength = current.EndArcLengthMeters - current.StartArcLengthMeters; - return candidateLength < currentLength - 1e-10d || - (Math.Abs(candidateLength - currentLength) <= 1e-10d && - candidate.CandidateIndex < current.CandidateIndex); -} - -private static bool IsLonger(LocalG2WindowVariant candidate, LocalG2WindowVariant current) -{ - double candidateLength = candidate.EndArcLengthMeters - candidate.StartArcLengthMeters; - double currentLength = current.EndArcLengthMeters - current.StartArcLengthMeters; - return candidateLength > currentLength + 1e-10d || - (Math.Abs(candidateLength - currentLength) <= 1e-10d && - candidate.CandidateIndex < current.CandidateIndex); -} - -private static bool IsMoreAsymmetric(LocalG2WindowVariant candidate, LocalG2WindowVariant current) -{ - double candidateValue = Math.Abs( - candidate.LeftWindowLengthMeters - candidate.RightWindowLengthMeters); - double currentValue = Math.Abs( - current.LeftWindowLengthMeters - current.RightWindowLengthMeters); - return candidateValue > currentValue + 1e-10d || - (Math.Abs(candidateValue - currentValue) <= 1e-10d && - candidate.CandidateIndex < current.CandidateIndex); -} - -private static void AddDistinctWindow( - List selected, - LocalG2WindowVariant candidate) -{ - for (int index = 0; index < selected.Count; index++) - { - if (SameArc(selected[index].StartArcLengthMeters, candidate.StartArcLengthMeters) && - SameArc(selected[index].EndArcLengthMeters, candidate.EndArcLengthMeters)) - return; - } - selected.Add(candidate); -} -``` - -Define this nested type inside `LocalG2CandidateBuilder`: - -```csharp -internal sealed class LocalG2CandidateBuildSession -{ - private readonly PreparedDirectionSegment _segment; - private readonly LocalG2SmoothingRegion _region; - private readonly double _outputSpacingMeters; - private readonly List _attempts; - private readonly int _primaryAttemptCount; - private readonly int _attemptLimit; - private readonly List _attemptDiagnostics = - new List(); - private int _nextAttemptIndex; - private int _nextCandidateIndex; - private bool _primaryBuilt; - private bool _fallbackBuilt; - - internal LocalG2CandidateBuildSession( - PreparedDirectionSegment segment, - LocalG2SmoothingRegion region, - double outputSpacingMeters, - LocalG2OptionsSnapshot options) - { - _segment = segment; - _region = region; - _outputSpacingMeters = outputSpacingMeters; - if (!IsValidInput(segment, region, outputSpacingMeters, options)) - { - _attempts = new List(); - _primaryAttemptCount = 0; - _attemptLimit = 0; - return; - } - IReadOnlyList windows = - SelectRepresentativeWindows(region.WindowVariants); - _attempts = CreateAttempts(region, windows, out _primaryAttemptCount); - _attemptLimit = Math.Min(options.MaximumCandidatesPerRegion, 12); - } - - internal int AttemptCount { get; private set; } - internal int SuccessfulCandidateCount { get; private set; } - internal IReadOnlyList AttemptDiagnostics => - new ReadOnlyCollection(_attemptDiagnostics); - - internal IReadOnlyList BuildPrimary( - CancellationToken cancellationToken) - { - if (_primaryBuilt) - throw new InvalidOperationException("Local G2 primary candidates were already built."); - _primaryBuilt = true; - return BuildUntil(_primaryAttemptCount, cancellationToken); - } - - internal IReadOnlyList BuildFallback( - CancellationToken cancellationToken) - { - if (!_primaryBuilt) - throw new InvalidOperationException("BuildPrimary must run before BuildFallback."); - if (_fallbackBuilt) - throw new InvalidOperationException("Local G2 fallback candidates were already built."); - _fallbackBuilt = true; - return BuildUntil(_attempts.Count, cancellationToken); - } - - private IReadOnlyList BuildUntil( - int endExclusive, - CancellationToken cancellationToken) - { - var candidates = new List(); - int boundedEnd = Math.Min(endExclusive, _attemptLimit); - while (_nextAttemptIndex < boundedEnd) - { - cancellationToken.ThrowIfCancellationRequested(); - int attemptIndex = _nextAttemptIndex; - CandidateAttempt attempt = _attempts[_nextAttemptIndex++]; - AttemptCount++; - bool succeeded = TryBuildCandidate( - _nextCandidateIndex, - _segment, - _region, - attempt.Window, - attempt.Profile, - attempt.Tier, - _outputSpacingMeters, - cancellationToken, - out LocalG2CandidateGeometry candidate); - _attemptDiagnostics.Add(new LocalG2CandidateAttemptDiagnostic( - attemptIndex, - attempt.Tier, - attempt.Profile, - attempt.Window.CandidateIndex, - attempt.Window.StartArcLengthMeters, - attempt.Window.EndArcLengthMeters, - succeeded, - succeeded ? candidate.CandidateIndex : -1, - succeeded ? "Built" : "CandidateGenerationFailed")); - if (succeeded) - { - candidates.Add(candidate); - _nextCandidateIndex++; - SuccessfulCandidateCount++; - } - } - return ReadOnly(candidates); - } -} -``` - -Change `TryBuildCandidate` to receive `LocalG2DerivativeScaleProfile profile` and `int candidateTier`, call `TryAssignDerivativeScales(nodes, profile)`, and pass the profile, tier, and diagnostics into `LocalG2CandidateGeometry`. - -- [ ] **Step 7: Measure real split-scale connections without adding a test-only production method** - -Construct all adjacent curves first, measure each real internal connection, then sample the same curve objects. Add: - -```csharp -private static bool TryMeasureConnection( - QuinticHermiteCurve2D incomingCurve, - QuinticHermiteCurve2D outgoingCurve, - BoundaryNode node, - out LocalG2InternalScaleDiagnostic diagnostic) -{ - diagnostic = null; - incomingCurve.Evaluate(1d, - out double lx, out double ly, - out double ldx, out double ldy, - out double lddx, out double lddy); - outgoingCurve.Evaluate(0d, - out double rx, out double ry, - out double rdx, out double rdy, - out double rddx, out double rddy); - double leftNorm = Math.Sqrt(ldx * ldx + ldy * ldy); - double rightNorm = Math.Sqrt(rdx * rdx + rdy * rdy); - if (!NumericGuard.IsPositiveFinite(leftNorm) || - !NumericGuard.IsPositiveFinite(rightNorm)) - return false; - - double leftAnchorError = Distance(lx, ly, node.X, node.Y); - double rightAnchorError = Distance(rx, ry, node.X, node.Y); - double anchorError = Math.Max(leftAnchorError, rightAnchorError); - double connectionPositionError = Distance(lx, ly, rx, ry); - double cosine = (ldx * rdx + ldy * rdy) / (leftNorm * rightNorm); - double tangentError = Math.Acos(Math.Max(-1d, Math.Min(1d, cosine))); - double leftCurvature = (ldx * lddy - ldy * lddx) / - (leftNorm * leftNorm * leftNorm); - double rightCurvature = (rdx * rddy - rdy * rddx) / - (rightNorm * rightNorm * rightNorm); - double curvatureError = Math.Abs(leftCurvature - rightCurvature); - if (!NumericGuard.IsFinite(anchorError) || - !NumericGuard.IsFinite(connectionPositionError) || - !NumericGuard.IsFinite(tangentError) || - !NumericGuard.IsFinite(curvatureError)) - return false; - - diagnostic = new LocalG2InternalScaleDiagnostic( - node.ArcLengthMeters, - node.IncomingDerivativeScale, - node.OutgoingDerivativeScale, - anchorError, - connectionPositionError, - tangentError, - curvatureError); - return true; -} - -private static double Distance(double x0, double y0, double x1, double y1) -{ - double dx = x1 - x0; - double dy = y1 - y0; - return Math.Sqrt(dx * dx + dy * dy); -} -``` - -Replace the single create-and-sample loop with this exact ordering: - -```csharp -var curves = new List(nodes.Count - 1); -for (int nodeIndex = 1; nodeIndex < nodes.Count; nodeIndex++) -{ - cancellationToken.ThrowIfCancellationRequested(); - if (!TryCreateCurve(nodes[nodeIndex - 1], nodes[nodeIndex], directionSign, - out QuinticHermiteCurve2D curve)) - return false; - curves.Add(curve); -} - -var scaleDiagnostics = new List(); -for (int nodeIndex = 1; nodeIndex < nodes.Count - 1; nodeIndex++) -{ - if (!TryMeasureConnection( - curves[nodeIndex - 1], - curves[nodeIndex], - nodes[nodeIndex], - out LocalG2InternalScaleDiagnostic diagnostic)) - return false; - scaleDiagnostics.Add(diagnostic); -} - -var sampled = new List(); -for (int curveIndex = 0; curveIndex < curves.Count; curveIndex++) -{ - cancellationToken.ThrowIfCancellationRequested(); - if (!TryAppendCurveSamples( - curves[curveIndex], - nodes[curveIndex], - nodes[curveIndex + 1], - segment, - outputSpacingMeters, - sampled, - cancellationToken)) - return false; -} -``` - -Pass `scaleDiagnostics` into the candidate geometry. These measurements are internal feasibility diagnostics required by the design; they are not exposed as a new callable test API. - -- [ ] **Step 8: Run the isolated feasibility gate** - -Build the disposable copy and run its candidate verification against the real `SingleTurn` fixture. Evaluate all successfully constructed Tier 1 candidates; only if none is accepted, construct and evaluate Tier 2. The gate is GREEN only when all of these are true: - -```text -at least one real candidate is accepted -accepted candidate internal anchor error <= 1e-9 m -at least one accepted candidate has different actual incoming/outgoing scales -connection tangent direction error <= 1e-8 rad -connection geometric curvature error <= 1e-8 1/m -unchanged curvature, raw-range, 20%, 2%, deviation, collision, and clearance gates pass -``` - -Record every attempted tuple as `(tier, representative index, start arc, end arc, profile)`, every successful candidate's internal scales, failure reason or accepted metrics, maximum deviation, minimum clearance, curvature range, peak, variation cost, and elapsed time in `.superpowers/sdd/local-g2-split-scale-feasibility-report.md`. - -If no tuple is accepted, stop the plan with the shared builder and geometry unchanged. Do not alter fixtures, coordinates, shared curvature, vehicle limits, evaluator tolerances, safety gates, or quality gates. - -- [ ] **Step 9: Apply the proven patch to the shared tree and verify GREEN** - -Only after Step 8 is GREEN, apply the exact geometry/builder patch from Steps 4–7 to the shared files. Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -If the normal build is blocked only by `auto_avoidance`, repeat the build and scripts in a disposable copy excluding that directory and record both the shared-tree failure and isolated GREEN evidence. Expected: build succeeds in the valid source set and both scripts pass. - -- [ ] **Step 10: Extend schedule/cap/determinism assertions** - -In the candidate script, inspect candidate profile, tier, window start/end, and candidate index. Assert exact global order: - -```text -Tier 1: representative P0 in representative order, first representative P1, first representative P2 -Tier 2: remaining representatives P1, then remaining representatives P2 -``` - -Loop `MaximumCandidatesPerRegion` from `1` through `12`; require `AttemptCount <= limit`, output count `<= limit`, dense successful indices `0..count-1`, and no Tier 2 attempt before all enabled Tier 1 attempts. Run forward, reverse, asymmetric-window, and clustered multi-transition scenarios twice and compare every profile, tier, window, index, point coordinate, heading, arc, and source. - -- [ ] **Step 11: Commit the proven candidate family** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "fix: recover Local G2 candidates with split scales" -``` - -Expected staged names: exactly the three files above. Do not commit `.superpowers/sdd` evidence. - ---- - -### Task 2: Cache raw-window analysis within one region evaluation - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1` - -**Interfaces:** - -- Consumes: candidates from one `LocalG2CandidateBuilder.LocalG2CandidateBuildSession` and one fixed `(rawPath, currentPath, region, request, options)` tuple. -- Produces: - -```csharp -internal EvaluationSession BeginRegionEvaluation( - PreparedPath rawPath, - PreparedPath currentPath, - LocalG2SmoothingRegion region, - PathSmoothingRequest request, - LocalG2OptionsSnapshot options); - -internal sealed class EvaluationSession -{ - internal LocalG2CandidateEvaluation Evaluate( - LocalG2CandidateGeometry candidate, - CancellationToken cancellationToken); - internal int RawWindowAnalysisCount { get; } -} -``` - -- Session lifetime supplies request identity; its cache key is exact `(SegmentIndex, StartArcLengthMeters, EndArcLengthMeters)` and cannot escape one region evaluation. - -- [ ] **Step 1: Add a failing repeated-window cache assertion** - -Add evaluator TestHook scenario `RawWindowCache`. It creates one evaluation session, evaluates three valid geometries sharing the same segment/start/end window, and returns `RawWindowAnalysisCount`. Add to the PowerShell script: - -```powershell -$rawWindowCache = Invoke-EvaluationScenario 'RawWindowCache' -Assert-Equal 1 $rawWindowCache.RawWindowAnalysisCount ` - 'P0/P1/P2 candidates for one window must share one raw-window analysis.' -``` - -Run: - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -``` - -Expected: FAIL because `RawWindowAnalysisCount`/`BeginRegionEvaluation` does not exist. - -- [ ] **Step 2: Add the region-scoped session and exact cache key** - -Add: - -```csharp -internal EvaluationSession BeginRegionEvaluation( - PreparedPath rawPath, - PreparedPath currentPath, - LocalG2SmoothingRegion region, - PathSmoothingRequest request, - LocalG2OptionsSnapshot options) => - new EvaluationSession(this, rawPath, currentPath, region, request, options); - -private readonly struct WindowKey : IEquatable -{ - internal WindowKey(int segmentIndex, double start, double end) - { - SegmentIndex = segmentIndex; - Start = start; - End = end; - } - private int SegmentIndex { get; } - private double Start { get; } - private double End { get; } - public bool Equals(WindowKey other) => - SegmentIndex == other.SegmentIndex && Start.Equals(other.Start) && End.Equals(other.End); - public override bool Equals(object value) => value is WindowKey other && Equals(other); - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = hash * 31 + SegmentIndex; - hash = hash * 31 + Start.GetHashCode(); - return hash * 31 + End.GetHashCode(); - } - } -} -``` - -The session owns `Dictionary`, all request inputs, and `RawWindowAnalysisCount`. On the first key, it extracts/analyzes the current raw window, verifies finite metrics, computes its curvature range, stores success or the stable failure reason, and increments the count exactly once. Repeated profiles reuse that immutable entry. - -- [ ] **Step 3: Split raw-window preparation from candidate-specific evaluation** - -Define the cached value: - -```csharp -private sealed class RawWindowEntry -{ - internal RawWindowEntry( - IReadOnlyList points, - PathGeometryAnalysis analysis, - double minimumCurvature, - double maximumCurvature, - string failureReason) - { - Points = points; - Analysis = analysis; - MinimumCurvature = minimumCurvature; - MaximumCurvature = maximumCurvature; - FailureReason = failureReason ?? string.Empty; - } - internal IReadOnlyList Points { get; } - internal PathGeometryAnalysis Analysis { get; } - internal double MinimumCurvature { get; } - internal double MaximumCurvature { get; } - internal string FailureReason { get; } - internal bool IsValid => Points != null && Analysis != null && string.IsNullOrEmpty(FailureReason); -} -``` - -Move only raw extraction, raw unified resampling/analysis, finite checks, and raw curvature-range computation into session preparation. Keep these operations inside `EvaluatePrepared` for every candidate: - -```text -candidate unified geometry analysis -vehicle maximum curvature -raw curvature-range comparison -maximum deviation -splice and full-path geometry analysis -full-body collision validation -clearance reserve -20 percent peak improvement -2 percent variation-cost tolerance -accepted path-length change -``` - -Keep the current seven-argument `Evaluate` as a compatibility wrapper: - -```csharp -return BeginRegionEvaluation(rawPath, currentPath, region, request, options) - .Evaluate(candidate, cancellationToken); -``` - -- [ ] **Step 4: Preserve the first concrete rejection when no candidate is accepted** - -Change `SelectBest` so it still compares all accepted candidates by deviation, peak, variation cost, length change, then candidate index, but remembers the first non-null rejection: - -```csharp -LocalG2CandidateEvaluation firstRejected = null; -for (int index = 0; index < evaluations.Count; index++) -{ - LocalG2CandidateEvaluation evaluation = evaluations[index]; - if (evaluation == null) continue; - if (!evaluation.Accepted) - { - if (firstRejected == null) firstRejected = evaluation; - continue; - } - if (best == null || Compare(evaluation, best) < 0) best = evaluation; -} -return best ?? firstRejected ?? - Rejected(-1, PathSmoothingRegionFailureReason.CandidateGenerationFailed, - "没有生成可评价的局部 G2 候选。"); -``` - -- [ ] **Step 5: Verify cache and unchanged gates** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_validation.ps1 -``` - -Expected: all pass; repeated-window count is exactly one, and every existing collision/clearance/curvature/improvement/variation failure classification remains unchanged. - -- [ ] **Step 6: Commit the region-scoped cache** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "perf: cache Local G2 raw window analysis" -``` - -Expected staged names: exactly the two files above. - ---- - -### Task 3: Finish tiered Task 8 publication and service integration - -**Files:** - -- Create from the existing untracked prototype: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` -- Create from the existing untracked prototype: `ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_service.ps1` - -**Interfaces:** - -- Consumes: `LocalG2CandidateBuilder.LocalG2CandidateBuildSession`, `LocalG2CandidateEvaluator.EvaluationSession`, immutable `RawPathBaseline`, detector report order, and `LocalG2RegionWorkOrder`. -- Produces: - -```csharp -internal PathSmoothingResult Smooth( - PathSmoothingRequest request, - PreparedPath preparedPath, - RawPathBaseline rawBaseline, - CancellationToken cancellationToken); -``` - -- Guarantees: Tier 2 is neither built nor evaluated after a Tier 1 acceptance; valid non-cancelled requests publish a complete verified path; cancellation publishes no partial path. - -- [ ] **Step 1: Run integration/service RED and record the first contractual failure** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1 -``` - -Expected before the tier/publication corrections: at least one required fixture status, actual candidate count, raw baseline, selected window, rollback, cancellation, or Tier 2 early-stop assertion fails. Record the first failure in `.superpowers/sdd/local-g2-split-scale-task-8-integration-report.md`. - -- [ ] **Step 2: Evaluate Tier 1 and conditionally construct Tier 2** - -Replace the pipeline's eager builder/evaluator loop with: - -```csharp -LocalG2CandidateBuilder.LocalG2CandidateBuildSession buildSession = _builder.BeginBuild( - preparedPath.Segments[region.SegmentIndex], - region, - request.Configuration.OutputSpacingMeters, - options); -LocalG2CandidateEvaluator.EvaluationSession evaluationSession = - _evaluator.BeginRegionEvaluation( - preparedPath, current, region, request, options); - -var candidates = new List(); -var evaluations = new List(); -IReadOnlyList primary = - buildSession.BuildPrimary(cancellationToken); -candidates.AddRange(primary); -EvaluateAll(primary, evaluationSession, evaluations, cancellationToken); -LocalG2CandidateEvaluation best = - LocalG2CandidateEvaluator.SelectBest(evaluations); - -if (!best.Accepted) -{ - IReadOnlyList fallback = - buildSession.BuildFallback(cancellationToken); - candidates.AddRange(fallback); - EvaluateAll(fallback, evaluationSession, evaluations, cancellationToken); - best = LocalG2CandidateEvaluator.SelectBest(evaluations); -} -``` - -Add: - -```csharp -private static void EvaluateAll( - IReadOnlyList candidates, - LocalG2CandidateEvaluator.EvaluationSession session, - List output, - CancellationToken cancellationToken) -{ - for (int index = 0; index < candidates.Count; index++) - { - cancellationToken.ThrowIfCancellationRequested(); - output.Add(session.Evaluate(candidates[index], cancellationToken)); - } -} -``` - -The candidate count passed into every report is `evaluations.Count`. Do not use `region.WindowVariants.Count`, build attempts, or all possible schedule entries. - -- [ ] **Step 3: Bind accepted and rollback reports to the actual selected geometry** - -Resolve the selected geometry by dense candidate index: - -```csharp -private static LocalG2CandidateGeometry FindCandidate( - IReadOnlyList candidates, - int candidateIndex) -{ - for (int index = 0; index < candidates.Count; index++) - if (candidates[index].CandidateIndex == candidateIndex) - return candidates[index]; - return null; -} -``` - -For an accepted evaluation, require `selectedCandidate != null`, store it and `evaluations.Count` in `AcceptedRegion`, and report its exact start/end/left/right window values. For no accepted candidate, use candidate index `-1` only when no evaluation exists; otherwise retain the first concrete rejection and its reason. Rollback reports use the stored selected candidate and actual evaluated count. - -Extend `AcceptedRegion` with: - -```csharp -internal AcceptedRegion( - LocalG2SmoothingRegion region, - PreparedPath before, - LocalG2CandidateEvaluation evaluation, - LocalG2CandidateGeometry candidate, - int evaluatedCandidateCount) -``` - -and read-only `Candidate` and `EvaluatedCandidateCount` properties. - -- [ ] **Step 4: Publish the trusted raw baseline for zero transitions and zero surviving improvements** - -Immediately after detection/planning, if `transitions.Count == 0`, publish the existing `rawBaseline` as `NotNeeded`; do not re-run analysis. After global rollback, if `improvedCount == 0`, publish that same baseline as `Unchanged` with detector-order reports. - -Use: - -```csharp -private static PathSmoothingResult PublishBaseline( - PathSmoothingStatus status, - RawPathBaseline baseline, - Stopwatch stopwatch, - IReadOnlyList reports) => - PathSmoothingResult.PublishLocalG2( - status, - baseline.Path, - baseline.Segments, - new PathSmoothingDiagnostics( - baseline.Metrics, - stopwatch.Elapsed, - 0, - 0d, - string.Empty), - reports); -``` - -- [ ] **Step 5: Preserve work/report order, full validation, and rollback** - -Keep immutable ascending `reportOrder`; process `workRegions` from `LocalG2RegionWorkOrder`. Candidate construction always uses `preparedPath.Segments[region.SegmentIndex]`; evaluation uses immutable `preparedPath` as raw reference and evolving `current` as splice input. - -After regional processing, validate the complete `current` path. On failure, roll accepted regions back in reverse acceptance order and revalidate after each rollback. Mark removed regions `GlobalValidationRollback`. Derive status after rollback only: - -```csharp -if (improvedCount == 0) - status = PathSmoothingStatus.Unchanged; -else if (improvedCount == regions.Count) - status = PathSmoothingStatus.Complete; -else - status = PathSmoothingStatus.PartialImprovement; -``` - -`Complete` and `PartialImprovement` require at least one `Improved` report. If every acceptance rolls back, publish the verified raw baseline. - -- [ ] **Step 6: Keep public service dispatch isolated** - -After common request validation, preparation, and `RawPathBaselineBuilder.TryCreate`, retain: - -```csharp -if (configuration.Method == SmoothingMethod.LocalG2Quintic) - return _localG2Pipeline.Smooth( - request, - preparedPath, - rawBaseline, - cancellationToken); -``` - -Validate `LocalG2QuinticOptions` only for Local G2. Legacy methods continue through `Resolve(configuration.Method)` and `_runner`; `PathSmoothingComparisonRequest.DefaultMethods` remains unchanged. - -- [ ] **Step 7: Complete Task 8 integration assertions** - -Run every fixture twice and require: - -```text -Straight -> NotNeeded -SingleTurn -> Complete -LargeHeadingChange -> Complete -SBend -> Complete or PartialImprovement -RectangleDetour -> Complete or PartialImprovement -MultiObstacleDetour -> Complete or PartialImprovement -ReverseGearSwitch -> Complete, PartialImprovement, or NotNeeded -``` - -Also require: - -```text -Tier 1 accepted -> evaluated CandidateCount <= 6 and no Tier 2 profile appears -Tier 1 rejected -> Tier 2 appears, CandidateCount <= 12 -CandidateCount equals the number actually evaluated -complete segment coverage and feasible diagnostics -configured collision/clearance checks on every published result -deterministic status, coordinates, candidate indices, and reports -strict 0.99 improvement request -> Unchanged -cancellation -> Cancelled with empty path -same-direction two-region processing -> back-to-front work, detector-order reports -global validation failure -> reverse rollback and safe raw fallback when all roll back -``` - -- [ ] **Step 8: Run focused and legacy GREEN checks** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_runner.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_integration.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 -powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1 -``` - -Expected: all pass in the valid source set. - -- [ ] **Step 9: Commit Task 8 publication files only** - -```powershell -git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_service.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "feat: publish tiered Local G2 presmoothing" -``` - -Expected staged names: exactly the four files above. `PathSmoothingService.cs` and its service test already contain provisional dirty hunks; stage only Local G2 Task 8 hunks and inspect the cached diff before committing. - ---- - -### Task 4: Enforce performance and final verification gates - -**Files:** - -- Create: `ClumsyPilot/tests/measure_path_smoothing_local_g2_performance.ps1` -- Verify: all files committed by Tasks 1–3 -- Verify: all `ClumsyPilot/tests/verify_path_smoothing_*.ps1` -- Record without committing: `.superpowers/sdd/local-g2-split-scale-task-8-final-report.md` - -**Interfaces:** - -- Consumes: a baseline assembly built from commit `bd08a9b` sources and the final assembly, the same `SingleTurn` fixture/configuration, and modes `Primary` and `Fallback`; the assembly path identifies baseline versus final code. -- Produces one JSON object per run: - -```json -{ - "Mode": "Primary", - "WarmupCount": 5, - "MeasurementCount": 30, - "P50Milliseconds": 0.0, - "P95Milliseconds": 0.0, - "MaximumAttemptCount": 6, - "MaximumEvaluatedCandidateCount": 6, - "MaximumRawWindowAnalysisCount": 4 -} -``` - -- [ ] **Step 1: Write the fixed benchmark harness** - -The script parameters are: - -```powershell -param( - [Parameter(Mandatory=$true)][string]$AssemblyPath, - [Parameter(Mandatory=$true)][ValidateSet('Primary','Fallback')][string]$Mode -) -``` - -After the parameter block, use this complete harness body: - -```powershell -$ErrorActionPreference = 'Stop' -$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' -if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } -$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) - -function Get-Type([string]$Name) { return $assembly.GetType($Name, $true) } -function Get-Property($Instance, [string]$Name) { - return $Instance.GetType().GetProperty( - $Name, [Reflection.BindingFlags]'Public,NonPublic,Instance').GetValue($Instance) -} -function Get-Method($Type, [string]$Name, [int]$ParameterCount) { - return @($Type.GetMethods([Reflection.BindingFlags]'Public,NonPublic,Instance,Static') | - Where-Object { $_.Name -eq $Name -and $_.GetParameters().Count -eq $ParameterCount })[0] -} -function New-Internal($Type) { - return [Activator]::CreateInstance( - $Type, [Reflection.BindingFlags]'Instance,NonPublic,Public', $null, @(), $null) -} -function Get-Percentile([double[]]$Values, [double]$Fraction) { - $ordered = @($Values | Sort-Object) - $index = [Math]::Ceiling($Fraction * $ordered.Count) - 1 - return [double]$ordered[[Math]::Max(0, [Math]::Min($ordered.Count - 1, $index))] -} - -$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' -$factoryType = Get-Type ($root + 'Test.SmoothingScenarioFactory') -$requestType = Get-Type ($root + 'PathSmoothingRequest') -$configurationType = Get-Type ($root + 'PathSmoothingConfiguration') -$methodType = Get-Type ($root + 'SmoothingMethod') -$preprocessorType = Get-Type ($root + 'Processing.PathSmoothingPreprocessor') -$optionsType = Get-Type ($root + 'LocalG2.LocalG2OptionsSnapshot') -$detectorType = Get-Type ($root + 'LocalG2.CurvatureTransitionDetector') -$plannerType = Get-Type ($root + 'LocalG2.LocalG2WindowPlanner') -$builderType = Get-Type ($root + 'LocalG2.LocalG2CandidateBuilder') -$evaluatorType = Get-Type ($root + 'LocalG2.LocalG2CandidateEvaluator') -$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json' -$fixtures = (Get-Method $factoryType 'CreateFixtureRequests' 1).Invoke( - $null, @((Resolve-Path $fixturePath).Path)) -$baseRequest = $fixtures[1].SmoothingRequest -$configuration = [Activator]::CreateInstance($configurationType) -$configuration.Method = [Enum]::Parse($methodType, 'LocalG2Quintic') -$request = [Activator]::CreateInstance($requestType, @( - $baseRequest.CoarsePath, - $baseRequest.Segments, - $baseRequest.Map, - $baseRequest.Vehicle, - $configuration)) -$preprocessor = [Activator]::CreateInstance($preprocessorType) -$prepareArgs = [object[]]@($request, $null, $null) -if (-not (Get-Method $preprocessorType 'TryPrepare' 3).Invoke($preprocessor, $prepareArgs)) { - throw 'SingleTurn preprocessing failed.' -} -$preparedPath = $prepareArgs[1] -$options = [Activator]::CreateInstance( - $optionsType, - [Reflection.BindingFlags]'Instance,NonPublic,Public', - $null, - @($configuration), - $null) -$detector = New-Internal $detectorType -$detectArgs = [object[]]@( - $request, - [double]$request.Vehicle.MaximumCurvaturePerMeter, - $options, - $null, - $null) -if (-not (Get-Method $detectorType 'TryDetect' 5).Invoke($detector, $detectArgs)) { - throw 'SingleTurn transition detection failed.' -} -$planner = New-Internal $plannerType -$planArgs = [object[]]@($preparedPath, $detectArgs[3], $options, $null, $null) -if (-not (Get-Method $plannerType 'TryPlan' 5).Invoke($planner, $planArgs)) { - throw 'SingleTurn window planning failed.' -} -$region = @($planArgs[3])[0] -$segmentIndex = [int](Get-Property $region 'SegmentIndex') -$preparedSegment = (Get-Property $preparedPath 'Segments')[$segmentIndex] -$beginBuild = @(Get-Method $builderType 'BeginBuild' 4) -$beginEvaluation = @(Get-Method $evaluatorType 'BeginRegionEvaluation' 5) -$isFinalApi = $beginBuild.Count -eq 1 -and $null -ne $beginBuild[0] - -function Invoke-OneMeasurement { - $builder = New-Internal $builderType - $evaluator = New-Internal $evaluatorType - $watch = [Diagnostics.Stopwatch]::StartNew() - $candidateList = [Collections.Generic.List[object]]::new() - $attemptCount = 0 - if ($isFinalApi) { - $buildSession = $beginBuild[0].Invoke($builder, @( - $preparedSegment, $region, [double]$configuration.OutputSpacingMeters, $options)) - $primary = (Get-Method ($buildSession.GetType()) 'BuildPrimary' 1).Invoke( - $buildSession, @([Threading.CancellationToken]::None)) - foreach ($candidate in $primary) { $candidateList.Add($candidate) } - if ($Mode -eq 'Fallback') { - $fallback = (Get-Method ($buildSession.GetType()) 'BuildFallback' 1).Invoke( - $buildSession, @([Threading.CancellationToken]::None)) - foreach ($candidate in $fallback) { $candidateList.Add($candidate) } - } - $attemptCount = [int](Get-Property $buildSession 'AttemptCount') - } - else { - $all = (Get-Method $builderType 'Build' 5).Invoke($builder, @( - $preparedSegment, - $region, - [double]$configuration.OutputSpacingMeters, - $options, - [Threading.CancellationToken]::None)) - $limit = if ($Mode -eq 'Primary') { 6 } else { 12 } - foreach ($candidate in $all) { - if ($candidateList.Count -ge $limit) { break } - $candidateList.Add($candidate) - } - $attemptCount = $candidateList.Count - } - - $rawWindowAnalysisCount = 0 - $evaluationSignature = [Collections.Generic.List[string]]::new() - if ($beginEvaluation.Count -eq 1 -and $null -ne $beginEvaluation[0]) { - $evaluationSession = $beginEvaluation[0].Invoke($evaluator, @( - $preparedPath, $preparedPath, $region, $request, $options)) - $sessionEvaluate = Get-Method ($evaluationSession.GetType()) 'Evaluate' 2 - foreach ($candidate in $candidateList) { - $evaluation = $sessionEvaluate.Invoke($evaluationSession, @( - $candidate, [Threading.CancellationToken]::None)) - $evaluationSignature.Add( - "$(Get-Property $candidate 'CandidateIndex'):$(Get-Property $evaluation 'Accepted'):$(Get-Property $evaluation 'FailureReason')") - } - $rawWindowAnalysisCount = [int](Get-Property $evaluationSession 'RawWindowAnalysisCount') - } - else { - $evaluate = Get-Method $evaluatorType 'Evaluate' 7 - foreach ($candidate in $candidateList) { - $evaluation = $evaluate.Invoke($evaluator, @( - $preparedPath, - $preparedPath, - $region, - $candidate, - $request, - $options, - [Threading.CancellationToken]::None)) - $evaluationSignature.Add( - "$(Get-Property $candidate 'CandidateIndex'):$(Get-Property $evaluation 'Accepted'):$(Get-Property $evaluation 'FailureReason')") - } - $rawWindowAnalysisCount = $candidateList.Count - } - $watch.Stop() - - $windows = @($candidateList | ForEach-Object { - '{0:R}|{1:R}' -f [double](Get-Property $_ 'StartArcLengthMeters'), - [double](Get-Property $_ 'EndArcLengthMeters') - } | Sort-Object -Unique) - return [PSCustomObject]@{ - ElapsedMilliseconds = $watch.Elapsed.TotalMilliseconds - AttemptCount = $attemptCount - EvaluatedCandidateCount = $candidateList.Count - RawWindowAnalysisCount = $rawWindowAnalysisCount - DistinctWindowCount = $windows.Count - Signature = ($evaluationSignature -join ';') - IsFinalApi = $isFinalApi - } -} - -for ($index = 0; $index -lt 5; $index++) { $null = Invoke-OneMeasurement } -$measurements = @() -for ($index = 0; $index -lt 30; $index++) { $measurements += Invoke-OneMeasurement } -$signature = $measurements[0].Signature -if (@($measurements | Where-Object Signature -ne $signature).Count -ne 0) { - throw 'Local G2 benchmark output is nondeterministic.' -} -$maximumAttempts = [int](($measurements | Measure-Object AttemptCount -Maximum).Maximum) -$maximumEvaluated = [int](($measurements | Measure-Object EvaluatedCandidateCount -Maximum).Maximum) -$maximumRaw = [int](($measurements | Measure-Object RawWindowAnalysisCount -Maximum).Maximum) -$maximumWindows = [int](($measurements | Measure-Object DistinctWindowCount -Maximum).Maximum) -if ($Mode -eq 'Primary' -and $maximumAttempts -gt 6) { throw 'Primary attempts exceed six.' } -if ($maximumAttempts -gt 12 -or $maximumEvaluated -gt 12) { throw 'Total candidate work exceeds twelve.' } -if ($maximumWindows -gt 4) { throw 'Representative windows exceed four.' } -if ($isFinalApi -and $maximumRaw -gt 4) { throw 'Final raw-window analyses exceed four.' } -$elapsed = [double[]]@($measurements | ForEach-Object ElapsedMilliseconds) -[PSCustomObject]@{ - Mode = $Mode - WarmupCount = 5 - MeasurementCount = 30 - P50Milliseconds = Get-Percentile $elapsed 0.50 - P95Milliseconds = Get-Percentile $elapsed 0.95 - MaximumAttemptCount = $maximumAttempts - MaximumEvaluatedCandidateCount = $maximumEvaluated - MaximumRawWindowAnalysisCount = $maximumRaw -} | ConvertTo-Json -Depth 3 -``` - -- [ ] **Step 2: Build comparable baseline and final assemblies** - -Create two disposable copies from the same current workspace, excluding `.git`, build outputs, `auto_avoidance`, and `.task8-sweep`. In the baseline copy, extract exactly these `bd08a9b` files over the copy: - -```text -ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs -ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs -ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs -ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs -``` - -The pipeline did not exist at the baseline commit. Resolve the baseline copy's absolute `LocalG2PreSmoothingPipeline.cs` path, assert it starts with the resolved disposable baseline root, and remove only that disposable file before building. Do not remove the shared-tree file. Build both copies with: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore --configuration Debug -``` - -Use the same machine, `Debug/netstandard2.0`, fixture JSON, map, vehicle, and request configuration for both. - -- [ ] **Step 3: Run and compare 5+30 performance measurements** - -Run the benchmark in separate PowerShell processes so the two `ClumsyPilot.dll` versions do not collide in one load context. Capture baseline primary/fallback and final primary/fallback JSON. Assert: - -```powershell -if ($finalPrimary.P95Milliseconds -gt 1.5 * $baselinePrimary.P95Milliseconds) { - throw 'Local G2 primary P95 exceeds 1.5x baseline.' -} -if ($finalFallback.P95Milliseconds -gt 2.0 * $baselineFallback.P95Milliseconds) { - throw 'Local G2 fallback P95 exceeds 2.0x baseline.' -} -``` - -Record all four P50/P95 values, ratios, attempt/evaluation/raw-analysis maxima, machine, commit IDs, build configuration, fixture, warmup count, and measurement count. A wall-clock regression blocks merge but must not become a production timeout. Fix duplicate analysis or scheduling overhead; do not remove safety/quality checks. - -- [ ] **Step 4: Commit the benchmark harness** - -```powershell -git add -- ClumsyPilot/tests/measure_path_smoothing_local_g2_performance.ps1 -git diff --cached --check -git diff --cached --name-only -git commit -m "test: benchmark bounded Local G2 recovery" -``` - -Expected staged name: exactly the benchmark script. - -- [ ] **Step 5: Build the final source and run all PathSmoothing scripts** - -Run: - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -$tests = @( - 'verify_path_smoothing_algorithm_input.ps1', - 'verify_path_smoothing_bezier.ps1', - 'verify_path_smoothing_bspline.ps1', - 'verify_path_smoothing_comparison.ps1', - 'verify_path_smoothing_contracts.ps1', - 'verify_path_smoothing_documentation.ps1', - 'verify_path_smoothing_fixtures.ps1', - 'verify_path_smoothing_geometry.ps1', - 'verify_path_smoothing_integration.ps1', - 'verify_path_smoothing_local_g2_candidates.ps1', - 'verify_path_smoothing_local_g2_curve.ps1', - 'verify_path_smoothing_local_g2_detection.ps1', - 'verify_path_smoothing_local_g2_integration.ps1', - 'verify_path_smoothing_png.ps1', - 'verify_path_smoothing_quintic.ps1', - 'verify_path_smoothing_runner.ps1', - 'verify_path_smoothing_service.ps1', - 'verify_path_smoothing_svg_csv.ps1', - 'verify_path_smoothing_validation.ps1' -) -foreach ($test in $tests) { - & powershell -ExecutionPolicy Bypass -File (Join-Path 'ClumsyPilot/tests' $test) - if ($LASTEXITCODE -ne 0) { throw "$test failed with exit code $LASTEXITCODE" } -} -``` - -Expected: zero build errors and all 19 scripts pass in the valid source set. Record any shared-build limitation plus the isolated-copy evidence if `auto_avoidance` is the sole unrelated blocker. - -- [ ] **Step 6: Audit safety, scope, determinism, and Task 9 boundary** - -Run: - -```powershell -git diff --check -git diff --cached --check -git status --short -git log -8 --oneline -rg -n "CurvatureRangeTolerance|MinimumClearanceReserveMeters|MinimumPeakGradientImprovementRatio|MaximumVariationCostRegressionRatio|MaximumCandidatesPerRegion" ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2 ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs -``` - -Confirm no safety value changed, no staged files remain, unrelated dirty files are untouched, Local G2 is absent from comparison defaults, repeated integration runs match exactly, and Task 9 README/documentation work has not started. - -- [ ] **Step 7: Record final evidence without an empty commit** - -Write `.superpowers/sdd/local-g2-split-scale-task-8-final-report.md` with the accepted `SingleTurn` tuple and scales, zero anchor error, G2 errors, unchanged safety/quality metrics, Tier 1/Tier 2 counts, raw-window cache counts, fixture statuses, rollback/cancellation results, performance P50/P95 ratios, build result, and `19/19` script result. Do not create a verification-only commit. diff --git a/docs/superpowers/plans/2026-08-02-local-g2-diagnostic-visualization.md b/docs/superpowers/plans/2026-08-02-local-g2-diagnostic-visualization.md deleted file mode 100644 index dfd9fae..0000000 --- a/docs/superpowers/plans/2026-08-02-local-g2-diagnostic-visualization.md +++ /dev/null @@ -1,558 +0,0 @@ -# Local G2 Diagnostic Visualization Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Export a factual Local G2 visualization that shows the ordinary strict `Unchanged` result and the recorded clearance-rejected diagnostic candidate without publishing or recommending that candidate. - -**Architecture:** The existing comparison service remains the source for normal Local G2 status and output. A test/demo-only evidence loader reads a compact immutable record of `single-turn/s0/r0/w5/seed2`, reconstructs a visual-only spliced path with the existing preprocessor/splicer/analyzer, and appends it to an immutable figure model. The existing SVG/PNG/CSV exporter then produces the six standard figures plus a seventh diagnostic figure only for that augmented model. - -**Tech Stack:** C# 10 targeting `netstandard2.0`, Newtonsoft.Json 13.0.4 already referenced by `ClumsyPilot.csproj`, existing `System.Drawing` PNG renderer, PowerShell verification scripts, .NET 10 Windows verification host. - -## Global Constraints - -- Do not modify `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/`, evaluator, validator, publication, or recommendation logic. -- The normal comparison must retain the actual Local G2 `PathSmoothingStatus`; a diagnostic candidate must never create a `PathSmoothingResult` or a recommendation. -- Evidence is fixed to fixture SHA-256 `3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563`, batch SHA-256 `ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed`, and stable key `single-turn/s0/r0/w5/seed2`. -- Preserve normal-export file stems `01-coarse-path-overview` through `06-curvature-comparison` and its six-file contract. -- The diagnostic candidate must be labelled `净空拒绝,未发布`; do not render a collision cross because the evidence records a clearance rejection, not an occupied-cell collision. -- Keep trajectories point-only. Do not add SVG paths or dashed stroke rendering. -- Place generated artifacts only below `ClumsyPilot/obj/path_smoothing_reports`. -- Use targeted `git add -- ` and `git commit --only -- `; do not include unrelated worktree changes. - ---- - -## File Structure - -| Path | Responsibility | -| --- | --- | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs` | Adds normal Local G2 to the immutable default offline comparison order. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs` | Holds normal Local G2 and diagnostic-candidate colors. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` | Adds normal Local G2 series and CSV metric row. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` | Creates an immutable model copy with one added diagnostic series. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` | Moves a five-entry legend upward enough to remain inside the fixed figure height. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` | Names the optional seventh figure. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` | Adds Local G2 to normal comparison figures and conditionally creates figure 07. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json` | Immutable compact evidence extract used by the visual-only route. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs` | Parses and verifies evidence identity, geometry, and rejection state. | -| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` | Reconstructs renderable candidate geometry and invokes the existing exporter. | -| `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` | Verifies and exports the augmented seven-file report. | -| `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` | Checks Local G2 default order and normal comparison semantics. | -| `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` | Checks normal Local G2 figure/CSV content and visual layout contracts. | -| `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1` | Checks evidence parsing and deterministic rejection validation. | -| `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1` | Calls the verification host for the seven-file diagnostic report. | -| `ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1` | Builds and exports the user-facing Local G2 diagnostic image. | - -## Task 1: Add Normal Local G2 To Existing Comparison Reports - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` -- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: `PathSmoothingComparisonRequest(PathSmoothingRequest, IReadOnlyList methods = null)` and `SmoothingFigureModelBuilder.Build(PathSmoothingComparisonResult, PlanningGridMap, Pose2D, Pose2D, string, string)`. -- Produces: A default method order of `CubicBSpline`, `LocalCubicBezier`, `PiecewiseQuintic`, `LocalG2Quintic`; figure key `local-g2`; CSV method `LocalG2Quintic`; unchanged standard six-figure export count. - -- [ ] **Step 1: Write the failing default-order and standard-model assertions** - - In `verify_path_smoothing_comparison.ps1`, parse the Local G2 enum and construct a request without the optional methods list. Add these assertions after the explicit three-method request assertions: - - ```powershell - $localG2 = [Enum]::Parse($methodType, 'LocalG2Quintic') - $defaultComparisonRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-SmoothingRequest), $null)) - Assert-Equal 4 $defaultComparisonRequest.Methods.Count 'Default comparison must include Local G2.' - Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic,LocalG2Quintic' (($defaultComparisonRequest.Methods | ForEach-Object ToString) -join ',') 'Default comparison order must be stable.' - ``` - - In `verify_path_smoothing_svg_csv.ps1`, add `#56B4E9` to the expected normal SVG colors, assert that `$model.Series` includes a series whose `Key` is `local-g2`, and assert that the generated CSV contains `LocalG2Quintic,Unchanged` for the frozen `single-turn` request. Add assertions that the standard figure set still contains only the six existing stems. - - In `Program.cs`, add a `VerifyNormalLocalG2Series(SmoothingFigureModel model)` call immediately after `VerifySixFigureDefinitionContract(model)`. It must require exactly one `local-g2` series and a `PathSmoothingStatus` value defined by the enum. If its strict output path is visible, require that it has at least two samples. Do not hard-code `Unchanged` for this artificial high-curvature host fixture; the frozen `single-turn` evidence assertion in the SVG/CSV test owns that requirement. - -- [ ] **Step 2: Run the focused checks and confirm they fail before implementation** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 - ``` - - Expected: the comparison assertion reports three default methods and the SVG/CSV check cannot find `local-g2` or `#56B4E9`. - -- [ ] **Step 3: Implement the smallest normal-comparison extension** - - Append the enum in the existing default array, preserving all existing order: - - ```csharp - private static readonly SmoothingMethod[] DefaultMethods = - { - SmoothingMethod.CubicBSpline, - SmoothingMethod.LocalCubicBezier, - SmoothingMethod.PiecewiseQuintic, - SmoothingMethod.LocalG2Quintic, - }; - ``` - - Add these fixed colors to `IeeeFigureStyle`: - - ```csharp - public const string LocalG2Color = "#56B4E9"; - public const string LocalG2DiagnosticColor = "#B1373E"; - ``` - - In `SmoothingFigureModelBuilder`, append the normal series and metric row after the existing piecewise-quintic entries. Use the exact stable key and label: - - ```csharp - series.Add(CreateSeries( - Find(comparison, SmoothingMethod.LocalG2Quintic), - SmoothingMethod.LocalG2Quintic, - "local-g2", - "局部 G2", - IeeeFigureStyle.LocalG2Color, - string.Empty, - false, - map)); - - CreateRow(Find(comparison, SmoothingMethod.LocalG2Quintic), "LocalG2Quintic", "局部 G2") - ``` - - In `SmoothingFigureSetBuilder.Build`, resolve `local-g2` with the other standard series and include it in only the existing all-path and curvature comparisons: - - ```csharp - SmoothingFigureSeries localG2 = Find(model, "local-g2"); - // Add View(localG2, 1d) after View(quintic, 1d) in figures 02 and 06. - ``` - - Keep individual figures `03` through `05` unchanged. In `SmoothingFigureDefinition`, use a five-entry-safe legend origin: - - ```csharp - public double LegendYPoints => LegendEntries.Count > 4 ? 332d : 340d; - ``` - - Do not special-case `Unchanged`: `PathSmoothingComparisonService` already supplies its strict path and status. Do not modify that service or the ranker. - -- [ ] **Step 4: Run the focused checks and confirm they pass** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 - ``` - - Expected: all three scripts exit `0`; normal exports retain exactly six figures; the Local G2 row is present with its actual `Unchanged` status. - -- [ ] **Step 5: Commit only Task 1 files** - - ```powershell - git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` - ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 ` - ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs - git -c core.autocrlf=false diff --cached --check - git commit --only -m "feat: show Local G2 in smoothing comparisons" -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` - ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 ` - ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs - ``` - -### Task 2: Freeze And Validate The Diagnostic Evidence Extract - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1` - -**Interfaces:** -- Produces: `public sealed class LocalG2DiagnosticEvidenceLoader` with `public LocalG2DiagnosticEvidence LoadAndVerify(string evidencePath)`. -- Produces: `LocalG2DiagnosticEvidence` properties `ScenarioId`, `FixtureSha256`, `CandidateStableKey`, `CandidateSha256`, `CandidateIndex`, `SegmentIndex`, `WindowStartArcLengthMeters`, `WindowEndArcLengthMeters`, start/end curvatures, `CandidatePoints`, `EvaluatorResult`, `StopGate`, and `PublishedStatus`. -- Consumed later by: `LocalG2DiagnosticVisualizationDemo.Export(string fixturePath, string evidencePath, string outputDirectory, CancellationToken cancellationToken = default)`. - -- [ ] **Step 1: Write the failing evidence-loader verification script** - - Create `verify_path_smoothing_local_g2_diagnostic_evidence.ps1`. Load `ClumsyPilot.dll`, resolve `MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.LocalG2DiagnosticEvidenceLoader`, and invoke `LoadAndVerify` with the new fixture path. Assert all of these exact values: - - ```powershell - Assert-Equal 'single-turn' $evidence.ScenarioId 'Diagnostic evidence scenario must be stable.' - Assert-Equal 'single-turn/s0/r0/w5/seed2' $evidence.CandidateStableKey 'Diagnostic evidence key must be stable.' - Assert-Equal 5 $evidence.CandidateIndex 'Diagnostic evidence candidate index must be stable.' - Assert-Equal 10 $evidence.CandidatePoints.Count 'Diagnostic evidence must retain all ten recorded samples.' - Assert-Equal 'InsufficientClearance' $evidence.EvaluatorResult 'Diagnostic evidence must retain the observed evaluator result.' - Assert-Equal 'Clearance' $evidence.StopGate 'Diagnostic evidence must retain the observed stop gate.' - Assert-Equal 'Unchanged' $evidence.PublishedStatus 'Diagnostic evidence must retain the strict final status.' - ``` - - Copy the JSON to a uniquely named temp path, replace only `"StopGate": "Clearance"` with `"StopGate": "Collision"`, and require that `LoadAndVerify` throws. Delete the temp copy in `finally`. - -- [ ] **Step 2: Run the evidence check and confirm it fails before implementation** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1 - ``` - - Expected: failure because the loader type and evidence file do not exist. - -- [ ] **Step 3: Add the immutable compact evidence extract and loader** - - Create the JSON file with this exact top-level contract and the ten recorded `CandidatePoints`. Preserve the displayed IEEE-754 decimal values; they are the evidence values, not rounded drawing inputs: - - ```json - { - "SourceMeasurementBatchSha256": "ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed", - "FixtureSha256": "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563", - "ScenarioId": "single-turn", - "CandidateStableKey": "single-turn/s0/r0/w5/seed2", - "CandidateSha256": "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392", - "CandidateIndex": 5, - "SegmentIndex": 0, - "WindowStartArcLengthMeters": 1.9199999999999982, - "WindowEndArcLengthMeters": 2.1199999999999983, - "StartGeometricCurvaturePerMeter": 0.41666666666666663, - "EndGeometricCurvaturePerMeter": 0.0, - "StartVehicleCurvaturePerMeter": 0.41666666666666663, - "EndVehicleCurvaturePerMeter": 0.0, - "EvaluatorResult": "InsufficientClearance", - "StopGate": "Clearance", - "PublishedStatus": "Unchanged", - "CandidatePoints": [ - { "X": 2.7216397035494579, "Y": 1.7279184433567971, "ReferenceArcLengthMeters": 1.9199999999999982, "HeadingRadians": 0.799999999999999, "UnwrappedHeadingRadians": 0.799999999999999, "Source": 4 }, - { "X": 2.7355188205174521, "Y": 1.7423187041196302, "ReferenceArcLengthMeters": 1.9399999999999982, "HeadingRadians": 0.80751185024996908, "UnwrappedHeadingRadians": 0.80751185024996908, "Source": 4 }, - { "X": 2.7492871654421882, "Y": 1.7568248978050907, "ReferenceArcLengthMeters": 1.9599999999999982, "HeadingRadians": 0.8157618125921976, "UnwrappedHeadingRadians": 0.8157618125921976, "Source": 4 }, - { "X": 2.7629234692741469, "Y": 1.7714552535786874, "ReferenceArcLengthMeters": 1.9799999999999982, "HeadingRadians": 0.82542852165703573, "UnwrappedHeadingRadians": 0.82542852165703573, "Source": 4 }, - { "X": 2.7764244476704873, "Y": 1.786210614200662, "ReferenceArcLengthMeters": 1.9999999999999982, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 }, - { "X": 2.7925350453990334, "Y": 1.803999635249544, "ReferenceArcLengthMeters": 2.0239999999999982, "HeadingRadians": 0.835253334801853, "UnwrappedHeadingRadians": 0.835253334801853, "Source": 4 }, - { "X": 2.80865418003672, "Y": 1.8217809211927092, "ReferenceArcLengthMeters": 2.0479999999999983, "HeadingRadians": 0.83333333355178374, "UnwrappedHeadingRadians": 0.83333333355178374, "Source": 4 }, - { "X": 2.8248074262445737, "Y": 1.8395312269498194, "ReferenceArcLengthMeters": 2.0719999999999983, "HeadingRadians": 0.8318933325232104, "UnwrappedHeadingRadians": 0.8318933325232104, "Source": 4 }, - { "X": 2.8409691920174533, "Y": 1.8572737784943611, "ReferenceArcLengthMeters": 2.0959999999999983, "HeadingRadians": 0.83237333274470626, "UnwrappedHeadingRadians": 0.83237333274470626, "Source": 4 }, - { "X": 2.8571139169604551, "Y": 1.8750318365841865, "ReferenceArcLengthMeters": 2.1199999999999983, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 } - ] - } - ``` - - Implement the public loader in the test namespace using `Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(evidencePath))`. Use `InvalidDataException` for every rejected input. The validation must require the three exact SHA/key constants, `ScenarioId == "single-turn"`, `CandidateIndex == 5`, `SegmentIndex == 0`, `EvaluatorResult == "InsufficientClearance"`, `StopGate == "Clearance"`, and `PublishedStatus == "Unchanged"`. - - Validate the two window values and all point coordinates/headings/reference arc lengths with this helper: - - ```csharp - private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); - ``` - - Require exactly ten points, first/last reference arcs equal the window endpoints within `1e-8d`, strictly increasing reference arcs, `Source == (int)SmoothedPathPointSource.LocalG2Transition`, and non-null start/end curvature values. Return only after all checks pass. - -- [ ] **Step 4: Run the evidence verification and confirm it passes** - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1 - ``` - - Expected: exit `0`; the valid extract loads and the altered stop gate is rejected. - -- [ ] **Step 5: Commit only Task 2 files** - - ```powershell - git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 - git -c core.autocrlf=false diff --cached --check - git commit --only -m "test: freeze Local G2 diagnostic evidence" -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 - ``` - -### Task 3: Build The Visual-Only Candidate And Optional Figure 07 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` -- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` -- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1` - -**Interfaces:** -- Produces: `internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series)` that preserves all existing model layout, metric rows, endpoints, and scales. -- Produces: `SmoothingFigureKind.LocalG2DiagnosticCandidate` and optional stem `07-local-g2-diagnostic-candidate`. -- Produces: `public SmoothingReportExportResult LocalG2DiagnosticVisualizationDemo.Export(string fixturePath, string evidencePath, string outputDirectory, CancellationToken cancellationToken = default)`. -- Produces: a host verification command that accepts a fixture path and evidence path, plus an export command that accepts fixture, evidence, and output-directory paths. - -- [ ] **Step 1: Write the failing seven-file verification host branch and wrapper script** - - In `Program.Main`, add command dispatch before the existing fixture/export cases: - - ```csharp - if (arguments.Length == 3 && arguments[0] == "--verify-local-g2-diagnostic") - { - VerifyLocalG2Diagnostic(arguments[1], arguments[2]); - Console.WriteLine("Local G2 diagnostic visualization verification completed."); - return 0; - } - if (arguments.Length == 4 && arguments[0] == "--export-local-g2-diagnostic") - { - ExportLocalG2Diagnostic(arguments[1], arguments[2], arguments[3]); - return 0; - } - ``` - - Create the wrapper script to resolve the fixture and evidence paths and run this command: - - ```powershell - & dotnet run --project $hostProject --no-restore -- --verify-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ``` - - Run it before the demo/model implementation. Expected: build error because `VerifyLocalG2Diagnostic` and `ExportLocalG2Diagnostic` do not yet exist. - -- [ ] **Step 2: Add immutable model augmentation and the optional figure definition** - - Add this internal method to `SmoothingFigureModel`: - - ```csharp - internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series) - { - if (series == null) throw new ArgumentNullException(nameof(series)); - var combined = new List(Series.Count + 1); - for (int index = 0; index < Series.Count; index++) - { - if (Series[index].Key == series.Key) - throw new ArgumentException("Figure series keys must be unique.", nameof(series)); - combined.Add(Series[index]); - } - combined.Add(series); - var copy = new SmoothingFigureModel( - ScenarioId, ScenarioLabel, WorldXMinMeters, WorldXMaxMeters, WorldYMinMeters, WorldYMaxMeters, - PathPanelX, PathPanelY, PathPanelWidth, PathPanelHeight, - CurvaturePanelX, CurvaturePanelY, CurvaturePanelWidth, CurvaturePanelHeight, - MetricsPanelX, MetricsPanelY, MetricsPanelWidth, MetricsPanelHeight, - Obstacles, combined, MetricRows, Start, Goal) - { - PathScaleX = PathScaleX, - PathScaleY = PathScaleY, - }; - return copy; - } - ``` - - Add `LocalG2DiagnosticCandidate` to `SmoothingFigureKind`. In `SmoothingFigureSetBuilder.Build`, retain the six normal definitions first. Use a non-throwing `TryFind` helper for key `local-g2-diagnostic`; when it succeeds, append exactly this overhead figure: - - ```csharp - BuildOverhead( - SmoothingFigureKind.LocalG2DiagnosticCandidate, - "07-local-g2-diagnostic-candidate", - "G2 诊断候选:净空拒绝,未发布;严格输出=原始路径", - model, - true, - View(raw, 0.45d), - View(diagnostic, 1d)) - ``` - - The diagnostic series must carry key `local-g2-diagnostic`, status `PathSmoothingStatus.Infeasible`, color `IeeeFigureStyle.LocalG2DiagnosticColor`, and an empty `ViolationMarkers` list. Do not reuse `SmoothingFigureModelBuilder.CreateSeries`, because its generic `Infeasible` behavior synthesizes a violation cross when it cannot identify an occupied point. - -- [ ] **Step 3: Implement `LocalG2DiagnosticVisualizationDemo` with the existing geometry components** - - The class stays in namespace `MultiWheelC.TrajectoryPlanning.PathSmoothing.Test` and creates no `PathSmoothingResult`. Its `Export` method must execute this exact sequence: - - ```csharp - LocalG2DiagnosticEvidence evidence = _evidenceLoader.LoadAndVerify(evidencePath); - PathSmoothingComparisonRequest request = FindFixtureRequest(fixturePath, evidence.ScenarioId); - PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken); - PathSmoothingComparisonEntry localG2 = FindEntry(comparison, SmoothingMethod.LocalG2Quintic); - Require(localG2 != null && localG2.Status == PathSmoothingStatus.Unchanged, - "Strict Local G2 result must be Unchanged for the diagnostic evidence."); - RequireSameGeometry(comparison.RawPathBaseline.Path, localG2.Path); - _preprocessor.TryPrepare(request.SmoothingRequest, out PreparedPath prepared, out string reason); - LocalG2CandidateGeometry candidate = CreateCandidate(evidence); - _splicer.TryReplace(prepared, candidate, out PreparedPath spliced, out reason); - _analyzer.TryAnalyze(spliced.Segments, request.SmoothingRequest.Configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason); - CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; - CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; - SmoothingFigureModel normal = _figureBuilder.Build( - comparison, - request.SmoothingRequest.Map, - new Pose2D(first.X, first.Y, first.Heading), - new Pose2D(last.X, last.Y, last.Heading), - evidence.ScenarioId, - evidence.ScenarioId); - SmoothingFigureModel augmented = normal.WithAdditionalSeries(CreateDiagnosticSeries(analysis.Path)); - return _exporter.Export(new SmoothingReportExportRequest { Model = augmented, OutputDirectory = outputDirectory, FileStem = "comparison" }); - ``` - - `FindFixtureRequest` must call `SmoothingScenarioFactory.CreateFixtureRequests(fixturePath)`, locate exactly one request by the matching fixture index from `SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath)`, and reject missing or duplicate `single-turn` IDs. `CreateCandidate` must turn every evidence point into: - - ```csharp - new SmoothingPoint2D( - point.X, point.Y, point.ReferenceArcLengthMeters, - point.HeadingRadians, point.UnwrappedHeadingRadians, - 0d, false, SmoothedPathPointSource.LocalG2Transition) - ``` - - Construct `LocalG2CandidateGeometry` with evidence index/window/curvatures, `0d` left and right lengths, the converted point list, and `true` for `internalConnectionsAreG2`. The zero clearance exists only to satisfy geometry-analysis input validity; it must not be fed to a validator or a metric row. - - `CreateDiagnosticSeries` must convert `analysis.Path` to `SmoothingFigurePoint` values, use label `G2 候选(净空拒绝,未发布)`, and supply `Array.Empty()` as violation markers. Its path can be visualized but is not a published/safe path. - -- [ ] **Step 4: Implement host verification and run it** - - `VerifyLocalG2Diagnostic` must create a fresh temp output directory, invoke the demo, and require all of the following before deleting the directory in `finally`: - - ```csharp - Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); - Require(report.SvgPaths.Count == 7 && report.PngPaths.Count == 7 && File.Exists(report.CsvPath), "Diagnostic export must publish seven SVGs, seven PNGs and one CSV."); - Require(Path.GetFileName(report.SvgPaths[6]) == "07-local-g2-diagnostic-candidate.svg", "Diagnostic SVG stem is incorrect."); - Require(Path.GetFileName(report.PngPaths[6]) == "07-local-g2-diagnostic-candidate.png", "Diagnostic PNG stem is incorrect."); - string diagnosticSvg = File.ReadAllText(report.SvgPaths[6]); - Require(diagnosticSvg.Contains("data-series=\"raw\"") && diagnosticSvg.Contains("data-series=\"local-g2-diagnostic\""), "Diagnostic SVG must contain raw and diagnostic samples."); - Require(diagnosticSvg.Contains("净空拒绝") && diagnosticSvg.Contains("未发布"), "Diagnostic SVG must disclose rejection and publication state."); - Require(!diagnosticSvg.Contains("violation-cross"), "Clearance rejection must not be drawn as an obstacle collision."); - Require(File.ReadAllText(report.CsvPath).Contains("LocalG2Quintic,Unchanged"), "CSV must retain the normal strict Local G2 row."); - ``` - - Run `VerifyPng(File.ReadAllBytes(path))` for every returned PNG and use the existing `ContainsTemporaryFiles` helper to assert atomic publication. Also copy the evidence file to a temp file, alter the fixture hash, call the demo with a separate empty output path, require an exception, and require that the output directory was never created. - - Run: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_visualization.ps1 - ``` - - Expected: exit `0`, seven valid figures, raw and diagnostic series in `07`, no fabricated collision cross, and bad evidence rejected atomically. - -- [ ] **Step 5: Commit only Task 3 files** - - ```powershell - git add -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs ` - ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 - git -c core.autocrlf=false diff --cached --check - git commit --only -m "feat: export Local G2 diagnostic candidate" -- ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` - ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs ` - ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs ` - ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 - ``` - -### Task 4: Add The User-Facing Export Script And Perform Full Acceptance - -**Files:** -- Create: `ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_png.ps1` - -**Interfaces:** -- Consumes: the diagnostic host export command, its fixture path, its evidence path, and its bounded output directory. -- Produces: `ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn/07-local-g2-diagnostic-candidate.png` and six companion standard figures. - -- [ ] **Step 1: Write the failing runner assertion in the PNG smoke test** - - In `verify_path_smoothing_png.ps1`, add a call to the not-yet-created runner with an explicit output directory below `obj/path_smoothing_reports`, followed by an existence assertion for its primary PNG. Its default runner paths must be: - - ```powershell - [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), - [string]$EvidencePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json'), - [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-single-turn') - ``` - - Add this smoke-test block after the existing host invocation, changing no unrelated test behavior: - - ```powershell - $runnerPath = Join-Path $PSScriptRoot 'run_local_g2_diagnostic_visualization.ps1' - $runnerOutput = Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-png-smoke' - & powershell -ExecutionPolicy Bypass -File $runnerPath -OutputDirectory $runnerOutput - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $primaryPng = Join-Path $runnerOutput '07-local-g2-diagnostic-candidate.png' - if (-not (Test-Path -LiteralPath $primaryPng)) { - throw "Local G2 diagnostic runner did not publish $primaryPng" - } - ``` - - Run the PNG smoke test. Expected: PowerShell reports that `run_local_g2_diagnostic_visualization.ps1` does not exist, so the runner acceptance assertion fails before implementation. - -- [ ] **Step 2: Implement the bounded export script and extend the PNG smoke test** - - Follow the existing `run_path_smoothing_comparison.ps1` root validation exactly: resolve `$clumsyPilotRoot`, require `$OutputDirectory` to equal or be below `$clumsyPilotRoot\obj\path_smoothing_reports`, resolve both input files, build `ClumsyPilot.csproj`, then execute: - - ```powershell - & dotnet run --project $hostProject --no-restore -- --export-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath $resolvedOutputDirectory - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Write-Output "Local G2 diagnostic visualization written below $resolvedOutputDirectory" - ``` - - In `verify_path_smoothing_png.ps1`, call the new diagnostic verification wrapper after the existing host verification so both standard six-file and diagnostic seven-file image contracts run in the normal PNG check. Retain the runner call and primary-PNG existence assertion added in Step 1; use a `local-g2-png-smoke` output subdirectory below the allowed report root. - -- [ ] **Step 3: Run the full automated acceptance sequence** - - Run these commands in order and inspect every exit code: - - ```powershell - dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_visualization.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 - powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\run_local_g2_diagnostic_visualization.ps1 - ``` - - Expected: every command exits `0`; the primary PNG, matching SVG, and `comparison.csv` exist under `ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn`. - -- [ ] **Step 4: Inspect the generated image and report the factual result** - - Open the primary file with the local image viewer: - - ```text - ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn/07-local-g2-diagnostic-candidate.png - ``` - - Confirm visually that the map is nonblank, the gray raw/final path and red diagnostic candidate are both visible, the legend/title disclose `净空拒绝` and `未发布`, there is no collision cross, and text remains inside the 4296-by-3120 PNG frame. Also open `02-all-paths-comparison.png` and `06-curvature-comparison.png` from the same directory to confirm that normal Local G2 appears with `Unchanged` alongside the existing methods. - -- [ ] **Step 5: Commit only Task 4 source/test files** - - Do not commit generated `obj` artifacts. Commit only the runner and PNG smoke-test changes: - - ```powershell - git add -- ` - ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_png.ps1 - git -c core.autocrlf=false diff --cached --check - git commit --only -m "test: add Local G2 diagnostic visualization runner" -- ` - ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 ` - ClumsyPilot/tests/verify_path_smoothing_png.ps1 - ``` - -## Final Verification Checklist - -- [ ] Re-read [`2026-08-02-local-g2-diagnostic-visualization-design.md`](../specs/2026-08-02-local-g2-diagnostic-visualization-design.md) and map every acceptance criterion to a passing command or visual check above. -- [ ] Run `git -c core.autocrlf=false diff --check` only on the files changed by these tasks. -- [ ] Verify that no `PathSmoothing/LocalG2/` implementation, evaluator, validator, publishing, or ranking file changed. -- [ ] Verify each task commit contains only its listed paths. -- [ ] Report the primary image path and state plainly that the red curve is a rejected diagnostic candidate, while the strict Local G2 result remains `Unchanged`. diff --git a/docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md b/docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md deleted file mode 100644 index f300c8a..0000000 --- a/docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md +++ /dev/null @@ -1,915 +0,0 @@ -# Daily Summary Job Domain Visualization Upgrade Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` (recommended) or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Upgrade `daily-summary-job` so it understands the current task, renders the function or algorithm's real domain effect, and connects visible objects to the current problem, cause, consequence, correction, expected result, and task-matched verification evidence. - -**Architecture:** Extend the normalized report facts with optional algorithm views and task-specific validation facts. Keep one diagnostic interaction shell, but render its central canvas through a declarative adapter registry selected from the current task's semantics. Split maintainable CSS and JavaScript assets during skill development, then inline them into the final self-contained HTML during rendering. - -**Tech Stack:** Python 3.12 standard library, `unittest`, HTML5, CSS, vanilla JavaScript, SVG/DOM, Node syntax checks. - -## Global Constraints - -- Implement the approved design in `docs/superpowers/specs/2026-08-03-daily-summary-job-domain-visualization-upgrade-design.md`. -- Modify the personal skill at `C:\Users\admin\.codex\skills\daily-summary-job`; request filesystem approval when the execution environment requires it. -- Do not hard-code trajectory planning as the meaning of algorithm visualization. Select the view from the current task's purpose, observable business objects, inputs, outputs, and correctness constraints. -- A generic flow diagram may assist navigation, but it must not replace a domain effect view when spatial, numeric, temporal, state, search, or structured-data evidence exists. -- Distinguish actual observation, static reconstruction, conceptual preview, verified result, and conflicting evidence in both data and presentation. -- Build validation scenarios from the current task and its correctness constraints. Do not substitute an unrelated fixed test matrix. -- Preserve reports that omit the new optional fields. -- Final HTML must contain no external resource or network dependency. -- Do not modify business code, run expensive tests by default, stage changes, or create Git commits. -- Preserve the current UTF-8, date/module classification, checkpoint limits, stable issue IDs, and Markdown/HTML pairing behavior. - -## User-Approved Scope Adjustment - -This implementation ships only the generic declarative `composite-scene` renderer and the unified diagnosis shell. Do not implement dedicated `spatial-scene`, `cartesian-series`, `graph-network`, `state-machine`, or `data-flow` renderers in this round. Keep the adapter registry as an extension point, so future task-specific work can add those renderers without changing the report contract. - ---- - -## File Structure - -**Modify** - -- `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` — task understanding, visualization-brief generation, domain-view selection, and task-matched validation workflow. -- `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md` — optional algorithm-view, issue-target, solution-preview, verified-result, and task-validation contracts. -- `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py` — validate new facts, render the Markdown algorithm section, bundle assets, and validate rendered links. -- `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` — schema, backward compatibility, Markdown, asset bundling, adapter, interaction, and CLI regression tests. -- `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html` — diagnostic-shell markup and asset placeholders. -- `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` — UI description and default prompt for task-matched domain visualization. - -**Create** - -- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css` — domain canvas, view modes, diagnostic drawer, evidence states, responsiveness, and reduced-motion styles. -- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js` — declarative scene-object renderer registry and built-in layout strategies. -- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js` — report state, selectors, view switching, target highlighting, diagnosis rendering, and keyboard interaction. - -The three development assets are embedded into every rendered report. They must never remain as runtime `` or ` - - -``` - -- [ ] **Step 4: Implement the asset loader and renderer replacement** - -```python -VISUAL_ASSET_FILES = { - "__VISUAL_STYLES__": "visualization-styles.css", - "__VISUAL_ADAPTERS__": "visualization-adapters.js", - "__VISUAL_RUNTIME__": "visualization-runtime.js", -} - - -def load_visual_assets(asset_dir: Path | None = None) -> dict[str, str]: - root = Path(asset_dir or Path(__file__).parent.parent / "assets") - return { - placeholder: (root / filename).read_text(encoding="utf-8") - for placeholder, filename in VISUAL_ASSET_FILES.items() - } - - -def render_html( - data: dict[str, Any], - template: str, - visual_assets: dict[str, str] | None = None, -) -> str: - validate_report_data(data) - replacements = { - "__REPORT_DATA__": safe_json_for_html(data), - **(visual_assets or load_visual_assets()), - } - rendered = template - for placeholder, value in replacements.items(): - if rendered.count(placeholder) != 1: - label = "report data placeholder" if placeholder == "__REPORT_DATA__" else "visual asset placeholder" - raise ValueError(f"template must contain exactly one {label}: {placeholder}") - rendered = rendered.replace(placeholder, value) - return rendered -``` - -When `--template` points to a custom template, continue loading the trusted bundled assets from the skill's `assets` directory unless a future explicit CLI option changes that contract. - -- [ ] **Step 5: Run bundling tests, full tests, and syntax checks** - -```powershell -python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py -node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js -node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js -``` - -Expected: Python suite passes; both Node checks exit 0. - ---- - -### Task 4: Implement the generic declarative domain-effect renderer - -**Files:** - -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` - -**Interfaces:** - -- Consumes: `algorithm_views[].scene.objects`, `scene.layers`, and the selected view mode. -- Produces: SVG/DOM elements carrying `data-target-id`, `data-evidence-state`, and accessible labels. -- Public JS API: `DailySummaryVisuals.register(name, renderer)`, `.select(name)`, and `.render(view, root, context)`. - -- [ ] **Step 1: Add task-derived adapter assertions** - -Use `visualization_data()` as the business fixture. Add static and generated-HTML assertions: - -```python -def test_domain_adapter_renders_task_objects_not_fixed_demo_content(self): - html = self.require_html_target().render_html( - self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8") - ) - for value in ("local-g2-smoother", "current-path", "curvature-peak", "preview-path"): - self.assertIn(value, html) - self.assertNotIn("固定轨迹示例", html) - -def test_unknown_safe_adapter_has_composite_fallback(self): - data = self.visualization_data() - data["algorithm_views"][0]["adapter"] = "custom-business-domain" - html = self.require_html_target().render_html( - data, TEMPLATE_PATH.read_text(encoding="utf-8") - ) - self.assertIn("custom-business-domain", html) - self.assertIn("composite-scene", html) -``` - -- [ ] **Step 2: Run the focused tests and confirm RED** - -Expected: the fallback or declarative object hooks are missing. - -- [ ] **Step 3: Add safe DOM/SVG construction helpers** - -Implement helpers that assign text through `textContent` and SVG attributes through `setAttribute`; never concatenate untrusted labels into `innerHTML`: - -```javascript -const SVG_NS = 'http://www.w3.org/2000/svg'; - -function element(name, attrs = {}, text = '') { - const node = document.createElement(name); - Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, String(value))); - if (text) node.textContent = text; - return node; -} - -function svgElement(name, attrs = {}) { - const node = document.createElementNS(SVG_NS, name); - Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, String(value))); - return node; -} - -function markTarget(node, object) { - node.dataset.targetId = object.id; - node.dataset.evidenceState = object.evidence_state; - node.setAttribute('tabindex', '0'); - node.setAttribute('role', 'button'); - node.setAttribute('aria-label', `${object.label},${object.evidence_state}`); - return node; -} -``` - -- [ ] **Step 4: Implement adapter strategies over shared primitives** - -Register these layout strategies, while keeping their data task-driven: - -- `composite-scene`: render supplied points, polylines, curves, regions, nodes, edges, state blocks, data items, annotations, and clear unsupported-kind cards for the remainder. - -The generic renderer must filter visible objects from the selected `scene.layers[].object_ids`; it must not invent domain samples. Unknown adapter names must select `composite-scene`. Do not add dedicated renderer implementations in this task. - -- [ ] **Step 5: Add evidence-state and target CSS** - -In `visualization-styles.css`, use line style, icon/text, and color together: - -```css -[data-evidence-state="actual"] { --state-color: var(--blue); } -[data-evidence-state="static"] { --state-color: var(--amber); } -[data-evidence-state="conceptual"] { --state-color: var(--amber); stroke-dasharray: 8 6; opacity: .82; } -[data-evidence-state="verified"] { --state-color: var(--green); } -[data-evidence-state="conflict"] { --state-color: var(--red); stroke-dasharray: 3 4; } -[data-target-id].is-highlighted { filter: drop-shadow(0 0 5px var(--state-color)); } -[data-target-id]:focus-visible { outline: 3px solid #e7a628; outline-offset: 3px; } -``` - -- [ ] **Step 6: Run Python tests and Node syntax checks** - -Use the commands from Task 3 Step 5. Expected: all pass, and no test claims business correctness beyond the current fixture's actual evidence. - ---- - -### Task 5: Build the four-mode interactive diagnosis shell - -**Files:** - -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` - -**Interfaces:** - -- State: `{ algorithmId, issueId, mode, solutionIndex }`. -- Modes: `baseline`, `current`, `proposed`, `verified`. -- Consumes: Task 1 issue links and Task 4 adapter API. - -- [ ] **Step 1: Write failing interaction-hook tests** - -```python -def test_renders_algorithm_selector_four_modes_domain_canvas_and_diagnosis_card(self): - html = self.require_html_target().render_html( - self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8") - ) - for hook in ( - 'id="algorithm-selector"', - 'data-view-mode="baseline"', - 'data-view-mode="current"', - 'data-view-mode="proposed"', - 'data-view-mode="verified"', - 'id="domain-canvas"', - 'id="diagnosis-current"', - 'id="diagnosis-cause"', - 'id="diagnosis-impact"', - 'id="diagnosis-solution"', - 'id="diagnosis-expected"', - 'id="task-validation"', - ): - self.assertIn(hook, html) - -def test_verified_mode_is_guarded_by_verified_result(self): - runtime = (TEMPLATE_PATH.parent / "visualization-runtime.js").read_text(encoding="utf-8") - self.assertIn("hasVerifiedResult", runtime) - self.assertIn("button.disabled", runtime) - self.assertIn("尚无修正后的匹配验证证据", runtime) -``` - -- [ ] **Step 2: Run the focused tests and confirm RED** - -Expected: the new shell hooks and verified-result guard are absent. - -- [ ] **Step 3: Replace the two-column issue workbench with the approved shell** - -Add: - -- algorithm and issue selectors; -- four mode buttons with `aria-pressed`; -- layer toggles; -- central `#domain-canvas`; -- algorithm-stage navigation; -- object diagnosis card; -- expandable source/test evidence; -- existing solution steps, validation gates, and roadmap below the canvas. - -When `algorithm_views` is absent, hide the algorithm controls and retain the legacy text diagnosis behavior. - -- [ ] **Step 4: Implement one state-driven render path** - -In `visualization-runtime.js`, use one render function so selectors, modes, canvas, diagnosis, validation, and buttons never drift: - -```javascript -const report = JSON.parse(document.getElementById('report-data').textContent); -const views = Array.isArray(report.algorithm_views) ? report.algorithm_views : []; -const issues = Array.isArray(report.issues) ? report.issues : []; -const state = { - algorithmId: views[0]?.id || '', - issueId: issues[0]?.id || '', - mode: views.length ? 'baseline' : 'current', - solutionIndex: 0, -}; - -function currentView() { - return views.find((view) => view.id === state.algorithmId) || null; -} - -function currentIssue() { - return issues.find((issue) => issue.id === state.issueId) || null; -} - -function hasVerifiedResult(issue) { - return Boolean(issue?.verified_result?.evidence_state === 'verified' && issue.verified_result.validation_refs?.length); -} - -function renderApp() { - const view = currentView(); - const issue = currentIssue(); - renderSelectors(view, issue); - renderModeButtons(issue); - renderDomainCanvas(view, issue); - renderDiagnosis(issue); - renderTaskValidation(report.task_validation); - renderExistingReportSections(issue); -} -``` - -- [ ] **Step 5: Implement mode-to-layer and diagnosis behavior** - -- `baseline`: show the normal algorithm layer and purpose/input/output/constraints. -- `current`: show current layer, highlight `target_ids`, then `effect_target_ids` in propagation order. -- `proposed`: show `solution_preview.target_ids`, expected result, and conceptual/static label. -- `verified`: enable only when `hasVerifiedResult(issue)`; show verified targets and validation references. - -Clicking or pressing Enter/Space on a visual target must select the linked issue. Arrow keys change issues only when focus is not in a form control; mode buttons and targets retain visible focus. - -- [ ] **Step 6: Add responsive and reduced-motion behavior** - -At desktop width use mode rail + canvas + diagnosis drawer. Under 900px stack the drawer below the canvas. Under 560px use single-column selectors and controls. When `prefers-reduced-motion: reduce` is active, reveal the complete impact path immediately rather than animating it. - -- [ ] **Step 7: Extend rendered-pair validation to cover algorithm facts** - -Update `_validate_rendered_text` so every algorithm view ID, view evidence state, linked issue target ID, correctness criterion ID, and executed check ID exists in the generated HTML. Require the view name, purpose, criterion statement, and check result in Markdown. Keep the existing issue ID/evidence checks and external-resource rejection. - -Add a negative test that removes `curvature-peak` from rendered HTML and expects `validate` to fail with `HTML is missing visual target: curvature-peak`. - -- [ ] **Step 8: Run the full suite and generated-HTML validation** - -```powershell -python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py -node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js -node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js -``` - -Expected: all tests and syntax checks pass; generated HTML contains no external resource. - ---- - -### Task 6: Teach the skill the task-understanding and dynamic-validation workflow - -**Files:** - -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` - -**Interfaces:** - -- Consumes: data contract and renderer from Tasks 1-5. -- Produces: repeatable Agent instructions that create task-matched views and validation facts without requiring the user to fill JSON manually. - -- [ ] **Step 1: Add the mandatory understanding sequence to SKILL.md** - -Insert a concise workflow before report JSON construction: - -```markdown -## Build a task-matched algorithm view - -When today's work changes, diagnoses, or discusses a function or algorithm: - -1. Identify its business purpose, inputs, outputs, stages, observable objects, and correctness constraints from current evidence. -2. Decide what domain effect lets a reader see the algorithm working. Prefer spatial scenes, numeric plots, search/state structures, timelines, or transformed data over a generic flowchart when the evidence supports them. -3. Build one `algorithm_views` entry from actual run/test data when available. Label source reconstruction as `static` and solution prediction as `conceptual`. -4. Link every visual issue to existing stage/object IDs. Show current targets, effect propagation, candidate changes, and verified results as separate states. -5. If the evidence cannot support a credible domain view, list the missing evidence and omit the invented scene. -``` - -- [ ] **Step 2: Replace generic validation wording with task-matched validation** - -```markdown -## Match validation to the current task - -Derive correctness criteria from the selected function or algorithm, then locate only tests, commands, samples, and runtime evidence that directly evaluate those criteria. Record checks actually run, their exact results, and missing evidence separately. If no matching test exists, propose a task-specific check and keep the conclusion unverified. Never claim coverage from an unrelated fixed scenario. -``` - -Retain the existing safety rule against expensive tests by default. - -- [ ] **Step 3: Document the complete schema and one non-prescriptive example** - -In `references/report-schema.md`, document every Task 1 field, allowed evidence states, object/layer linking rules, task-validation shape, verified-result requirements, and legacy behavior. Use one example only to illustrate the contract, and state explicitly that its domain does not constrain adapter selection. - -- [ ] **Step 4: Regenerate UI metadata from the updated skill** - -Run: - -```powershell -python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\generate_openai_yaml.py C:\Users\admin\.codex\skills\daily-summary-job --interface 'display_name=Daily Summary Job' --interface 'short_description=按当前任务生成带领域算法诊断的交互日报' --interface 'default_prompt=使用 $daily-summary-job 理解当前任务和算法,以匹配的领域效果图展示正常机制、问题、影响、修正方案与验证结果,并更新今日日报。' -``` - -Expected: `agents/openai.yaml` contains only the interface block with the three supplied values and valid UTF-8 Chinese. - -- [ ] **Step 5: Validate skill structure and concise loading behavior** - -```powershell -python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\admin\.codex\skills\daily-summary-job -``` - -Expected: `Skill is valid!`. Confirm `SKILL.md` stays under 500 lines and keeps detailed field definitions in `references/report-schema.md`. - ---- - -### Task 7: End-to-end verification on the current task and backward compatibility - -**Files:** - -- Test: all files under `C:\Users\admin\.codex\skills\daily-summary-job` -- Generate temporary outputs only under a verified temporary directory or this project's `dailywork_report` when explicitly updating the real report. - -**Interfaces:** - -- Consumes: completed skill from Tasks 1-6. -- Produces: fresh verification evidence for schema, rendering, interaction hooks, self-containment, task matching, and legacy reports. - -- [ ] **Step 1: Run the complete automated suite** - -```powershell -python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py -``` - -Expected: every test passes with zero failures. Record the actual test count; do not reuse the previous count of 31. - -- [ ] **Step 2: Run skill and JavaScript validation** - -```powershell -python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\admin\.codex\skills\daily-summary-job -node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js -node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js -``` - -Expected: skill valid; both JavaScript files exit 0. - -- [ ] **Step 3: Run a temporary legacy report round trip** - -Use the existing `sample_data()` shape without `algorithm_views`. Run `render`, `validate`, and `render --update` in a new temporary project. Expected: Markdown and HTML are created, validate returns `valid: true`, and update reuses the same pair. - -- [ ] **Step 4: Run a temporary current-task domain-view round trip** - -Use the Task 1 `visualization_data()` facts, which match the current path-smoothing work rather than an unrelated generic test. Run `render`, then `validate`. Assert: - -- `valid` is `true`; -- HTML contains the task's actual object IDs and values; -- current, proposed, and verified controls are present; -- verified mode is disabled because this fixture has no verified result; -- Markdown contains the task-specific criterion and missing evidence; -- no external URL, ``, or ` - -``` - -Use text and icons together for status; do not rely on color alone. Provide visible focus states, arrow-key issue navigation, responsive single-column fallbacks, and a no-animation media query. Display “概念示意” whenever a problem lacks numeric evidence. - -- [ ] **Step 4: Implement safe JSON embedding and rendering** - -```python -def safe_json_for_html(data: dict[str, Any]) -> str: - raw = json.dumps(data, ensure_ascii=False, separators=(",", ":")) - return raw.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026") - -def render_html(data: dict[str, Any], template: str) -> str: - validate_report_data(data) - if template.count("__REPORT_DATA__") != 1: - raise ValueError("template must contain exactly one report data placeholder") - return template.replace("__REPORT_DATA__", safe_json_for_html(data)) -``` - -- [ ] **Step 5: Run the focused tests** - -Expected: HTML safety, interaction-hook, evidence-consistency, and accessibility-source tests pass. - ---- - -### Task 5: Add checkpoint, render, update, and validate CLI workflows - -**Files:** -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py` - -**Interfaces:** -- Produces CLI subcommands `inspect`, `checkpoint`, `render`, and `validate`. -- All successful commands emit compact JSON to stdout; failures return nonzero with a specific message on stderr. - -- [ ] **Step 1: Add failing end-to-end CLI tests** - -Use `tempfile.TemporaryDirectory` to verify: - -1. `checkpoint` creates one compact JSON under `.daily-summary-job/YYYY-MM-DD/checkpoints`. -2. `render` creates canonical state plus a Markdown/HTML pair under `_rep/YYYY-MM-DD`. -3. `render --update` preserves the original sequence and paths. -4. A second topic receives the next sequence. -5. `validate` rejects mismatched issue identifiers or an external URL in HTML. - -- [ ] **Step 2: Run tests and confirm CLI failures** - -Expected: FAIL because the subcommands are not wired. - -- [ ] **Step 3: Implement the four subcommands** - -- `inspect`: report project root, local date, changed paths, existing report modules, inferred module, and evidence file candidates without writing. -- `checkpoint`: validate compact input, create the checkpoint directory, and write UTF-8 JSON atomically. -- `render`: validate full input, plan or reuse paths, render both outputs to temporary siblings, validate them, atomically replace the pair, and persist canonical state. -- `validate`: compare issue identifiers and evidence levels across canonical JSON, Markdown, and HTML; reject external resources. - -Use `tempfile.NamedTemporaryFile(delete=False, dir=target.parent)` and `Path.replace` only after both staged files pass validation. Clean up staged files in `finally` without deleting existing deliverables. - -- [ ] **Step 4: Run all script tests** - -Run: - -```powershell -python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py' -v -``` - -Expected: all tests pass. - ---- - -### Task 6: Write the concise skill workflow and metadata-aligned instructions - -**Files:** -- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` -- Verify: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` - -**Interfaces:** -- Consumes: `scripts/prepare_report.py`, `references/report-schema.md`, and `assets/interactive-report-template.html`. -- Produces: A skill another Codex instance can invoke for record, generate, or update intents without loading unrelated history. - -- [ ] **Step 1: Replace scaffold placeholders with final frontmatter** - -Use only the required YAML keys: - -```yaml ---- -name: daily-summary-job -description: Record compact development checkpoints and generate or update evidence-grounded daily work reports with paired Markdown and self-contained interactive HTML. Use when the user asks to record current development progress, summarize today's coding work, organize problems and improvements, visualize problem/solution reasoning, or update an existing daily development report. ---- -``` - -- [ ] **Step 2: Write the imperative workflow** - -The body must tell the invoking agent to: - -1. Determine record/generate/update intent without requiring fixed wording. -2. Read only current context and today's relevant evidence. -3. Run `inspect` before any write. -4. Preserve evidence boundaries and conflicts. -5. Create the normalized JSON using `report-schema.md`. -6. Use `checkpoint` for compact progress capture. -7. Use `render` for new reports and `render --update` for exact-topic updates. -8. Run `validate` and report precise paths. -9. Never fix business code, run Git commit, fabricate evidence, or read historical days by default. - -- [ ] **Step 3: Verify interface metadata remains aligned** - -`agents/openai.yaml` must show `Daily Summary Job`, the approved Chinese short description, and a default prompt explicitly containing `$daily-summary-job`. Do not add icons, colors, dependencies, or policy fields. - ---- - -### Task 7: Validate the installed skill and run a disposable full workflow - -**Files:** -- Verify only: `C:\Users\admin\.codex\skills\daily-summary-job\**` -- Create and remove only: a dedicated directory under the system temporary directory. - -**Interfaces:** -- Produces: Validation evidence for skill structure, unit behavior, report generation, update stability, and offline HTML constraints. - -- [ ] **Step 1: Run skill structure validation** - -```powershell -python 'C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py' 'C:\Users\admin\.codex\skills\daily-summary-job' -``` - -Expected: validation succeeds. - -- [ ] **Step 2: Run the full script test suite** - -```powershell -python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py' -v -``` - -Expected: all tests pass. - -- [ ] **Step 3: Create a disposable simulated project** - -Create one explicit temporary project containing `src/Map`, `src/PathSmoothing`, and an existing `dailywork_report/pathsmoothing_rep`. Feed a checkpoint and a full report fixture containing achievements, two evidence levels, an improvement, validation results, and next steps. - -- [ ] **Step 4: Run record, generate, update, and validation commands** - -Expected: - -- checkpoint path is date-scoped; -- multi-module input selects `cross-module_rep` unless explicitly overridden; -- generation creates one paired report; -- update keeps the same pair; -- every issue identifier appears in normalized JSON, Markdown, and HTML; -- HTML contains no `http://`, `https://`, external script, or external image reference. - -- [ ] **Step 5: Inspect the final installed file set and repository scope** - -Run: - -```powershell -Get-ChildItem -LiteralPath 'C:\Users\admin\.codex\skills\daily-summary-job' -Recurse -File | Select-Object FullName,Length -git status --short -- 'docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md' 'docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md' -``` - -Expected: only the new skill files exist in the personal directory; the repository shows the two uncommitted documentation files and no task-caused business-code changes. - -## Execution Choice - -The user requested immediate execution without Git commits. Execute this plan inline with `superpowers:executing-plans`; do not dispatch subagents and do not pause for a separate execution-choice prompt. diff --git a/docs/superpowers/plans/2026-08-03-em-planner-foundation-implementation.md b/docs/superpowers/plans/2026-08-03-em-planner-foundation-implementation.md deleted file mode 100644 index 846abc1..0000000 --- a/docs/superpowers/plans/2026-08-03-em-planner-foundation-implementation.md +++ /dev/null @@ -1,557 +0,0 @@ -# EM Planner Foundation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build the immutable EM Planner contracts, validated configuration, direction-segment boundary model, reverse-safe Frenet transforms, and topology-preserving static lateral corridor. - -**Architecture:** Keep the planner core request-driven and independent from hardware, UI, clocks, and solver details. Adapt one consumable `PathSmoothingResult` into an immutable current-direction reference window, then project only within that segment and build a collision-checked connected lateral interval around the selected seed. - -**Tech Stack:** C# 10, .NET Standard 2.0 library, existing `PlanningGridMap`, `FootprintCollisionChecker`, and a `net10.0-windows` console verification host. - -## Global Constraints - -- Namespace for new production types: `MultiWheelC.TrajectoryPlanning.EMPlanner` plus responsibility-specific child namespaces. -- First release supports ordinary nonholonomic forward/reverse motion only; lateral translation, crab motion, and in-place rotation are rejected. -- World units are metres, seconds, radians, metres/second, radians/second, and inverse metres. -- Vehicle body X points forward, body Y points left, and positive yaw is counter-clockwise. -- `TravelYaw = VehicleYaw` forward and `TravelYaw = Normalize(VehicleYaw + π)` reverse. -- Frenet `s` always increases in actual travel direction; positive `l` is left of travel, including reverse. -- A planning window never crosses a gear-switch boundary and never drops an exact terminal anchor. -- Static-corridor selection preserves the topology chosen by Hybrid A*; it never jumps to another disconnected free interval. -- Initial corridor spacings are `0.10 m` longitudinal and `0.025 m` lateral; maximum lateral offset is `0.30 m`. -- Existing user changes in `ClumsyPilot.csproj`, Map, CoarsePath, and PathSmoothing must be preserved. -- Dynamic-obstacle trajectory prediction and dynamic space-time boundaries are outside this plan. - ---- - -## Locked File Structure - -```text -ClumsyPilot/ParkrobTrajplanner/EMPlanner/ -├── Contracts/ -│ ├── EmBoundaryType.cs -│ ├── EmMotionModel.cs -│ ├── EmPlanningRequest.cs -│ ├── EmPlanningResult.cs -│ ├── EmPlanningStatus.cs -│ ├── EmTerminalType.cs -│ ├── EmTrajectory.cs -│ ├── EmTrajectoryMetadata.cs -│ ├── EmTrajectoryPoint.cs -│ └── VehicleMotionState.cs -├── Configuration/ -│ ├── CorridorConfiguration.cs -│ ├── EmPlannerConfiguration.cs -│ ├── FrenetConfiguration.cs -│ ├── LateralConfiguration.cs -│ ├── LateralWeights.cs -│ ├── LongitudinalConfiguration.cs -│ ├── LongitudinalWeights.cs -│ ├── SchedulingConfiguration.cs -│ ├── SolverConfiguration.cs -│ └── ValidationConfiguration.cs -├── Diagnostics/ -│ ├── EmPlannerDebugOptions.cs -│ ├── EmPlanningDiagnostics.cs -│ └── IEmPlannerDebugSink.cs -├── Segmentation/ -│ ├── DirectionSegmentView.cs -│ ├── ReferenceBoundary.cs -│ ├── ReferenceHorizonSlicer.cs -│ └── ReferencePathSegmenter.cs -├── Frenet/ -│ ├── FrenetProjection.cs -│ ├── FrenetProjector.cs -│ ├── FrenetReferencePoint.cs -│ ├── FrenetTransform.cs -│ └── ReferencePathInterpolator.cs -├── Corridor/ -│ ├── LateralInterval.cs -│ ├── StaticCorridor.cs -│ └── StaticCorridorBuilder.cs -└── Validation/ - └── EmPlanningRequestValidator.cs - -ClumsyPilot/tests/EMPlannerVerificationHost/ -├── EMPlannerVerificationHost.csproj -├── Program.cs -├── Verification.cs -├── EmFixtureFactory.cs -├── FoundationChecks.cs -├── SegmentationChecks.cs -├── FrenetChecks.cs -└── CorridorChecks.cs -``` - -## Shared Public Interfaces - -```csharp -public sealed class VehicleMotionState -{ - public VehicleMotionState(Pose2D pose, double signedLongitudinalSpeedMetersPerSecond, - double? longitudinalAccelerationMetersPerSecondSquared, - DateTimeOffset capturedAtUtc, long sequenceId); - public Pose2D Pose { get; } - public double SignedLongitudinalSpeedMetersPerSecond { get; } - public double? LongitudinalAccelerationMetersPerSecondSquared { get; } - public DateTimeOffset CapturedAtUtc { get; } - public long SequenceId { get; } -} - -public sealed class EmPlanningRequest -{ - public EmPlanningRequest(PathSmoothingResult referencePath, PlanningGridMap map, - VehicleParameters vehicle, VehicleMotionState vehicleState, - EmPlannerConfiguration configuration, int segmentIndex, - EmTrajectory previousTrajectory, DateTimeOffset requestedAtUtc, - DateTimeOffset effectiveAtUtc, string outputTrajectoryId, - string referencePathId, string previousTrajectoryId, - EmMotionModel motionModel); -} - -public sealed class DirectionSegmentView -{ - public int SegmentIndex { get; } - public TravelDirection Direction { get; } - public IReadOnlyList Points { get; } - public ReferenceBoundary StartBoundary { get; } - public ReferenceBoundary EndBoundary { get; } -} - -public sealed class FrenetProjector -{ - public bool TryProject(Pose2D worldPose, DirectionSegmentView segment, - double minimumReferenceS, double maximumReferenceS, - double maximumDistanceMeters, out FrenetProjection projection); -} - -public sealed class StaticCorridorBuilder -{ - public bool TryBuild(DirectionSegmentView segment, double startReferenceS, - double endReferenceS, IReadOnlyList seed, - PlanningGridMap map, VehicleParameters vehicle, - CorridorConfiguration configuration, out StaticCorridor corridor, - out string failureReason); -} -``` - -### Task 1: Verification Host and Immutable Contracts - -**Files:** -- Modify: `ClumsyPilot/ClumsyPilot.csproj` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/Verification.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/` - -**Interfaces:** -- Consumes: existing `Pose2D`, `TravelDirection`, `PathSmoothingResult`, `PlanningGridMap`, and `VehicleParameters`. -- Produces: the contracts shown in “Shared Public Interfaces”, `EmPlanningStatus`, `EmTerminalType`, `EmBoundaryType`, and immutable trajectory containers. - -- [ ] **Step 1: Add the isolated verification host and a failing contract check** - -Add these exact MSBuild rules without rewriting surrounding user changes: - -```xml - - -``` - -Create the host project with: - -```xml - - - Exe - net10.0-windows - disable - enable - - - - - -``` - -`FoundationChecks.Run()` must construct a reverse `VehicleMotionState`, assert that its signed speed remains negative, construct an `EmTrajectoryPoint`, and assert all constructor-supplied values are unchanged. `Program.Main` accepts `foundation`, runs the check, prints `PASS foundation`, and returns `1` after printing an exception when a check fails. - -- [ ] **Step 2: Run the host and verify the contracts are absent** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -``` - -Expected: build failure naming `VehicleMotionState` or `EmTrajectoryPoint`. - -- [ ] **Step 3: Implement immutable contracts with constructor validation** - -Use `ReadOnlyCollection` copies for every published list. `EmTrajectoryPoint` has exactly these public properties: - -```csharp -public double X { get; } -public double Y { get; } -public double Yaw { get; } -public double SignedLongitudinalVelocity { get; } -public double Speed { get; } -public double VelocityX { get; } -public double VelocityY { get; } -public double YawRate { get; } -public double TimeFromStart { get; } -public double VehicleCurvature { get; } -public int SegmentIndex { get; } -public double SegmentLocalS { get; } -public double PathS { get; } -public TravelDirection Direction { get; } -public EmBoundaryType BoundaryType { get; } -``` - -Its constructor accepts the authoritative `signedLongitudinalVelocity` and derives, rather than accepts, the redundant fields: - -```csharp -Speed = Math.Abs(signedLongitudinalVelocity); -VelocityX = signedLongitudinalVelocity * Math.Cos(yaw); -VelocityY = signedLongitudinalVelocity * Math.Sin(yaw); -YawRate = signedLongitudinalVelocity * vehicleCurvature; -``` - -Define these exact status values: - -```text -Success -SuccessWithFallback -InvalidInput -UnsupportedMotionMode -StaleVehicleState -StateDirectionMismatch -InvalidReferencePath -ProjectionFailed -CorridorInfeasible -LateralInfeasible -LongitudinalInfeasible -StoppingDistanceInsufficient -SolverUnavailable -SolverTimedOut -Cancelled -ValidationFailed -Superseded -Failed -``` - -`EmMotionModel` has one supported value, `NonholonomicForwardReverse`, plus explicit `CrabTranslation` and `InPlaceRotation` values that validation maps to `UnsupportedMotionMode`. Only the two success statuses may construct a result with a non-empty trajectory. Define `EmBoundaryType` as `None`, `RollingSafetyStop`, `GearSwitchApproach`, `GearSwitchDeparture`, and `Goal`; define `EmTerminalType` as `RollingSafetyStop`, `GearSwitch`, and `Goal`. - -`EmTrajectoryMetadata` contains `TrajectoryId`, `GeneratedAtUtc`, `EffectiveAtUtc`, `MapSnapshotId`, `ReferencePathId`, `VehicleStateSequenceId`, `PreviousTrajectoryId`, `SegmentIndex`, `Direction`, and `TerminalType`; `EmTrajectory` exposes that metadata plus immutable points. `GeneratedAtUtc` is copied from request `RequestedAtUtc`, and `EffectiveAtUtc` plus `OutputTrajectoryId` are caller-supplied so the pure planner never reads a clock or creates a random ID. `EmTrajectoryPoint` retains internal longitudinal acceleration and jerk values for validation but derives all public redundant velocity fields from signed longitudinal velocity. - -- [ ] **Step 4: Run the foundation check** - -Run the same command. Expected: `PASS foundation` and exit code `0`. - -- [ ] **Step 5: Commit the host and contracts** - -```powershell -git add ClumsyPilot/ClumsyPilot.csproj ClumsyPilot/tests/EMPlannerVerificationHost ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts -git commit -m "feat: add EM planner contracts" -``` - -### Task 2: Configuration, Diagnostics, and Request Validation - -**Files:** -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/` -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Diagnostics/` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` - -**Interfaces:** -- Consumes: Task 1 contracts. -- Produces: `EmPlannerConfiguration.CreateDefault()`, `EmPlanningRequestValidator.Validate(EmPlanningRequest)`, debug options, and diagnostics. - -- [ ] **Step 1: Write failing checks for exact defaults and rejection rules** - -Add assertions for these exact defaults: - -```text -ReplanPeriodSeconds=0.20 -TimeHorizonSeconds=6.0 -DistanceHorizonMeters=5.0 -OutputTimeStepSeconds=0.05 -SolverTimeoutSeconds=0.10 -HandoffLookaheadSeconds=0.30 -MaximumVehicleStateAgeSeconds=0.20 -LongitudinalSampleSpacingMeters=0.10 -LateralSampleSpacingMeters=0.025 -MaximumLateralOffsetMeters=0.30 -AdditionalClearanceReserveMeters=0.02 -MaximumCollisionCheckStepMeters=0.025 -MaximumProjectionDistanceMeters=0.50 -MinimumFrenetDenominator=0.20 -BoundaryAnchorToleranceMeters=1e-8 -MaximumLateralStepPerIterationMeters=0.05 -MaximumLateralSlope=0.50 -MaximumLateralSecondDerivativePerMeter=1.00 -MaximumLateralThirdDerivativePerSquareMeter=2.00 -MaximumForwardSpeedMetersPerSecond=0.20 -MaximumReverseSpeedMetersPerSecond=0.20 -MaximumAccelerationMetersPerSecondSquared=0.20 -MaximumDecelerationMetersPerSecondSquared=0.30 -MaximumJerkMetersPerSecondCubed=0.50 -MaximumLateralAccelerationMetersPerSecondSquared=0.20 -MaximumCurvatureRatePerMeterPerSecond=0.50 -StopSpeedToleranceMetersPerSecond=0.01 -ZeroSpeedHoldSeconds=0.20 -MaximumOuterIterations=5 -MaximumOsqpIterations=4000 -AbsoluteTolerance=1e-5 -RelativeTolerance=1e-5 -StrictResidualTolerance=1e-5 -WarmStart=true -Polish=true -NativeVerbose=false -``` - -Assert LS weights `10,1,5,10,5,20,5,10` in reference/heading/second/third/curvature/curvature-variation/previous/rolling-terminal order, and ST weights `10,1,10,5,1` in speed/acceleration/jerk/previous/terminal-acceleration order. - -Also assert validation rejects: null request members, a map with `PlanningReady=false`, a smoothing failure status, an out-of-range segment index, a non-finite speed, negative sequence ID, stale state, and vehicle geometry without a valid curvature limit. -Assert `CrabTranslation` and `InPlaceRotation` return `UnsupportedMotionMode` before projection. - -- [ ] **Step 2: Run and verify the configuration types are absent** - -Run the foundation group. Expected: build failure naming `EmPlannerConfiguration`. - -- [ ] **Step 3: Implement configuration snapshots and validation** - -Use settable configuration DTOs only at the request boundary, then make `EmPlanningRequestValidator` return a copied internal snapshot. Validate every double with `NumericGuard`; enforce positive sample spacings, `0 < MinimumFrenetDenominator < 1`, non-negative margins, and positive horizons. Treat only `Complete`, `PartialImprovement`, `NotNeeded`, and `Unchanged` smoothing statuses as consumable. - -`LateralConfiguration` owns trust-region and derivative hard limits plus `LateralWeights`; `LongitudinalConfiguration` owns direction speed, acceleration, deceleration, jerk, lateral-acceleration, curvature-rate, stop tolerance, hold duration, and `LongitudinalWeights`; `ValidationConfiguration` owns absolute spatial/kinematic consistency tolerances. Configuration validation requires every weight to be finite and non-negative and every normalization scale to be finite and positive. - -`EmPlannerDebugOptions` exposes only flags and a sink: - -```csharp -EnableSummary -EnableProjectionTrace -EnableCorridorTrace -EnableLateralSolverTrace -EnableLongitudinalSolverTrace -EnableTrajectoryDump -EnableVisualization -Sink -``` - -Debug-sink exceptions are caught and recorded in diagnostics; they never escape the planner. - -- [ ] **Step 4: Run foundation checks and confirm all rejection messages are deterministic** - -Expected: `PASS foundation`; run twice and compare stdout exactly. - -- [ ] **Step 5: Commit configuration and validation** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration ClumsyPilot/ParkrobTrajplanner/EMPlanner/Diagnostics ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs -git commit -m "feat: validate EM planner requests" -``` - -### Task 3: Direction Segmentation and Exact Boundary Anchors - -**Files:** -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/SegmentationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs` - -**Interfaces:** -- Consumes: `PathSmoothingResult.Path`, `PathSmoothingResult.Segments`, and Task 1 boundary enums. -- Produces: `DirectionSegmentView`, `ReferenceBoundary`, and exact horizon slices. - -- [ ] **Step 1: Write failing gear-pair and horizon checks** - -Build a fixture whose forward segment ends at `(2,0,0,s=2)`, whose reverse segment starts with a duplicated pose at the same source arc length, and whose direction changes only on the second member of the pair. Assert: - -```text -segment 0 end boundary = GearSwitchApproach -segment 1 start boundary = GearSwitchDeparture -segment identity differs even when pose and source arc length match -horizon 1.95 injects an interpolated RollingSafetyStop at exactly 1.95 -horizon 2.05 for segment 0 still ends at exactly 2.00 and never includes segment 1 -``` - -- [ ] **Step 2: Run the segmentation group and verify failure** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- segmentation -``` - -Expected: build failure naming `ReferencePathSegmenter`. - -- [ ] **Step 3: Implement segment views and exact slicing** - -`ReferenceBoundary` identity is the tuple `(SegmentIndex, SegmentLocalS, BoundaryType)`. Rebase every selected segment to `SegmentLocalS=0` without changing the source point. `ReferenceHorizonSlicer.Slice` must interpolate X, Y, unwrapped vehicle yaw, geometric curvature, vehicle curvature, curvature derivative, and clearance at the exact terminal S, then label the anchor `RollingSafetyStop`, `GearSwitch`, or `Goal`. - -Never implement horizon selection by filtering `point.ArcLength <= end`; always append or replace with the exact anchor after bracketing interpolation. - -- [ ] **Step 4: Run segmentation checks** - -Expected: `PASS segmentation`, with assertions covering exact equality within `1e-8 m`. - -- [ ] **Step 5: Commit segmentation** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: preserve EM planner segment boundaries" -``` - -### Task 4: Reverse-Safe Frenet Projection and Reconstruction - -**Files:** -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet/` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/FrenetChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: `DirectionSegmentView` from Task 3. -- Produces: interpolation, bounded projection, world/Frenet conversion, and optimized-yaw reconstruction. - -- [ ] **Step 1: Write failing forward, reverse, wraparound, and loop checks** - -Cover these equations directly: - -```text -travelYaw = forward ? vehicleYaw : Normalize(vehicleYaw + PI) -deltaS = dx*cos(travelYaw) + dy*sin(travelYaw) -l = -dx*sin(travelYaw) + dy*cos(travelYaw) -x = referenceX - l*sin(travelYaw) -y = referenceY + l*cos(travelYaw) -optimizedTravelYaw = referenceTravelYaw + atan2(dl, 1-referenceK*l) -optimizedVehicleYaw = reverse ? Normalize(optimizedTravelYaw + PI) : optimizedTravelYaw -``` - -Use a reverse reference whose vehicle yaw is near `-π`, points on both sides of the wrap, and a U-shaped segment with two spatially close branches. Assert the projector remains inside the supplied S interval and chooses the locally nearest branch, then reconstructs the original world point within `1e-8 m`. - -- [ ] **Step 2: Run the Frenet group and verify failure** - -Expected: build failure naming `FrenetProjector`. - -- [ ] **Step 3: Implement deterministic bounded projection** - -Use segment-line projection for the coarse candidate, clamp the interpolation fraction to `[0,1]`, then compare squared world distance. Resolve equal-distance ties by smaller absolute delta from the seed/reference S and then smaller S. Reject projections farther than `MaximumProjectionDistanceMeters`; never search another direction segment. - -`FrenetTransform` rejects reconstruction when `1-referenceK*l < MinimumFrenetDenominator`. Angles used for interpolation are unwrapped; only public yaw is normalized with `AngleMath.NormalizeRadians`. - -- [ ] **Step 4: Run Frenet checks** - -Expected: `PASS frenet`, including reverse `l>0` being left of motion and therefore body-right. - -- [ ] **Step 5: Commit Frenet support** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Frenet ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: add reverse-safe Frenet transforms" -``` - -### Task 5: Topology-Preserving Static Corridor - -**Files:** -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor/` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/CorridorChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: Task 4 interpolation/transforms, `PlanningGridMap`, `VehicleParameters`, and `FootprintCollisionChecker`. -- Produces: immutable `StaticCorridor` stations containing `ReferenceS`, `MinimumL`, `MaximumL`, and seed L. - -- [ ] **Step 1: Write failing corridor connectivity checks** - -Create maps through `PlanningMapFactory`: an empty map, a static rectangle narrowing the left side of a straight path, and an obstacle splitting lateral samples into disconnected left/right intervals. Assert: - -```text -empty map corridor = [-0.30, +0.30] at interior stations -every accepted lateral sample passes exact rotated-footprint collision checking -prior-trajectory seed is used before l=0 -the chosen interval contains the seed at every station -a disappearing seed-connected interval returns false instead of switching sides -first and last stations match exact requested ReferenceS anchors -``` - -- [ ] **Step 2: Run the corridor group and verify failure** - -Expected: build failure naming `StaticCorridorBuilder`. - -- [ ] **Step 3: Implement broad-phase plus exact-footprint sampling** - -At each `0.10 m` S station, sample L in `0.025 m` increments from `-0.30` to `+0.30`, always injecting the exact seed L and exact offset limits. Use the distance field only to accept obviously clear samples; all remaining samples go through `FootprintCollisionChecker.IsPoseCollisionFree` with `AdditionalClearanceReserveMeters=0.02`. Group adjacent free samples, choose only the group containing the seed, and propagate overlap with the previously chosen interval. - -When propagation loses overlap, return failure with the first failed S. Do not select another free group, invert L for reverse, or infer a new obstacle side. - -- [ ] **Step 4: Run all foundation-plan checks** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- all-foundation -``` - -Expected output: - -```text -PASS foundation -PASS segmentation -PASS frenet -PASS corridor -``` - -- [ ] **Step 5: Commit corridor support** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Corridor ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: build static EM lateral corridors" -``` - -### Task 6: Foundation Documentation and Gate - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: all preceding tasks. -- Produces: a documented, repeatable foundation verification command for later plans. - -- [ ] **Step 1: Add README contract examples** - -Document the pipeline, coordinate formulas, reverse sign example, exact gear-pair behavior, configuration units, and the command used to run `all-foundation`. State explicitly that the module does not yet solve LS or ST at this gate. - -- [ ] **Step 2: Run format and placeholder checks** - -```powershell -git diff --check -rg -n "NotImplementedException|throw new Exception\(\)" ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost -``` - -Expected: `git diff --check` returns no diagnostics and the incomplete-implementation scan returns no matches. - -- [ ] **Step 3: Run the complete foundation gate twice** - -Run `all-foundation` twice. Expected: identical four PASS lines and exit code `0` both times. - -- [ ] **Step 4: Confirm the normal project baseline separately** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -``` - -Expected at this repository baseline: only the already-documented legacy `auto_avoidance/MultiWheelAutoAvoidance.cs` missing-reference errors may remain. Any new error under `EMPlanner` fails this gate. - -- [ ] **Step 5: Commit documentation** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs -git commit -m "docs: describe EM planner foundation" -``` - -## Completion Gate - -- `all-foundation` passes twice with deterministic output. -- Forward and reverse projection/reconstruction agree within `1e-8 m`. -- No segment, projection, horizon, or corridor crosses a gear boundary. -- Corridor samples pass exact body collision checks and never change disconnected topology. -- No solver, scheduler, hardware access, dynamic prediction, or trajectory publication logic has leaked into this phase. diff --git a/docs/superpowers/plans/2026-08-03-em-planner-lateral-ls-implementation.md b/docs/superpowers/plans/2026-08-03-em-planner-lateral-ls-implementation.md deleted file mode 100644 index d486aeb..0000000 --- a/docs/superpowers/plans/2026-08-03-em-planner-lateral-ls-implementation.md +++ /dev/null @@ -1,334 +0,0 @@ -# EM Planner Lateral LS Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Optimize a collision-free, curvature-feasible lateral path inside the selected static corridor using sequential convex programming over OSQP QPs. - -**Architecture:** Discretize `l, dl, ddl, dddl` over exact reference-S stations, build normalized quadratic costs and linear integration/corridor constraints, and linearize nonlinear vehicle curvature inside an outer trust-region loop. Reconstruct each accepted candidate in world coordinates, recompute true path arc length, and independently validate it before exposing it to ST. - -**Tech Stack:** C# 10, .NET Standard 2.0, foundation Frenet/corridor types, solver-neutral `IQpSolver`, OSQP backend for integration checks. - -## Global Constraints - -- This plan depends on completion of the foundation and OSQP-backend plans. -- LS runs on exactly one current direction segment and uses `ReferenceS` as its independent variable. -- `l>0` is left of travel for both forward and reverse; do not reinterpret it as body-left in reverse. -- Corridor bounds, maximum lateral offset, trust region, start state, terminal event, Frenet denominator, and vehicle curvature are hard constraints. -- Initial derivative bounds: `|Δl|<=0.05 m` per SQP iteration, `|dl|<=0.50`, `|ddl|<=1.00 1/m`, `|dddl|<=2.00 1/m²`. -- Enforce `1-referenceK*l >= 0.20` at every knot. -- SQP outer-iteration limit is `5`; OSQP iteration limit is `4000`; absolute/relative tolerances are `1e-5`. -- Cost weights: reference `10`, heading `1`, second derivative `5`, third derivative `10`, curvature `5`, curvature variation `20`, previous trajectory `5`, rolling terminal `10`. -- Every cost term is divided by the square of its physical scale before its weight is applied. -- LS scales are maximum lateral offset for L, maximum slope for DL, maximum second derivative for DDL, maximum third derivative for DDDL, vehicle maximum curvature for curvature, and `max(1, reference max |dk/ds|)` for curvature variation. -- Gear-switch and goal terminals require `l=0` and `dl=0`; a rolling safety terminal uses a soft terminal penalty. -- Only the last independently validated feasible candidate may survive a later timeout or failed outer iteration. -- The output world path is re-parameterized by actual `PathS`; later ST code must not use `ReferenceS` as traveled distance. - ---- - -## Locked File Structure - -```text -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/ -├── LateralCandidate.cs -├── LateralConstraintBuilder.cs -├── LateralGeometryEvaluator.cs -├── LateralObjectiveBuilder.cs -├── LateralPath.cs -├── LateralPathPoint.cs -├── LateralPlanner.cs -├── LateralPlanningInput.cs -├── LateralPlanningResult.cs -├── LateralSolutionValidator.cs -├── LateralVariableLayout.cs -└── SequentialConvexOptimizer.cs - -ClumsyPilot/tests/EMPlannerVerificationHost/ -├── FakeQpSolver.cs -├── LateralModelChecks.cs -└── LateralIntegrationChecks.cs -``` - -## Shared Interfaces - -```csharp -public sealed class LateralPlanningInput -{ - public LateralPlanningInput(DirectionSegmentView referenceSegment, - StaticCorridor corridor, FrenetProjection startProjection, - EmTerminalType terminalType, VehicleParameters vehicle, - EmPlannerConfiguration configuration, - IReadOnlyList previousTrajectorySeed); -} - -public sealed class LateralPlanner -{ - public LateralPlanner(IQpSolver qpSolver); - public LateralPlanningResult Plan(LateralPlanningInput input, - CancellationToken cancellationToken); -} - -public sealed class LateralPathPoint -{ - public double ReferenceS { get; } - public double PathS { get; } - public double L { get; } - public double DL { get; } - public double DDL { get; } - public double DDDL { get; } - public double X { get; } - public double Y { get; } - public double VehicleYaw { get; } - public double GeometricCurvature { get; } - public double VehicleCurvature { get; } - public double VehicleCurvatureDerivative { get; } -} -``` - -### Task 1: Variable Layout and Exact Discrete Lateral Dynamics - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralVariableLayout.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanningInput.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCandidate.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPathPoint.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPath.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanningResult.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: foundation corridor, reference, configuration, and terminal types. -- Produces: deterministic variable indices and immutable lateral inputs/results. - -- [ ] **Step 1: Write failing layout and dynamics checks** - -For `N=4`, assert disjoint contiguous ranges for `l[0..3]`, `dl[0..3]`, `ddl[0..3]`, and `dddl[0..2]`, with total variable count `4*N-1`. For unequal S gaps, verify the integration equations: - -```text -ddl[i+1] = ddl[i] + ds*dddl[i] -dl[i+1] = dl[i] + ds*ddl[i] + 0.5*ds^2*dddl[i] -l[i+1] = l[i] + ds*dl[i] + 0.5*ds^2*ddl[i] + (ds^3/6)*dddl[i] -``` - -Reject fewer than two stations, non-increasing S, corridor/input station mismatch, and a start projection outside the first hard interval. - -- [ ] **Step 2: Run the lateral-model group and verify failure** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-model -``` - -Expected: build failure naming `LateralVariableLayout`. - -- [ ] **Step 3: Implement layouts and immutable model types** - -Expose index methods `L(i)`, `DL(i)`, `DDL(i)`, and `DDDL(i)` that range-check every input. Copy all station and seed lists. A failed result has no candidate; success and fallback results require a non-empty independently validated `LateralPath`. - -- [ ] **Step 4: Run the model checks** - -Expected: `PASS lateral-model`. - -- [ ] **Step 5: Commit lateral model types** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: add lateral optimization model" -``` - -### Task 2: Normalized Objective and Linear Hard Constraints - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs` - -**Interfaces:** -- Consumes: Task 1 layout, `SparseTripletBuilder`, corridor bounds, a linearization candidate, and approved LS weights. -- Produces: a validated `QuadraticProgram` for one SQP iteration. - -- [ ] **Step 1: Write failing coefficient-level QP checks** - -For a three-station straight reference with unit scales, inspect P, q, A, lower, and upper arrays and assert: - -```text -reference cost adds 2*w_l to P(l_i,l_i) -jerk cost adds 2*w_dddl to P(dddl_i,dddl_i) -previous-seed cost adds 2*w_previous and -2*w_previous*l_previous -every integration equality appears once with equal lower/upper bounds -corridor, derivative, trust-region, and Frenet-denominator rows use hard finite bounds -gear/goal terminal rows force l_N=0 and dl_N=0 -rolling terminal adds objective terms but no zero terminal equalities -``` - -The test must also show every weight is applied after division by its named scale squared. - -- [ ] **Step 2: Run and verify builders are absent** - -Expected: build failure naming `LateralObjectiveBuilder`. - -- [ ] **Step 3: Implement objective and hard-row assembly** - -Build the OSQP objective convention `0.5*x'Px + q'x`, so a squared residual `w*((x-target)/scale)^2` contributes `2w/scale²` to P and `-2w*target/scale²` to q. Assemble integration rows exactly from Task 1. Intersect corridor bounds with maximum offset, trust region, and linearized denominator bounds before adding each L row; return infeasible before calling the solver when an intersection is empty. - -Use `FakeQpSolver` only in the verification host. It records the last problem/settings/warm start and returns a caller-supplied `QpSolveResult`. - -- [ ] **Step 4: Run coefficient-level checks** - -Expected: `PASS lateral-model`; no coefficient comparison tolerance larger than `1e-10`. - -- [ ] **Step 5: Commit QP assembly** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: assemble lateral LS quadratic programs" -``` - -### Task 3: Nonlinear Geometry Evaluation and Independent Validation - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralGeometryEvaluator.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralSolutionValidator.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs` - -**Interfaces:** -- Consumes: solved `l/dl/ddl/dddl`, Frenet interpolation, direction, and vehicle curvature limit. -- Produces: world-space `LateralPath` with recomputed `PathS`, curvature, and validation residuals. - -- [ ] **Step 1: Write failing reconstruction and curvature checks** - -Cover straight and constant-curvature references in both directions. Assert: - -```text -world X/Y use x_ref-l*sin(travelYaw), y_ref+l*cos(travelYaw) -vehicle yaw adds PI only for reverse -PathS[0]=0 and increments by actual reconstructed chord/geometry length -PathS is strictly increasing even when ReferenceS gaps vary -VehicleCurvature = directionSign*GeometricCurvature -yawRate identity remains valid for a signed test speed -denominator below 0.20 is rejected -curvature beyond vehicle limit is rejected -non-finite values are rejected -``` - -- [ ] **Step 2: Run and verify geometry evaluator is absent** - -Expected: build failure naming `LateralGeometryEvaluator`. - -- [ ] **Step 3: Implement evaluation and strict validation** - -Evaluate geometry from the full Frenet derivative formulas used by the design, not a small-angle replacement. Compute unwrapped travel yaw first, derive geometric curvature with respect to actual path direction, convert to vehicle curvature using direction sign, and compute curvature derivative over actual `PathS`. Use centred differences internally and one-sided endpoints. - -The validator independently recomputes corridor membership, start/terminal residuals, derivative limits, denominator, curvature limit, finite values, and strictly increasing S. It does not trust solver residuals or reuse the QP constraint matrix as its only proof. - -- [ ] **Step 4: Run geometry checks** - -Expected: `PASS lateral-model`, including forward/reverse mirrored cases. - -- [ ] **Step 5: Commit nonlinear evaluation** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs -git commit -m "feat: validate lateral path geometry" -``` - -### Task 4: Sequential Convex Outer Loop and Feasible-Candidate Fallback - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanner.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: Tasks 1–3, `IQpSolver`, warm starts, cancellation, and timeout settings. -- Produces: `LateralPlanningResult` with the last strict feasible path or an explicit failure. - -- [ ] **Step 1: Write failing SQP state-machine checks with `FakeQpSolver`** - -Script solver outcomes and assert: - -```text -first solved candidate is validated before becoming fallback -second timeout returns first candidate as SuccessWithFallback -an invalid solved vector never replaces the fallback -SolvedInaccurate requires QP residual <=1e-5 and full lateral validation -trust region is centred on the previous iterate and never exceeds 0.05 m -outer loop stops after at most 5 calls -cancellation before a call returns Cancelled -no feasible candidate plus timeout returns SolverTimedOut with no path -``` - -- [ ] **Step 2: Run the lateral-integration group and verify failure** - -Expected: build failure naming `SequentialConvexOptimizer`. - -- [ ] **Step 3: Implement the outer loop** - -Initialize from the previous trajectory seed when it covers all stations; otherwise use the corridor-clamped zero-offset seed. Per iteration: linearize geometry, assemble the QP, solve with the remaining time budget, evaluate world geometry, validate independently, store a deep copy if feasible, and test convergence using max absolute L change plus objective improvement. Warm-start the next QP with the complete previous primal vector. - -Return the most specific status. A timeout/cancellation after a validated candidate maps to fallback success; infeasible corridor/QP with no candidate maps to lateral infeasible. - -- [ ] **Step 4: Run scripted SQP checks** - -Expected: `PASS lateral-integration`. - -- [ ] **Step 5: Commit SQP orchestration** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: optimize lateral paths with SQP" -``` - -### Task 5: Real-OSQP Lateral Scenarios and Gate - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` - -**Interfaces:** -- Consumes: `OsqpNativeSolver`, foundation fixtures, and complete LS pipeline. -- Produces: a verified lateral path contract ready for ST. - -- [ ] **Step 1: Add fixed real-solver scenarios** - -Run: straight empty map forward, straight empty map reverse, gentle curve, static obstacle narrowing the existing corridor, gear-switch terminal, and rolling terminal. Assert every result is solved or documented fallback, stays in corridor, respects curvature, and ends at the exact ReferenceS anchor. - -- [ ] **Step 2: Add determinism and topology assertions** - -Run each scenario twice with identical inputs. Compare status, point count, and every numeric output within `1e-10`; assert the obstacle case remains in the seed-connected interval and does not cross to the disconnected side. - -- [ ] **Step 3: Run the complete lateral gate** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-all -git diff --check -``` - -Expected output: - -```text -PASS lateral-model -PASS lateral-integration -PASS lateral-real-osqp -``` - -- [ ] **Step 4: Document LS variables, hard constraints, costs, and fallback** - -Add the exact equations, normalization scales, terminal differences, `ReferenceS`/`PathS` boundary, and last-feasible publication rule to the README. - -- [ ] **Step 5: Commit lateral integration evidence** - -```powershell -git add ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md -git commit -m "test: verify lateral LS scenarios" -``` - -## Completion Gate - -- Coefficient-level tests prove the intended normalized QP, not merely a plausible output path. -- Forward and reverse reconstructed geometry obey the same world-coordinate convention. -- Exact gear/goal terminal L conditions and rolling soft terminal behavior are distinct. -- No candidate outside hard corridor, denominator, derivative, curvature, or boundary constraints is published. -- The published lateral path has actual strictly increasing `PathS` ready for longitudinal optimization. diff --git a/docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md b/docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md deleted file mode 100644 index 5749481..0000000 --- a/docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md +++ /dev/null @@ -1,394 +0,0 @@ -# EM Planner Longitudinal ST and Publication Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add curvature-aware longitudinal optimization, assemble the complete immutable trajectory, independently validate it, and expose a pure one-shot `EmPlanningService.Plan` pipeline. - -**Architecture:** Build a speed envelope over the LS result’s actual `PathS`, then optimize time-knot `s,u,a,j` variables with exact constant-jerk integration and hard stop boundaries. Convert the validated longitudinal/lateral pair into redundant but consistent public trajectory fields, run an independent world-space and kinematic publication validator, and only then return success. - -**Tech Stack:** C# 10, .NET Standard 2.0, LS path types, solver-neutral QP layer, OSQP, existing map and swept-footprint collision checker. - -## Global Constraints - -- This plan depends on completion of foundation, OSQP backend, and lateral LS plans. -- ST uses actual optimized `PathS`; it never receives or publishes `ReferenceS` as traveled distance. -- Internal speed `u>=0`; public signed longitudinal velocity is `directionSign*u`. -- Initial limits: forward/reverse speed `0.20 m/s`, acceleration `0.20 m/s²`, deceleration `0.30 m/s²`, jerk `0.50 m/s³`, lateral acceleration `0.20 m/s²`, curvature-rate limit `0.50 1/(m*s)`. -- Default output time step is `0.05 s`, time horizon `6.0 s`, distance horizon `5.0 m`, stop tolerance `0.01 m/s`, and zero-speed hold `0.20 s`. -- Every terminal is hard bounded by `s_N=terminalS` and `u_N=0`; terminal acceleration is soft. -- Objective weights: speed reference `10`, acceleration `1`, jerk `10`, previous trajectory `5`, terminal acceleration `1`. -- ST scales are direction maximum speed for U, `max(MaximumAcceleration,MaximumDeceleration)` for A, maximum jerk for J, and terminal S for S-tracking; zero terminal S uses scale `1` only for the degenerate stopped result. -- There is no progress cost when terminal S is fixed. -- Speed limit is the minimum of direction maximum, lateral-acceleration limit, curvature-rate limit, and stopping envelope. -- Output contains `X,Y,Yaw,SignedLongitudinalVelocity,Speed,VelocityX,VelocityY,YawRate,TimeFromStart,VehicleCurvature`. -- `SignedLongitudinalVelocity` is authoritative; all redundant speed fields are constructor-derived and independently rechecked. -- Full rotated-body pose and swept-motion collision checks run after optimization. -- A non-success result never contains a non-empty trajectory. -- Dynamic obstacle prediction, time-varying obstacle boundaries, and dynamic behavior decisions remain outside this plan. - ---- - -## Locked File Structure - -```text -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/ -├── LongitudinalCandidate.cs -├── LongitudinalConstraintBuilder.cs -├── LongitudinalObjectiveBuilder.cs -├── LongitudinalPlanner.cs -├── LongitudinalPlanningInput.cs -├── LongitudinalPlanningResult.cs -├── LongitudinalSolutionValidator.cs -├── LongitudinalVariableLayout.cs -├── PathSpeedLimit.cs -├── PathSpeedLimitBuilder.cs -└── SequentialLongitudinalOptimizer.cs - -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/ -└── PlanningHorizonSelector.cs - -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/ -├── EmTrajectoryAssembler.cs -├── LateralPathInterpolator.cs -└── TrajectorySampleSchedule.cs - -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/ -└── EmTrajectoryValidator.cs - -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/ -├── IEmPlanningService.cs -└── EmPlanningService.cs - -ClumsyPilot/tests/EMPlannerVerificationHost/ -├── LongitudinalModelChecks.cs -├── LongitudinalIntegrationChecks.cs -├── TrajectoryChecks.cs -└── EmPlanningServiceChecks.cs -``` - -## Shared Interfaces - -```csharp -public sealed class LongitudinalPlanner -{ - public LongitudinalPlanner(IQpSolver qpSolver); - public LongitudinalPlanningResult Plan(LongitudinalPlanningInput input, - CancellationToken cancellationToken); -} - -public sealed class EmTrajectoryAssembler -{ - public EmTrajectory Assemble(LateralPath path, - LongitudinalPlanningResult longitudinal, - EmTrajectoryMetadata metadata); -} - -public interface IEmPlanningService -{ - EmPlanningResult Plan(EmPlanningRequest request, - CancellationToken cancellationToken); -} - -public sealed class EmPlanningService : IEmPlanningService -{ - public EmPlanningService(IQpSolver qpSolver, - IEmPlannerDebugSink defaultDebugSink = null); - public EmPlanningResult Plan(EmPlanningRequest request, - CancellationToken cancellationToken); -} -``` - -### Task 1: Speed Envelope over Actual PathS - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimit.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: validated `LateralPath`, direction, current speed/acceleration, terminal S/type, and longitudinal configuration. -- Produces: a finite piecewise-linear maximum-speed envelope indexed only by `PathS`. - -- [ ] **Step 1: Write failing speed-limit checks** - -For fixed path points, assert: - -```text -direction limit = 0.20 m/s -if |k|=2 1/m, lateral limit = sqrt(0.20/2) -if |dk/ds|=4 1/m², curvature-rate limit = 0.50/4 -zero curvature and zero derivative do not divide by zero -combined limit is the minimum finite non-negative value -stopping limit at path position s = sqrt(2*MaximumDeceleration*(terminalS-s)) -speed envelope is interpolated by PathS, not ReferenceS -terminal speed is exactly zero -``` - -Add a start state whose jerk/deceleration-limited stopping distance exceeds the available terminal distance; expect `StoppingDistanceInsufficient` before a QP call. - -- [ ] **Step 2: Run and verify the envelope types are absent** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: build failure naming `PathSpeedLimitBuilder`. - -- [ ] **Step 3: Implement finite limits and stopping precheck** - -Use a curvature epsilon of `1e-10`. Clamp every computed limit to the direction maximum and to `sqrt(2*MaximumDeceleration*(terminalS-s))`; the terminal value is exactly zero. Compute a conservative jerk/deceleration stop by first ramping acceleration down at maximum negative jerk until maximum deceleration or zero speed, integrating distance exactly for constant jerk, then adding constant-deceleration distance. Reject when this distance plus `1e-8 m` exceeds terminal distance. - -`PlanningHorizonSelector` computes `TerminalReferenceS` before LS as the minimum of remaining current-segment reference length, `DistanceHorizonMeters`, and the reference distance reachable within `TimeHorizonSeconds` while reserving the same jerk-limited stop tail and zero-speed hold. It returns `GearSwitch` or `Goal` when the exact segment boundary wins and `RollingSafetyStop` otherwise; it never returns a reference S past the current segment end. After LS reconstruction, ST discards that numeric reference distance and uses the last lateral point's actual `PathS` as `terminalS`; stopping feasibility is checked again against that actual distance. - -- [ ] **Step 4: Run speed-envelope checks** - -Expected: `PASS longitudinal-model`. - -- [ ] **Step 5: Commit speed envelope** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: build EM path speed limits" -``` - -### Task 2: Time-Knot Layout, Dynamics, Objective, and Hard Constraints - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalVariableLayout.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalCandidate.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningResult.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` - -**Interfaces:** -- Consumes: Task 1 envelope, a current S iterate, previous-trajectory samples, and QP sparse builders. -- Produces: one convex time-domain QP and immutable candidate/result types. - -- [ ] **Step 1: Write failing variable and coefficient checks** - -For `N=5`, assert ranges for `s[0..4]`, `u[0..4]`, `a[0..4]`, and `j[0..3]`, total `4*N-1`, and exact constant-jerk equations: - -```text -a[i+1] = a[i] + dt*j[i] -u[i+1] = u[i] + dt*a[i] + 0.5*dt^2*j[i] -s[i+1] = s[i] + dt*u[i] + 0.5*dt^2*a[i] + (dt^3/6)*j[i] -``` - -Inspect the QP and prove: `u>=0`, monotonic S, acceleration/deceleration bounds, jerk bounds, speed-envelope bounds at the current S iterate, exact start state, `s_N=terminalS`, `u_N=0`, and no objective coefficient rewards fixed terminal progress. - -- [ ] **Step 2: Run and verify ST builders are absent** - -Expected: build failure naming `LongitudinalVariableLayout`. - -- [ ] **Step 3: Implement normalized ST QP assembly** - -Use the same `0.5*x'Px+q'x` convention. Apply squared-residual coefficients for reference speed, acceleration, jerk, previous S/U, and terminal acceleration after division by their physical scales squared. Determine knot count as `ceil(TimeHorizonSeconds/OutputTimeStepSeconds)+1`; include exact zero and exact horizon times. - -At each outer iteration, read the piecewise speed envelope at the current candidate S. Bound S to `[0,terminalS]` and enforce `s[i+1]>=s[i]`. Add exact start S/U/A and terminal S/U equalities. - -- [ ] **Step 4: Run coefficient-level ST checks** - -Expected: `PASS longitudinal-model`, coefficient tolerance `1e-10`. - -- [ ] **Step 5: Commit ST model assembly** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs -git commit -m "feat: assemble longitudinal ST quadratic programs" -``` - -### Task 3: Longitudinal Outer Loop and Strict Validation - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanner.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: Tasks 1–2 and `IQpSolver`. -- Produces: a validated `s,u,a,j` time profile with last-feasible fallback semantics. - -- [ ] **Step 1: Write failing fake-solver state-machine checks** - -Script solved, inaccurate, timeout, infeasible, and invalid-vector outcomes. Assert the same last-feasible rules as LS, maximum five envelope iterations, warm start, cancellation behavior, and that no candidate violating monotonic S, speed limit, acceleration, jerk, terminal S, or terminal zero speed can become fallback. - -- [ ] **Step 2: Run and verify longitudinal optimizer is absent** - -Expected: build failure naming `SequentialLongitudinalOptimizer`. - -- [ ] **Step 3: Implement iteration and independent checks** - -Seed S by simulating the requested speed while reserving a conservative stop tail; seed U/A/J consistently. Each iteration rebuilds the speed bounds at candidate S, solves with remaining budget, validates directly in physical units, and stores a deep copy only when strict. Converge on maximum S/U change and objective improvement; do not accept a vector merely because OSQP reports solved. - -- [ ] **Step 4: Run scripted and real-OSQP longitudinal checks** - -Cover forward, reverse, curvature-limited, jerk-limited stop, short segment, and zero-start-speed cases. Expected: `PASS longitudinal-integration`. - -- [ ] **Step 5: Commit longitudinal optimizer** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: optimize longitudinal ST profiles" -``` - -### Task 4: Complete Trajectory Assembly and Redundant-Field Consistency - -**Files:** -- Create: all files under `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: validated `LateralPath` and longitudinal profile. -- Produces: immutable public `EmTrajectory` with exact terminal event and zero-speed hold. - -- [ ] **Step 1: Write failing forward/reverse trajectory-field checks** - -At every time knot assert: - -```text -signedV = directionSign*u -speed = abs(signedV) -vx = signedV*cos(yaw) -vy = signedV*sin(yaw) -yawRate = signedV*VehicleCurvature -TimeFromStart strictly increases -PathS never decreases -``` - -For reverse, prove the world velocity points along travel rather than vehicle yaw. Assert the exact gear/goal/rolling terminal point is present, has zero signed speed and zero yaw rate, and is followed by `0.20 s` of identical-pose zero-speed hold samples at `0.05 s` spacing. - -- [ ] **Step 2: Run and verify assembler is absent** - -Expected: build failure naming `EmTrajectoryAssembler`. - -- [ ] **Step 3: Implement interpolation and immutable assembly** - -Interpolate lateral geometry by `PathS` with unwrapped yaw, then normalize public yaw. Construct `EmTrajectoryPoint` only from authoritative signed speed and vehicle curvature so redundant fields cannot diverge. Inject the exact terminal time/position before the hold tail if it is not already a regular knot; never drop it through list filtering. - -- [ ] **Step 4: Run trajectory checks** - -Expected: `PASS trajectory`, including reverse and exact-boundary fixtures. - -- [ ] **Step 5: Commit trajectory assembly** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: assemble complete EM trajectories" -``` - -### Task 5: Independent World-Space Publication Validator - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` - -**Interfaces:** -- Consumes: assembled trajectory, map, vehicle, configuration, current segment, and terminal boundary. -- Produces: a strict validation report used as the only publication gate. - -- [ ] **Step 1: Write failing mutation-style validation checks** - -Start from a valid trajectory, create altered copies, and assert rejection for: NaN, non-increasing time, decreasing PathS, wrong direction sign, inconsistent speed components, inconsistent yaw rate, excessive speed/acceleration/jerk/curvature/curvature-rate, skipped terminal anchor, nonzero terminal speed, pose collision, swept collision, and a point beyond segment end. - -- [ ] **Step 2: Run and verify validator is absent** - -Expected: build failure naming `EmTrajectoryValidator`. - -- [ ] **Step 3: Implement independent publication validation** - -Recompute finite differences in time for acceleration and jerk, recompute yaw-rate identity, check every point through `FootprintCollisionChecker.IsPoseCollisionFree`, and check each adjacent pair through `IsSweptMotionCollisionFree` with maximum step `0.025 m`. Use absolute/relative numeric tolerances from configuration; do not reuse solver status as evidence. - -- [ ] **Step 4: Run all mutation checks** - -Expected: `PASS trajectory`; every altered trajectory has a deterministic first failure code and point index. - -- [ ] **Step 5: Commit publication validator** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs -git commit -m "feat: validate published EM trajectories" -``` - -### Task 6: Pure One-Shot EmPlanningService - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/IEmPlanningService.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` - -**Interfaces:** -- Consumes: every completed core component. -- Produces: the approved pure `Plan(request, cancellationToken)` API. - -- [ ] **Step 1: Write failing end-to-end status and dataflow checks** - -Cover success forward, success reverse, invalid smoothing status, stale state, direction mismatch, projection failure, corridor infeasible, lateral infeasible, stopping-distance insufficient, longitudinal infeasible, solver unavailable, timeout with and without fallback, cancellation, validation failure, gear switch, goal, and rolling stop. Assert input map ID, reference path ID, state sequence ID, prior trajectory ID, and segment index appear unchanged in result diagnostics. - -- [ ] **Step 2: Run and verify facade is absent** - -Expected: build failure naming `EmPlanningService`. - -- [ ] **Step 3: Implement deterministic pipeline orchestration** - -Call stages in this exact order: - -```text -request/config validation -direction-segment selection -bounded ego projection -exact horizon and terminal selection through PlanningHorizonSelector -previous-trajectory seed projection -static connected corridor -LS optimization and validation -PathS speed envelope -ST optimization and validation -trajectory assembly -world-space publication validation -immutable result publication -``` - -Use the request timestamp rather than reading the system clock. Invoke debug sinks only behind flags and catch sink exceptions. Never expose a partial solver vector or mutate request-owned lists. - -- [ ] **Step 4: Run the complete core gate twice** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -git diff --check -``` - -Expected output both times: - -```text -PASS longitudinal-model -PASS longitudinal-integration -PASS trajectory -PASS em-planning-service -``` - -- [ ] **Step 5: Document and commit the one-shot API** - -Document request construction, all output fields/units/signs, statuses, terminal types, and a minimal forward/reverse usage example, then commit: - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: publish one-shot EM trajectories" -``` - -## Completion Gate - -- ST coefficient tests prove exact time dynamics, bounds, normalized costs, and fixed-terminal semantics. -- Every successful trajectory ends at an exact zero-speed terminal and contains a safe hold tail. -- Redundant output fields satisfy their authoritative formulas for every point. -- Full pose and swept collision checks pass in world coordinates. -- Every failure status publishes an empty trajectory and deterministic diagnostics. -- Two identical one-shot requests produce identical statuses and numeric trajectories. diff --git a/docs/superpowers/plans/2026-08-03-em-planner-osqp-backend-implementation.md b/docs/superpowers/plans/2026-08-03-em-planner-osqp-backend-implementation.md deleted file mode 100644 index 610c2ae..0000000 --- a/docs/superpowers/plans/2026-08-03-em-planner-osqp-backend-implementation.md +++ /dev/null @@ -1,382 +0,0 @@ -# EM Planner OSQP Backend Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Provide a solver-independent convex-QP contract and a pinned Windows x64 OSQP 1.0.0 backend that loads `osqp.dll` safely from the plugin directory. - -**Architecture:** Mathematical planners build validated immutable CSC problems against `IQpSolver`; the OSQP adapter owns all native memory and maps native outcomes into planner-neutral statuses. The upstream shared library is built with a fixed ABI configuration, preloaded by absolute path, version-checked, and never allowed to crash the host when absent or incompatible. - -**Tech Stack:** C# 10, .NET Standard 2.0, P/Invoke with Cdecl, OSQP 1.0.0 C API, builtin QDLDL algebra, CMake 3.18+, Visual Studio x64 compiler. - -## Global Constraints - -- This plan depends on `2026-08-03-em-planner-foundation-implementation.md` Task 1 and its verification host. -- Pin upstream source tag `v1.0.0`; do not bind the incompatible 0.6 API. -- Build Windows x64, double precision, 32-bit indices, unpacked settings, builtin algebra, shared library, no MKL or CUDA. -- Required build switches: `OSQP_USE_FLOAT=OFF`, `OSQP_USE_LONG=OFF`, `OSQP_PACK_SETTINGS=OFF`, `OSQP_ALGEBRA_BACKEND=builtin`, `OSQP_BUILD_SHARED_LIB=ON`. -- Native library filename in source and deployed plugin is exactly `osqp.dll`. -- `ClumsyPilot.dll` locates and preloads the sibling DLL from `Assembly.Location`; current directory and system `PATH` are not inputs. -- Every native entry point uses `CallingConvention.Cdecl`. -- All pinned arrays, CSC wrappers, settings, and solver handles are released in reverse acquisition order. -- `SolvedInaccurate` is publishable only after independent strict residual and domain validation. -- Missing DLL, wrong architecture, version mismatch, invalid exports, and setup failure return structured solver outcomes. -- Native verbose output is disabled. - ---- - -## Locked File Structure - -```text -ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/ -├── IQpSolver.cs -├── QpSolveResult.cs -├── QpSolveStatus.cs -├── QpSolverSettings.cs -├── QuadraticProgram.cs -├── SparseCscMatrix.cs -├── SparseTripletBuilder.cs -└── Osqp/ - ├── OsqpNativeLoader.cs - ├── OsqpNativeMethods.cs - ├── OsqpNativeSolver.cs - ├── OsqpNativeStructures.cs - └── OsqpStatusMapper.cs - -ClumsyPilot/ThirdParty/OSQP/ -├── build-win-x64.ps1 -├── LICENSE -├── NOTICE -├── VERSION -├── SHA256SUMS -└── win-x64/osqp.dll - -ClumsyPilot/tests/EMPlannerVerificationHost/ -├── OptimizationChecks.cs -└── OsqpChecks.cs -``` - -## Shared Interfaces - -```csharp -public interface IQpSolver -{ - QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, - IReadOnlyList warmStart, CancellationToken cancellationToken); -} - -public sealed class QuadraticProgram -{ - public QuadraticProgram(SparseCscMatrix upperTriangularP, IReadOnlyList q, - SparseCscMatrix a, IReadOnlyList lowerBounds, - IReadOnlyList upperBounds); - public int VariableCount { get; } - public int ConstraintCount { get; } -} - -public sealed class QpSolveResult -{ - public QpSolveStatus Status { get; } - public IReadOnlyList Primal { get; } - public double Objective { get; } - public double PrimalResidual { get; } - public double DualResidual { get; } - public int Iterations { get; } - public TimeSpan SolveTime { get; } - public string NativeStatus { get; } - public string Diagnostic { get; } -} -``` - -Official references used to lock this ABI: - -- `https://osqp.org/docs/interfaces/C.html` -- `https://osqp.org/docs/get_started/migration_guide.html` -- `https://github.com/osqp/osqp/tree/v1.0.0` - -### Task 1: Solver-Neutral Sparse QP Contracts - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/IQpSolver.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveResult.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveStatus.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolverSettings.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QuadraticProgram.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/SparseCscMatrix.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/SparseTripletBuilder.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/OptimizationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: `NumericGuard` and cancellation tokens. -- Produces: the shared interfaces above and deterministic sparse-matrix assembly used by LS and ST. - -- [ ] **Step 1: Write failing CSC canonicalization checks** - -Build triplets in shuffled order with duplicate coordinates and assert the resulting CSC matrix: - -```text -has ColumnPointers length ColumnCount+1 -sorts row indices ascending inside each column -sums duplicate coordinates -drops exact zero sums -rejects NaN, infinity, negative indices, and out-of-range indices -stores only the upper triangle for P -``` - -Also construct the micro problem `min 0.5*x^2 - 2*x` subject to `0 <= x <= 1` and assert its immutable arrays cannot be changed through the source lists. - -- [ ] **Step 2: Run and verify solver contracts are absent** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- optimization -``` - -Expected: build failure naming `SparseTripletBuilder`. - -- [ ] **Step 3: Implement canonical CSC and QP validation** - -`SparseCscMatrix` stores copied arrays `Values`, `RowIndices`, and `ColumnPointers`. Validate monotonic pointers, `ColumnPointers[0]==0`, final pointer equals nonzero count, and all rows are in range. `QuadraticProgram` enforces square P, matching variable dimensions, matching constraint dimensions, `lower<=upper`, finite coefficients, and bounds limited to `±1e30` rather than CLR infinity. - -Use these exact statuses: - -```csharp -public enum QpSolveStatus -{ - Solved, - SolvedInaccurate, - PrimalInfeasible, - DualInfeasible, - MaximumIterations, - TimeLimit, - Cancelled, - SolverUnavailable, - InvalidProblem, - NativeError -} -``` - -- [ ] **Step 4: Run optimization checks** - -Expected: `PASS optimization`. - -- [ ] **Step 5: Commit QP contracts** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: add solver-neutral QP contracts" -``` - -### Task 2: Reproducible OSQP 1.0.0 Native Package - -**Files:** -- Create: all files under `ClumsyPilot/ThirdParty/OSQP/` - -**Interfaces:** -- Consumes: Git, CMake 3.18+, and a Visual Studio x64 compiler. -- Produces: a versioned `win-x64/osqp.dll` with a recorded SHA-256 and matching license files. - -- [ ] **Step 1: Write the native build script** - -`build-win-x64.ps1` must create a unique temporary directory, clone only tag `v1.0.0`, configure with this exact command shape, and remove the temporary directory in `finally`: - -```powershell -cmake -S $sourceRoot -B $buildRoot -A x64 ` - -DOSQP_ALGEBRA_BACKEND=builtin ` - -DOSQP_BUILD_SHARED_LIB=ON ` - -DOSQP_BUILD_STATIC_LIB=OFF ` - -DOSQP_BUILD_DEMO_EXE=OFF ` - -DOSQP_BUILD_UNITTESTS=OFF ` - -DOSQP_USE_FLOAT=OFF ` - -DOSQP_USE_LONG=OFF ` - -DOSQP_PACK_SETTINGS=OFF ` - -DOSQP_ENABLE_PRINTING=OFF ` - -DOSQP_CODEGEN=OFF ` - -DOSQP_ENABLE_DERIVATIVES=OFF -cmake --build $buildRoot --config Release --target osqp -``` - -The script resolves the generated DLL explicitly, verifies exactly one match, copies upstream `LICENSE` and `NOTICE`, writes `VERSION` with tag and build flags, computes `Get-FileHash -Algorithm SHA256`, and writes `SHA256SUMS` using a lowercase hexadecimal digest. - -- [ ] **Step 2: Execute the build script** - -```powershell -powershell -ExecutionPolicy Bypass -File ClumsyPilot/ThirdParty/OSQP/build-win-x64.ps1 -``` - -Expected: `win-x64/osqp.dll`, `LICENSE`, `NOTICE`, `VERSION`, and `SHA256SUMS` exist; the script prints `OSQP v1.0.0 win-x64 package ready`. - -- [ ] **Step 3: Verify architecture, exports, and hash** - -Use `dumpbin /headers` to assert machine `x64`, `dumpbin /exports` to assert `osqp_version`, `osqp_setup`, `osqp_solve`, and `osqp_cleanup`, then recompute SHA-256 and compare with `SHA256SUMS`. A missing tool is a failed packaging gate, not a skipped check. - -- [ ] **Step 4: Verify license contents came from the pinned tag** - -Compare bytes against the tag checkout before the temporary checkout is removed. Expected: exact equality for both files. - -- [ ] **Step 5: Commit the reproducible native package** - -```powershell -git add ClumsyPilot/ThirdParty/OSQP -git commit -m "build: pin OSQP 1.0.0 win-x64" -``` - -### Task 3: Absolute-Path Native Loader and ABI Structures - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeLoader.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeMethods.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeStructures.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: the pinned DLL and Task 1 QP contracts. -- Produces: a process-lifetime loader plus exact OSQP 1.0.0 double/int32 structures. - -- [ ] **Step 1: Write failing loader checks** - -Copy the verification host output to a temporary `plugins` directory with `ClumsyPilot.dll` and, in separate cases, no native DLL, a text file named `osqp.dll`, and the real DLL. Assert the first two return `SolverUnavailable` diagnostics without `BadImageFormatException` escaping; the real DLL reports version `1.0.0`. Start 16 parallel first-use calls and assert a single stable module handle. - -- [ ] **Step 2: Run the OSQP group without loader implementation** - -Expected: build failure naming `OsqpNativeLoader`. - -- [ ] **Step 3: Implement loader and ABI definitions** - -Use Windows `LoadLibraryW`, `GetProcAddress`, and `FreeLibrary` from `kernel32`; resolve the plugin directory from `typeof(OsqpNativeLoader).Assembly.Location`. Reject `IntPtr.Size != 8`. Preload the absolute sibling path and retain the handle for process lifetime. - -Define `OSQPInt` as C# `int` and `OSQPFloat` as C# `double`, matching the pinned build. Define sequential layouts for `OSQPCscMatrix`, `OSQPSettings`, `OSQPInfo`, `OSQPSolution`, and the four-pointer prefix of `OSQPSolver` exactly as the v1.0.0 public headers specify. Add an internal layout check for expected offsets and total sizes before the first solve. - -Declare only these native functions initially: - -```text -osqp_version -osqp_set_default_settings -osqp_setup -osqp_warm_start -osqp_solve -osqp_cleanup -``` - -Do not depend on `OSQPCscMatrix_new`, `OSQPCscMatrix_free`, `OSQPSettings_new`, or `OSQPSettings_free`: those helpers are not marked with the public export macro in the pinned header. Allocate the two CSC structures and settings block with `Marshal.AllocHGlobal`, initialize settings through `osqp_set_default_settings`, and release those managed-owned blocks with `Marshal.FreeHGlobal`. - -- [ ] **Step 4: Run missing, corrupt, real, and concurrent loader checks** - -Expected: `PASS osqp-loader` and no process crash. - -- [ ] **Step 5: Commit loader and structures** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: load pinned OSQP native library" -``` - -### Task 4: OSQP Solve Lifecycle and Status Mapping - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeSolver.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpStatusMapper.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs` - -**Interfaces:** -- Consumes: `IQpSolver`, Task 3 native functions, and `QpSolverSettings`. -- Produces: `OsqpNativeSolver : IQpSolver`. - -- [ ] **Step 1: Write failing solve/status checks** - -Test three fixed QPs: - -```text -bounded optimum: min 0.5*x^2 - 2*x, 0<=x<=1, expected x=1 -equality optimum: min x^2+y^2, x+y=1, expected x=y=0.5 -infeasible: x>=1 and x<=0, expected PrimalInfeasible -``` - -Assert residuals, iteration count, objective, native status, and solve time are populated. Add a `1e-9 second` time-limit case that maps only to `TimeLimit` or a valid solved status; no native status may be silently treated as solved. - -- [ ] **Step 2: Run and verify `OsqpNativeSolver` is absent** - -Expected: build failure naming `OsqpNativeSolver`. - -- [ ] **Step 3: Implement one-shot native ownership** - -Pin P/Q/A/L/U and optional warm-start arrays; allocate and populate P/A `OSQPCscMatrix` blocks; allocate settings and initialize it through `osqp_set_default_settings`; overwrite `verbose=0`, `warm_starting`, `polishing`, `max_iter`, `eps_abs`, `eps_rel`, and `time_limit`; call setup, optional warm start, solve, then marshal solution and info. Copy all result values before cleanup. Release the solver through `osqp_cleanup`, then settings/matrix blocks through `Marshal.FreeHGlobal`, then array pins in reverse order inside `finally`. - -Map native status values exactly: - -```text -1 Solved -2 SolvedInaccurate -3/4 PrimalInfeasible -5/6 DualInfeasible -7 MaximumIterations -8 TimeLimit -9/10/11 NativeError -``` - -Cancellation is checked before native setup and after solve. OSQP's configured time limit is the bound for a solve already inside native code. - -- [ ] **Step 4: Run all OSQP checks repeatedly** - -```powershell -1..20 | ForEach-Object { - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp - if ($LASTEXITCODE -ne 0) { throw "OSQP verification failed on iteration $_" } -} -``` - -Expected: every iteration prints `PASS osqp-loader` and `PASS osqp-solve`. - -- [ ] **Step 5: Commit solver lifecycle** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs -git commit -m "feat: solve QPs through OSQP" -``` - -### Task 5: Backend Completion Gate - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: all tasks in this plan. -- Produces: the stable `IQpSolver` boundary required by LS and ST plans. - -- [ ] **Step 1: Document native deployment and diagnostics** - -Add the exact source/deployment layouts, pinned version, build flags, license placement, absolute loading rule, and solver status mapping to the README. - -- [ ] **Step 2: Run optimization and OSQP gates** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- optimization -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp -git diff --check -``` - -Expected: both groups pass and Git reports no whitespace errors. - -- [ ] **Step 3: Verify the DLL is self-contained** - -Run a dependency inspection on `win-x64/osqp.dll`. Expected: only Windows system/runtime DLLs; no MKL, CUDA, or separately deployed QDLDL DLL. - -- [ ] **Step 4: Verify clean plugin-directory loading** - -Copy only `ClumsyPilot.dll` and `osqp.dll` to a fresh directory, copy the verification host executable beside them, and run the micro QP with the working directory set elsewhere. Expected: solved result, proving loading does not depend on current directory. - -- [ ] **Step 5: Commit backend documentation** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs -git commit -m "docs: describe OSQP plugin deployment" -``` - -## Completion Gate - -- `IQpSolver` contains no OSQP-specific type. -- Pinned native metadata, license, notice, hash, and DLL agree with OSQP v1.0.0. -- Loader failures are structured and never terminate the host. -- Fixed feasible and infeasible QPs map to the correct statuses with finite diagnostics. -- Twenty repeated solve/cleanup cycles pass without handle growth or access violations. diff --git a/docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md b/docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md deleted file mode 100644 index 2178ed8..0000000 --- a/docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md +++ /dev/null @@ -1,375 +0,0 @@ -# EM Planner Rolling Execution and Plugin Deployment Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add version-safe rolling replanning, trajectory handoff, gear-switch execution states, controller adaptation, and deterministic packaging of `ClumsyPilot.dll` with `osqp.dll` and licenses. - -**Architecture:** Keep `EmPlanningService` pure and place scheduling, cancellation, stale-result suppression, previous-trajectory reuse, and command generation in a sibling `TrajectoryExecution` module. The coordinator publishes only a fully validated current-version trajectory; the executor samples that immutable trajectory, manages zero-speed gear transitions, and converts its fields into a generic controller command without coupling EM optimization to the hardware API. - -**Tech Stack:** C# 10, .NET Standard 2.0, `Task`/`CancellationToken`, immutable EM trajectories, PowerShell packaging, Windows x64 plugin layout. - -## Global Constraints - -- This plan depends on all four preceding implementation plans. -- Default replan period is `0.20 s`; time horizon `6.0 s`; distance horizon `5.0 m`; handoff lookahead `0.30 s`. -- The coordinator accepts captured `VehicleMotionState`; it does not read localization, wheel speed, UI, or hardware directly. -- Each cycle binds `MapSnapshotId`, `ReferencePathId`, `VehicleState.SequenceId`, `PreviousTrajectoryId`, and `SegmentIndex`. -- A result is publishable only if all bound identities still match the latest cycle and the cycle version is current. -- Normal replans hand off from the previous trajectory only when tracking error and age are within configuration limits and no gear boundary is crossed. -- If a replan fails, the previously published trajectory remains executable and ends in its own zero-speed safety tail. -- Gear change occurs only after measured speed remains below `0.01 m/s` for at least `0.20 s`. -- The executor never commands lateral body velocity or in-place rotation. -- The generic control command uses signed longitudinal velocity and yaw rate; world `vx/vy`, speed, curvature, and pose remain available for monitoring. -- No dynamic-obstacle prediction or dynamic behavior state is introduced. -- Plugin runtime layout is `plugins/ClumsyPilot.dll`, `plugins/osqp.dll`, and `plugins/licenses/*`. -- Deployment refuses a non-x64 host/runtime package or an OSQP hash mismatch. - ---- - -## Locked File Structure - -```text -ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/ -├── EmPlanningCoordinator.cs -├── GearSwitchStateMachine.cs -├── GearSwitchState.cs -├── IEmPlanningCycleSink.cs -├── IVehicleStateProvider.cs -├── PlanningCycleIdentity.cs -├── PlanningCycleInput.cs -├── PlanningCycleResult.cs -├── TrajectoryControlAdapter.cs -├── TrajectoryControlCommand.cs -├── TrajectoryExecutionState.cs -├── TrajectoryExecutor.cs -├── TrajectoryHandoffSelector.cs -└── TrajectorySampler.cs - -ClumsyPilot/scripts/ -└── Publish-ClumsyPilotPlugin.ps1 - -ClumsyPilot/tests/EMPlannerVerificationHost/ -├── CoordinatorChecks.cs -├── ExecutorChecks.cs -└── PluginPackagingChecks.cs -``` - -## Shared Interfaces - -```csharp -public interface IVehicleStateProvider -{ - VehicleMotionState Capture(); -} - -public sealed class EmPlanningCoordinator -{ - public EmPlanningCoordinator(IEmPlanningService planningService, - IEmPlanningCycleSink sink = null); - public Task PlanLatestAsync(PlanningCycleInput input, - CancellationToken cancellationToken); - public EmTrajectory PublishedTrajectory { get; } -} - -public sealed class TrajectoryExecutor -{ - public TrajectoryExecutionState State { get; } - public TrajectoryControlCommand Update(DateTimeOffset now, - VehicleMotionState measuredState, EmTrajectory trajectory); -} - -public sealed class TrajectoryControlCommand -{ - public double SignedLongitudinalVelocity { get; } - public double YawRate { get; } - public TravelDirection Direction { get; } - public bool RequestDirectionChange { get; } - public bool HoldBrake { get; } - public bool IsTrajectoryComplete { get; } -} -``` - -### Task 1: Cycle Identity, Scheduling Decision, and Stale-Result Suppression - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleIdentity.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleInput.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleResult.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/IEmPlanningCycleSink.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: pure `IEmPlanningService`, captured request inputs, and configuration scheduling values. -- Produces: latest-wins asynchronous planning and a read-only published trajectory. - -- [ ] **Step 1: Write failing coordinator concurrency checks** - -Use a controllable fake planning service to start cycle A, then cycle B before A completes. Complete B first with success and A later with success. Assert B alone is published and A returns `Superseded`. Add checks for map ID, reference ID, state sequence, segment, and prior trajectory ID changes invalidating an otherwise successful result. - -Also assert `ShouldStartCycle(now)` is false before `0.20 s` and true at exactly `0.20 s`; this decision uses caller-supplied time. - -- [ ] **Step 2: Run and verify coordinator types are absent** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -``` - -Expected: build failure naming `EmPlanningCoordinator`. - -- [ ] **Step 3: Implement latest-wins coordination** - -Use an incrementing `long` cycle version, a private lock only around publication state, and a per-cycle linked cancellation source. Starting a newer cycle cancels the prior source. After planning, compare the complete identity and version again under the publication lock; map stale success to `Superseded` without exposing its trajectory. - -Sink exceptions are caught and reported in the cycle diagnostic. Never hold the publication lock while running the planner or invoking a sink. - -- [ ] **Step 4: Run concurrency checks 100 times** - -```powershell -1..100 | ForEach-Object { - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator - if ($LASTEXITCODE -ne 0) { throw "Coordinator verification failed on iteration $_" } -} -``` - -Expected: every iteration prints `PASS coordinator`. - -- [ ] **Step 5: Commit coordinator identity logic** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "feat: coordinate rolling EM replans" -``` - -### Task 2: Safe Previous-Trajectory Handoff - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectorySampler.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryHandoffSelector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs` - -**Interfaces:** -- Consumes: current published trajectory, measured state, latest segment identity, and `0.30 s` handoff lookahead. -- Produces: either a future trajectory-derived start state/seed or a measured-state start with no seed. - -- [ ] **Step 1: Write failing handoff acceptance/rejection checks** - -Assert acceptance only when trajectory age is valid, position/yaw/speed tracking errors are inside configured tolerances, the future sample remains on the same segment/direction, and the interval contains no gear boundary. Assert rejection for stale trajectory, large error, terminal proximity, segment mismatch, direction mismatch, and a handoff time beyond the trajectory. - -- [ ] **Step 2: Run and verify selector is absent** - -Expected: build failure naming `TrajectoryHandoffSelector`. - -- [ ] **Step 3: Implement time interpolation and selection** - -Binary-search `TimeFromStart`, interpolate X/Y, unwrapped yaw, signed speed, curvature, and PathS, then derive redundant fields through the trajectory-point constructor. Never interpolate across different boundary types, segment indices, or directions. Return a result object that states `PreviousTrajectory` or `MeasuredState` and includes a deterministic rejection reason. - -- [ ] **Step 4: Run handoff checks** - -Expected: `PASS coordinator`, including a reverse same-segment handoff. - -- [ ] **Step 5: Commit handoff logic** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs -git commit -m "feat: select safe EM trajectory handoffs" -``` - -### Task 3: Gear-Switch State Machine and Trajectory Executor - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchState.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchStateMachine.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutionState.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutor.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: measured state, current time, sampled trajectory, and exact boundary types. -- Produces: zero-speed holding, one-shot direction-change request, confirmed departure, and completion states. - -- [ ] **Step 1: Write failing state-transition checks** - -Cover this exact sequence: - -```text -Following -> ApproachingGearSwitch -ApproachingGearSwitch -> HoldingZero when command reaches boundary -HoldingZero remains while |measured speed| >= 0.01 m/s -HoldingZero timer resets if speed rises above tolerance -HoldingZero -> RequestingDirectionChange after continuous 0.20 s below tolerance -RequestingDirectionChange emits exactly one request -AwaitingDirectionConfirmation holds zero -confirmed direction -> Following next segment -goal/rolling terminal -> Completed while holding zero -``` - -Assert no transition can output nonzero signed speed during holding or direction confirmation. - -- [ ] **Step 2: Run and verify executor types are absent** - -Expected: build failure naming `GearSwitchStateMachine`. - -- [ ] **Step 3: Implement explicit state and event inputs** - -The state machine receives caller-supplied `now`, measured signed speed, desired/current directions, and a boolean direction-confirmation input. It does not call hardware. `TrajectoryExecutor` samples the trajectory, delegates boundary behavior to the state machine, and returns an immutable execution state containing the selected point and reason. - -- [ ] **Step 4: Run executor state checks** - -Expected: `PASS executor`, including forward-to-reverse and reverse-to-forward sequences. - -- [ ] **Step 5: Commit executor state machine** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs -git commit -m "feat: execute EM gear-switch boundaries" -``` - -### Task 4: Generic Control Adapter - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlCommand.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlAdapter.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/IVehicleStateProvider.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutor.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs` - -**Interfaces:** -- Consumes: an `EmTrajectoryPoint` and gear-switch execution state. -- Produces: signed linear velocity, yaw rate, direction-change request, brake hold, and completion flags. - -- [ ] **Step 1: Write failing command-mapping checks** - -For forward and reverse points assert command signed velocity and yaw rate equal the point fields exactly. Assert `Speed`, world `vx/vy`, pose, and curvature remain available in execution telemetry but are not reinterpreted as body lateral velocity. Assert holding states always override both command velocities to zero. - -- [ ] **Step 2: Run and verify adapter is absent** - -Expected: build failure naming `TrajectoryControlAdapter`. - -- [ ] **Step 3: Implement adapter without hardware coupling** - -`TrajectoryControlAdapter.CreateCommand` copies signed longitudinal velocity and yaw rate in normal following. For hold, switch, invalid, or completed states it sets both to zero and sets the corresponding flags. Do not reference `MultiVehicleScriptVx`, `MultiVehicleScriptVy`, or `MultiVehicleScriptVth`; a later hardware-specific adapter may map this generic command after the existing controller field semantics are confirmed. - -- [ ] **Step 4: Run adapter and executor checks** - -Expected: `PASS executor`; add an invariant that ordinary following never requests nonzero body lateral velocity or `v=0, yawRate!=0`. - -- [ ] **Step 5: Commit control adaptation** - -```powershell -git add ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs -git commit -m "feat: adapt EM trajectories to control commands" -``` - -### Task 5: Plugin Output and License Packaging - -**Files:** -- Modify: `ClumsyPilot/ClumsyPilot.csproj` -- Create: `ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** -- Consumes: a successfully built `ClumsyPilot.dll` plus the pinned OSQP package. -- Produces: the exact deployable `plugins` tree. - -- [ ] **Step 1: Write failing packaging checks** - -Create a temporary output directory, invoke the future script, and assert exactly: - -```text -plugins/ClumsyPilot.dll -plugins/osqp.dll -plugins/licenses/OSQP-LICENSE.txt -plugins/licenses/OSQP-NOTICE.txt -plugins/licenses/OSQP-VERSION.txt -``` - -Assert the deployed native hash equals `SHA256SUMS`, `ClumsyPilot.dll` is a managed assembly, OSQP is x64, and rerunning packaging replaces files without leaving stale temporary files. - -- [ ] **Step 2: Run and verify packaging script is absent** - -Expected: `PluginPackagingChecks` fails because `Publish-ClumsyPilotPlugin.ps1` does not exist. - -- [ ] **Step 3: Add build-output metadata and transactional publish script** - -Add these items to `ClumsyPilot.csproj` without rewriting existing targets: - -```xml - - - - -``` - -The publish script takes mandatory `-ManagedDll` and `-OutputDirectory`, resolves both absolute paths, validates inputs and hash, stages the five files in a unique sibling temporary directory, then renames the completed `plugins` directory into place. It refuses to operate when the resolved output is a drive root or workspace root. - -- [ ] **Step 4: Run packaging checks against a fresh directory** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package -``` - -Expected: `PASS plugin-package`; no files exist outside the temporary test root. - -- [ ] **Step 5: Commit packaging** - -```powershell -git add ClumsyPilot/ClumsyPilot.csproj ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1 ClumsyPilot/tests/EMPlannerVerificationHost -git commit -m "build: package ClumsyPilot with OSQP" -``` - -### Task 6: Rolling End-to-End and Final Gate - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` - -**Interfaces:** -- Consumes: all core, coordinator, executor, and packaging components. -- Produces: a complete first-version EM Planner workflow with safe rolling fallback. - -- [ ] **Step 1: Add deterministic rolling scenarios** - -Simulate caller-supplied time and measured states for: normal repeated forward replans, reverse replans, a solver failure with old-trajectory continuation, a superseded slow cycle, tracking-error reset to measured state, forward/reverse gear switch, and final goal stop. Assert every executed command comes from a currently published validated trajectory or is a zero hold. - -- [ ] **Step 2: Add failure-tail assertions** - -Force all new plans to fail after a successful publication. Advance time through the old trajectory and assert it reaches its exact zero-speed terminal and stays zero; no extrapolated nonzero command is allowed after its final point. - -- [ ] **Step 3: Run the full first-version gate** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -git diff --check -``` - -Expected groups include foundation, OSQP, lateral, longitudinal, trajectory, facade, coordinator, executor, and plugin packaging, all with PASS output. - -- [ ] **Step 4: Update README with ownership and deployment** - -Document the pure-planner/coordinator/executor boundary, caller responsibilities, update cadence, handoff rules, gear state sequence, all trajectory fields, generic controller command, OSQP files, packaging command, and explicitly deferred dynamic-obstacle scope. - -- [ ] **Step 5: Commit rolling integration evidence** - -```powershell -git add ClumsyPilot/tests/EMPlannerVerificationHost ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md -git commit -m "test: verify rolling EM execution" -``` - -## Completion Gate - -- One slow cycle can never overwrite a newer published trajectory. -- All handoffs remain within one segment/direction and use measured state when tracking is unsafe. -- Failed replans leave a complete prior trajectory that terminates safely at zero. -- Gear changes require measured zero-speed dwell and emit one explicit request. -- Generic control output never invents crab or in-place-rotation behavior. -- Packaging produces the exact DLL/license tree and validates the pinned native hash. diff --git a/docs/superpowers/plans/2026-08-03-em-planner-windowed-execution-implementation.md b/docs/superpowers/plans/2026-08-03-em-planner-windowed-execution-implementation.md deleted file mode 100644 index bec3123..0000000 --- a/docs/superpowers/plans/2026-08-03-em-planner-windowed-execution-implementation.md +++ /dev/null @@ -1,247 +0,0 @@ -# EM Planner Windowed Execution Bootstrap Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Initialize the single cross-window EM Planner progress file with verified protocol state and a complete Stage 1 startup prompt. - -**Architecture:** Keep all durable cross-window state in one committed Markdown file. The file points to the approved design and implementation plans, records that no implementation stage has started, preserves the known dirty-worktree baseline, and gives the next Codex window enough exact instructions to execute only Foundation Tasks 1–3. - -**Tech Stack:** Markdown, Git, PowerShell validation, existing EM Planner design and implementation plans. - -## Global Constraints - -- Follow `docs/superpowers/specs/2026-08-03-em-planner-windowed-execution-design.md` exactly. -- Create only `docs/superpowers/progress/em-planner-progress.md` during this bootstrap. -- Stage 1 begins in `NotStarted`; no EM production or test code is implemented in this bootstrap. -- The branch is `trajplanner` and the protocol design baseline is commit `554c84f`. -- The five EM implementation plans are anchored by commit `8dd8ff0`. -- Existing unrelated worktree changes remain untouched and unstaged. -- Never use `git add .`, `git add -A`, destructive reset, checkout restoration, or untracked-file cleanup. -- The initial next-window prompt must prohibit subagents and restrict execution to Foundation Tasks 1–3. -- The progress checkpoint is committed separately from every later functionality commit. - ---- - -## Locked File Structure - -```text -docs/superpowers/progress/ -└── em-planner-progress.md -``` - -### Task 1: Initialize the Cross-Window Progress Checkpoint - -**Files:** -- Create: `docs/superpowers/progress/em-planner-progress.md` - -**Interfaces:** -- Consumes: design commit `554c84f`, plan commit `8dd8ff0`, the ten-stage mapping, and the existing dirty-worktree baseline. -- Produces: the durable Stage 1 entry state and a complete prompt for a fresh Codex window. - -- [ ] **Step 1: Run the failing precondition check** - -Run: - -```powershell -$progress = 'docs/superpowers/progress/em-planner-progress.md' -if (Test-Path -LiteralPath $progress) { - throw "Progress file already exists; inspect it instead of overwriting it." -} -throw "Expected bootstrap failure: progress file does not exist." -``` - -Expected: nonzero exit with `Expected bootstrap failure: progress file does not exist.` If the file already exists, stop and reconcile it with the design rather than replacing user state. - -- [ ] **Step 2: Verify protocol and plan baselines before creating state** - -Run: - -```powershell -git cat-file -e '554c84f^{commit}' -if ($LASTEXITCODE -ne 0) { throw 'Windowed-execution design commit is missing.' } -git cat-file -e '8dd8ff0^{commit}' -if ($LASTEXITCODE -ne 0) { throw 'EM implementation-plan commit is missing.' } -if ((git branch --show-current) -ne 'trajplanner') { throw 'Expected trajplanner branch.' } -$staged = @(git diff --cached --name-only) -if ($staged.Count -ne 0) { throw "Unexpected staged files: $($staged -join ', ')" } -Write-Output 'PASS windowed-execution bootstrap prerequisites' -``` - -Expected: `PASS windowed-execution bootstrap prerequisites` and exit code `0`. - -- [ ] **Step 3: Create the exact initial progress document** - -Create the file with this complete content: - -````markdown -# EM Planner Execution Progress - -## Current State - -- Current stage: 1 — Foundation contracts and boundaries -- Stage status: NotStarted -- Current branch: `trajplanner` -- Last checkpoint commit: None; Stage 1 has not started -- Protocol design baseline: `554c84f` -- Implementation-plan baseline: `8dd8ff0` - -## Completed Tasks - -| Stage | Plan | Tasks | Commits | Verification | -|---:|---|---|---|---| -| — | — | — | — | No implementation task has started | - -## Current Verification - -- Commands: - - `git cat-file -e 554c84f^{commit}` - - `git cat-file -e 8dd8ff0^{commit}` - - `git branch --show-current` - - `git diff --cached --name-only` -- Result: Bootstrap prerequisites passed; Stage 1 entry is ready -- Verified commit: `554c84f` for protocol design and `8dd8ff0` for implementation plans - -## Preserved Workspace State - -- The workspace already contains many unrelated Map, CoarsePath, PathSmoothing, project-file, report, and documentation changes. They belong to the user and must remain untouched and unstaged. -- Use explicit file paths for every `git add`; never stage the whole workspace. -- Inspect the existing diff before modifying `ClumsyPilot/ClumsyPilot.csproj`, then append only the Stage 1 rules required by the Foundation plan. -- The normal `ClumsyPilot.csproj` build can be blocked by legacy `auto_avoidance/MultiWheelAutoAvoidance.cs` references to `NetTopologySuite` and `OpenCvSharp`. -- Stage 1 must use the isolated `EMPlannerVerificationHost` rule from the Foundation plan. Do not delete or rewrite legacy functionality to hide the baseline build failure. - -## Decisions Needed - -- None. The Stage 1 scope and contracts are approved. - -## Next Stage - -- Stage: 1 — Foundation contracts and boundaries -- Plan: `docs/superpowers/plans/2026-08-03-em-planner-foundation-implementation.md` -- Tasks: - - Task 1 — Verification Host and Immutable Contracts - - Task 2 — Configuration, Diagnostics, and Request Validation - - Task 3 — Direction Segmentation and Exact Boundary Anchors -- Entry checks: - - Confirm branch `trajplanner`. - - Confirm commits `554c84f` and `8dd8ff0` exist. - - Confirm the staging area is empty. - - Capture `git status --short` before editing. - - Confirm no prior Stage 1 functionality commit is recorded; if EM files already exist, inspect and reconcile them before continuing. -- Exit gate: - - Each of Foundation Tasks 1–3 has its own passing verification and independent commit. - - The Foundation verification host passes `foundation` and `segmentation` groups required through Task 3. - - Exact gear-switch approach/departure identities and exact horizon anchors are verified. - - `git diff --check` reports no new whitespace errors. - - The staging area is empty after all task commits. - - This progress file is updated to mark Stage 1 completed and Stage 2 ready, then committed alone. - -## Next-Window Prompt - -```text -请继续 ParkingRobot 仓库的 EM Planner 多窗口实施。 - -工作目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -本窗口是阶段 1:Foundation 契约与边界,只执行 Foundation 实施计划的 Task 1–3,不得提前执行 Task 4 或后续阶段。 - -开始工作前必须完整读取: -1. docs/superpowers/specs/2026-08-03-em-planner-ls-st-design.md -2. docs/superpowers/specs/2026-08-03-em-planner-windowed-execution-design.md -3. docs/superpowers/progress/em-planner-progress.md -4. docs/superpowers/plans/2026-08-03-em-planner-foundation-implementation.md - -执行时使用 executing-plans、test-driven-development 和 verification-before-completion 技能,并完整读取对应 SKILL.md。不要使用子代理。 - -开始前: -- 确认工作目录和分支 trajplanner; -- 确认提交 554c84f 与 8dd8ff0 存在; -- 检查 git status --short 和暂存区; -- 保存开始时的工作区状态用于结束时对照; -- 当前工作区存在大量用户修改,全部保留,不得清理、覆盖或顺带提交; -- 如果进度文件、Git 或实际文件不一致,先诊断并报告,不要直接覆盖。 - -阶段范围: -- Task 1:Verification Host and Immutable Contracts; -- Task 2:Configuration, Diagnostics, and Request Validation; -- Task 3:Direction Segmentation and Exact Boundary Anchors。 - -执行要求: -- 严格按 Foundation 计划逐任务进行 TDD; -- 每个任务先运行计划指定的失败验证,再实现最小内容,再运行通过验证; -- 每个任务形成独立提交,只用显式路径 git add; -- 修改 ClumsyPilot/ClumsyPilot.csproj 前先查看现有差异,只追加本阶段要求; -- 不修改无关 Map、CoarsePath、PathSmoothing 和旧 auto_avoidance 文件; -- 不实现 Frenet、静态走廊、OSQP、LS、ST、滚动执行或动态障碍功能; -- 主项目既有 auto_avoidance 依赖失败不能通过删除旧功能规避;使用计划中的隔离验证宿主; -- 任何完成声明前运行计划指定验证和 git diff --check。 - -阶段结束时: -- 确认 Task 1–3 各有独立提交和测试证据; -- 运行截至 Task 3 的 Foundation 阶段出口验证; -- 核对每个提交只包含计划文件,暂存区为空; -- 更新 docs/superpowers/progress/em-planner-progress.md:记录提交、验证命令和结果,将阶段 1 标为 Completed,将阶段 2 标为 ready; -- 只暂存进度文件并单独提交阶段检查点; -- 返回本阶段完成内容、提交列表、测试证据、遗留问题; -- 给出阶段 2 可直接复制的新窗口提示词。 -``` -```` - -- [ ] **Step 4: Validate structure, task scope, and prompt completeness** - -Run: - -```powershell -$progress = 'docs/superpowers/progress/em-planner-progress.md' -$content = Get-Content -LiteralPath $progress -Raw -Encoding UTF8 -$required = @( - 'Stage status: NotStarted', - 'Protocol design baseline: `554c84f`', - 'Implementation-plan baseline: `8dd8ff0`', - 'Task 1 — Verification Host and Immutable Contracts', - 'Task 2 — Configuration, Diagnostics, and Request Validation', - 'Task 3 — Direction Segmentation and Exact Boundary Anchors', - '不要使用子代理', - '不得提前执行 Task 4', - '只暂存进度文件并单独提交阶段检查点' -) -foreach ($term in $required) { - if (-not $content.Contains($term)) { throw "Missing progress item: $term" } -} -if ($content.Contains('Task 4 — Reverse-Safe')) { throw 'Stage 1 prompt includes Stage 2 implementation.' } -$fences = ([regex]::Matches($content, '```')).Count -if (($fences % 2) -ne 0) { throw "Unbalanced code fences: $fences" } -git diff --check -- $progress -if ($LASTEXITCODE -ne 0) { throw 'Progress-file diff check failed.' } -Write-Output 'PASS initial EM Planner progress checkpoint' -``` - -Expected: `PASS initial EM Planner progress checkpoint` and exit code `0`. - -- [ ] **Step 5: Commit only the initialized progress checkpoint** - -Run: - -```powershell -$progress = 'docs/superpowers/progress/em-planner-progress.md' -$before = @(git diff --cached --name-only) -if ($before.Count -ne 0) { throw "Unexpected staged files: $($before -join ', ')" } -git add -- $progress -$staged = @(git diff --cached --name-only) -if ($staged.Count -ne 1 -or $staged[0] -ne $progress) { - throw "Staged scope is not progress-only: $($staged -join ', ')" -} -git diff --cached --check -if ($LASTEXITCODE -ne 0) { throw 'Staged diff check failed.' } -git commit -m "docs: initialize EM planner execution progress" -``` - -Expected: one commit containing only `docs/superpowers/progress/em-planner-progress.md`; the staging area is empty afterward. - -## Completion Gate - -- The progress file exists and names Stage 1 as `NotStarted`. -- It references the approved design and implementation-plan commits. -- It contains Foundation Tasks 1–3 exactly and excludes Stage 2 implementation. -- It records dirty-worktree and legacy-build protections. -- Its Stage 1 prompt is directly copyable into a fresh Codex window. -- The bootstrap commit contains only the progress file. diff --git a/docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md b/docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md deleted file mode 100644 index c60f19e..0000000 --- a/docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md +++ /dev/null @@ -1,106 +0,0 @@ -# LocalG2-Only PathSmoothing Reorganization Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` to execute this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Convert `PathSmoothing` into a LocalG2-only module, remove the three legacy smoothing algorithms, preserve LocalG2 visualization and fixture workflows, and organize the source tree and README using the established `CoarsePath` module pattern. - -**Architecture:** The production facade always runs the LocalG2 pipeline. Shared path preparation and validation remain intact; B-spline, local Bezier, and piecewise quintic implementations and their configuration are removed. Offline reports remain a factual comparison of raw coarse path versus LocalG2 only, with visualization sources placed below an `Output` layer like `CoarsePath`. - -**Tech Stack:** C# 10, .NET SDK, Newtonsoft.Json, existing System.Drawing/StbImageWriteSharp report exporter, PowerShell verification hosts. - -## Global Constraints - -- Do not read, search, enumerate, copy, modify, delete, stage, or commit `ClumsyPilot/ParkrobTrajplanner/auto_avoidance`; do not enumerate `ClumsyPilot/ParkrobTrajplanner` as a parent. -- Preserve LocalG2 candidate construction, validation, publication statuses, fixture data, diagnostic candidate visualization, and generated report artifacts below `ClumsyPilot/obj/path_smoothing_reports`. -- Remove all production references to `CubicBSpline`, `LocalCubicBezier`, and `PiecewiseQuintic` smoothing. -- Retain the raw-path baseline in reports. Normal reports must contain only raw and LocalG2 series and four figures; diagnostic reports may append the already-rejected LocalG2 candidate as a fifth figure. -- Maintain current default `MinimumClearanceReserveMeters = 0d`. -- Do not delete unrelated user work or generated report directories. - ---- - -### Task 1: Establish a LocalG2-only verification contract - -**Files:** -- Modify: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` -- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` -- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` - -**Interfaces:** -- The comparison request exposes exactly one requested method: `SmoothingMethod.LocalG2Quintic`. -- A normal report has a raw baseline plus one LocalG2 row/series; the diagnostic report retains its optional rejected candidate figure. - -- [ ] Add failing assertions that reject the three removed enum names, require one requested comparison method, require two normal figure series, and require exactly two CSV rows after the header. -- [ ] Run the focused PowerShell checks and confirm they fail against the four-algorithm implementation. -- [ ] Update host assertions for the new two-series normal report while retaining the seven-file diagnostic contract. -- [ ] Re-run the focused checks after Tasks 2 and 3 and record the exit codes. - -### Task 2: Remove legacy algorithms and simplify the production facade - -**Files:** -- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/` -- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs` -- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs` -- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` - -**Interfaces:** -- `SmoothingMethod` retains only `LocalG2Quintic`. -- `PathSmoothingConfiguration` defaults `Method` to `LocalG2Quintic` and exposes only shared safety/sampling fields and `LocalG2Quintic` options. -- `PathSmoothingService.Smooth(request, cancellationToken)` directly validates/prepares/builds the raw baseline and invokes `LocalG2PreSmoothingPipeline`. - -- [ ] Delete legacy source files only after their callers are removed. -- [ ] Remove legacy smoothness/retry configuration and cloning code; preserve output spacing, collision step, clearance reserve, and LocalG2 options. -- [ ] Replace the multi-method resolver and fallback path in `PathSmoothingService` with its LocalG2-only route. -- [ ] Compile the isolated PathSmoothing host and confirm no source references to the removed methods remain in allowed paths. - -### Task 3: Reorganize report sources into an Output layer and reduce the report model - -**Files:** -- Move: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/` to `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/` -- Move: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/` to `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/` -- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingMethodRanker.cs` -- Modify: moved comparison request/result/service consumers and all moved visualization files. - -**Interfaces:** -- `PathSmoothingComparisonRequest` owns one immutable LocalG2 request rather than a caller-selectable method list. -- `PathSmoothingComparisonResult` contains a raw baseline and exactly one LocalG2 entry. -- Normal figure and CSV builders emit `RawPath` and `LocalG2Quintic` only. - -- [ ] Move source directories with their namespaces changed from `PathSmoothing.Comparison` and `PathSmoothing.Visualization` to `PathSmoothing.Output.Comparison` and `PathSmoothing.Output.Visualization`. -- [ ] Simplify comparison execution to warm up and measure LocalG2 only; retain deterministic timing/digest behavior for its sole entry. -- [ ] Remove visual style colors, legend rows, labels, metric rows, and all source references for the three deleted algorithms. -- [ ] Publish four normal figures with stable stems `01-coarse-path-overview`, `02-all-paths-comparison`, `03-local-g2-overview`, and `04-curvature-comparison`; append `05-local-g2-diagnostic-candidate` only to an augmented diagnostic model. -- [ ] Update all allowed source/test imports to the new `Output` namespaces. - -### Task 4: Align test/demo entry points and document the module - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` - -**Interfaces:** -- Fixture reports use the LocalG2-only comparison request and retain all eight fixtures. -- The README mirrors the `CoarsePath/README.md` information architecture for LocalG2 inputs, safety gates, result statuses, report output, and known limitations. - -- [ ] Update test/demo imports and expected report shapes for the Output namespaces and LocalG2-only model. -- [ ] Create `README.md` with the following ordered sections: Module Overview, File Structure, Smoothing Data Flow, Result Status and Publication Rules, Coordinates and Units, Minimal Call Example, Detailed Usage Guide, Fixture Reports and Visualization, Common Errors, and First-Version Limits. -- [ ] State explicitly that a candidate passing collision validation may still be retained when its quality gate fails, and that `0 m` reserve removes only the additional clearance reserve, not collision or curvature checks. - -### Task 5: Verify source layout and retain visualization artifacts - -**Files:** -- Verify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/` -- Verify: `ClumsyPilot/obj/path_smoothing_reports/` - -- [ ] Build and run the isolated current-source LocalG2 visualization host against all eight fixture scenarios. -- [ ] Confirm all normal report directories contain the expected four PNG/SVG figures and CSV, and that `02-all-paths-comparison.png` presents raw plus LocalG2 only. -- [ ] Run the focused comparison/SVG/diagnostic verification scripts where their dependencies are available; report any root-build limitation separately. -- [ ] Inspect at least the `single-turn` normal report and `05-local-g2-diagnostic-candidate.png` to confirm LocalG2 labels, nonblank rendering, and retained diagnostic semantics. -- [ ] Update `.superpowers/sdd/progress.md` with the actual cleanup results and verification evidence. diff --git a/docs/superpowers/plans/2026-08-04-em-observation-movementtest.md b/docs/superpowers/plans/2026-08-04-em-observation-movementtest.md deleted file mode 100644 index 4ea3973..0000000 --- a/docs/superpowers/plans/2026-08-04-em-observation-movementtest.md +++ /dev/null @@ -1,457 +0,0 @@ -# EM Observation MovementTest Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. - -**Goal:** Build a real-localization, observe-only MovementTest that creates a configurable start/goal map, plans Hybrid A* → Local G2 → EM trajectories, and shows world, LS, and ST diagnostics without sending a chassis command. - -**Architecture:** Put map construction, planning bootstrap, rolling EM requests, trajectory observation, and LS/ST derivation in pure, testable classes. Keep MDCS reads, prompts, painters, background timing, and cancellation in one thin MovementTest host. The host may only read DetourInterface and BasicPilotBase.Chassis; its only control output is a displayed TrajectoryControlCommand. - -**Tech Stack:** C# 10, netstandard2.0, existing Clumsy MovementTest/Painter UI, MDCS localization and chassis read APIs, Hybrid A*, Local G2, EM planner, OSQP, and EMPlannerVerificationHost. - -## Global Constraints - -- All map geometry and UI world coordinates are mm; Pose2D, velocities, and EM geometry are m, m/s, and rad. -- Bounds are exactly the start/goal axis-aligned rectangle expanded by MapPaddingMeters on all sides. Obstacles must fit these bounds; they must not enlarge them. -- Default settings are: padding 2.0 m, resolution 50 mm, replan 0.20 s, observer period 0.05 s. -- Capture world pose through DetourInterface.getCartLocation() and signed body-longitudinal velocity from BasicPilotBase.Chassis.GetCarSpeed(true).Vx. Create a monotonically increasing state sequence id. -- Output is TrajectoryControlCommand for display only. Do not invoke SendXYThSpeed, SendMotion, SendTh, AccumulateSpeed, ComputeWheelsGeometrically, brake/wheel adapter methods, or a geometric controller. -- Keep runtime source under ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest. Do not change the existing coarse-path factory, whose unrelated manual demo uses an 8 m expansion. -- Stop/cancel must cancel worker activity and clear the World, LS, and ST painter layers. - ---- - -## File structure - -| File | Responsibility | -| --- | --- | -| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs | Settings, manual-obstacle DTOs, validation, exact map-job construction. | -| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs | Hybrid A* + Local G2 bootstrap, rolling EM requests, time observation, LS/ST models. | -| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs | Three painter layers and presentation text; no MDCS/hardware use. | -| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs | Discoverable test, MDCS state reader, prompts, background session, console, cancellation. | -| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md | Operator configuration, layer interpretation, unit and safety guidance. | -| ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs | Deterministic regression checks and an actuator-call source audit. | -| ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs | Adds the trajectory-observation command. | - -### Task 1: Configuration and exact rectangle-map inputs - -**Files:** - -- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs -- Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs - -**Interfaces:** - -- Consumes: Pose2D, VehicleParameters, PlanningMapRequest, MapBoundsMm, ManualObstacleSource, CircleObstacle, AxisAlignedRectangleObstacle. -- Produces: TrajectoryObservationSettings.Validate(), TrajectoryObservationObstacle.Circle(double, double, double), TrajectoryObservationObstacle.Rectangle(double, double, double, double), and TrajectoryObservationSetupFactory.CreateBootstrapJob(Pose2D, Pose2D, TrajectoryObservationSettings, IReadOnlyList, long). - -- [ ] **Step 1: Write failing map-bounds and obstacle checks** - -Create the verification host class and invoke it with a new trajectory-observation argument: - -~~~csharp -internal static class TrajectoryObservationChecks -{ - public static void Run() - { - VerifiesStartGoalBoundsUseOnlyConfiguredPadding(); - RejectsObstacleOutsideConfiguredBounds(); - } - - private static void VerifiesStartGoalBoundsUseOnlyConfiguredPadding() - { - var settings = new TrajectoryObservationSettings - { - MapPaddingMeters = 2d, - MapResolutionMillimeters = 50f, - }; - CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - new Pose2D(10d, -5d, 0d), new Pose2D(13d, -1d, 0d), settings, - Array.Empty(), 17L); - - Verification.NearlyEqual(8000d, job.MapRequest.Bounds.XMin, "observer map x min"); - Verification.NearlyEqual(15000d, job.MapRequest.Bounds.XMax, "observer map x max"); - Verification.NearlyEqual(-7000d, job.MapRequest.Bounds.YMin, "observer map y min"); - Verification.NearlyEqual(1000d, job.MapRequest.Bounds.YMax, "observer map y max"); - Verification.NearlyEqual(50d, job.MapRequest.ResolutionMm, "observer map resolution"); - } -} -~~~ - -Modify Program.Main to accept trajectory-observation, call TrajectoryObservationChecks.Run(), then write PASS trajectory-observation. Add the same call to em-all. - -- [ ] **Step 2: Run the new check to prove it fails** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: compilation fails because TrajectoryObservationSettings and TrajectoryObservationSetupFactory do not exist. - -- [ ] **Step 3: Implement the contracts and factory** - -Create the editable configuration contract: - -~~~csharp -public sealed class TrajectoryObservationSettings -{ - public double MapPaddingMeters { get; set; } = 2d; - public float MapResolutionMillimeters { get; set; } = 50f; - public double ReplanPeriodSeconds { get; set; } = 0.20d; - public double ObserverPeriodSeconds { get; set; } = 0.05d; - public double VehicleLengthMeters { get; set; } = 0.80d; - public double VehicleWidthMeters { get; set; } = 0.60d; - public double SafetyMarginMeters { get; set; } = 0.05d; - public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d; - - public void Validate(); - public VehicleParameters CreateVehicle(); -} -~~~ - -Implement finite/positive validation. Implement the obstacle as world-mm circle or axis-aligned rectangle with GetBounds() and ToMapObstacle(). Build bounds with the following exact calculation, rounded outward to the configured grid: - -~~~csharp -double padMm = settings.MapPaddingMeters * 1000d; -var bounds = new MapBoundsMm( - ToGridLower(Math.Min(start.X, goal.X) * 1000d - padMm, settings.MapResolutionMillimeters), - ToGridUpper(Math.Max(start.X, goal.X) * 1000d + padMm, settings.MapResolutionMillimeters), - ToGridLower(Math.Min(start.Y, goal.Y) * 1000d - padMm, settings.MapResolutionMillimeters), - ToGridUpper(Math.Max(start.Y, goal.Y) * 1000d + padMm, settings.MapResolutionMillimeters)); -~~~ - -Reject an obstacle unless its full envelope is contained in bounds. With zero obstacles set AllowExplicitEmptyMap true. Otherwise construct exactly one required ManualObstacleSource named trajectory-observer-manual with the supplied positive snapshot version. Return a CoarsePathPlanningJob with new HybridAStarConfiguration, StartDirection = null, and GoalDirection = GoalDirectionConstraint.Any. - -- [ ] **Step 4: Run focused and existing checks** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -~~~ - -Expected: both exit 0 and print PASS trajectory-observation and PASS foundation. - -- [ ] **Step 5: Commit the input layer** - -~~~powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs -git commit -m "feat: add observation test map inputs" -~~~ - -### Task 2: Pure planning bootstrap, time observation, and LS/ST derivation - -**Files:** - -- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs - -**Interfaces:** - -- Consumes: CoarsePathPlanningService, PathSmoothingService, EmPlanningCoordinator, TrajectoryExecutor, FrenetProjector, and caller-supplied VehicleMotionState. -- Produces: TrajectoryObservationBootstrapper.Bootstrap(CoarsePathPlanningJob, CancellationToken), TrajectoryObservationController.StartCycle(DateTimeOffset, VehicleMotionState, CancellationToken), TrajectoryObservationController.Observe(DateTimeOffset, VehicleMotionState), and TrajectoryObservationCharts.Build(EmTrajectory, DirectionSegmentView, double). - -- [ ] **Step 1: Add failing chart and time-sampling checks** - -Extend TrajectoryObservationChecks.Run() by adding VerifiesLsAndStUsePublishedTrajectoryData(). Use a fixed two-point EmTrajectory whose EffectiveAtUtc is 2026-08-04T00:00:00Z, with TimeFromStart values 0 and 1, PathS values 4 and 5, and known signed speeds. Assert that Build returns two ST samples (0,4) and (1,5), two speed samples, and the expected LS projection count. Call Observe at 00:00:00.500Z and assert that TrajectoryExecutor selected an interpolated point with TimeFromStart == 0.5d. - -- [ ] **Step 2: Run the new check to prove it fails** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: compilation fails because TrajectoryObservationCharts and TrajectoryObservationController do not exist. - -- [ ] **Step 3: Implement the pipeline** - -Bootstrap must use exactly this success gate: - -~~~csharp -CoarsePathPlanningJobResult coarse = coarseService.Plan(job, cancellationToken); -if (coarse.PlanningResult.Status != PlanningStatus.Success) - return TrajectoryObservationBootstrapResult.FromFailure( - job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status); - -var smoothingRequest = new PathSmoothingRequest( - CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map), - coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle, - new PathSmoothingConfiguration()); -PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, cancellationToken); -if (!IsPublishedSmoothingStatus(smooth.Status)) - return TrajectoryObservationBootstrapResult.FromFailure( - job, coarse, smooth, smooth.Diagnostics.TerminationReason); -return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth)); -~~~ - -IsPublishedSmoothingStatus accepts only Complete, PartialImprovement, NotNeeded, and Unchanged. CopyFiniteClearance replaces a positive-infinite clearance with the finite map diagonal before copying each CoarsePathPoint. - -TrajectoryObservationController owns EmPlanningCoordinator and TrajectoryExecutor. For a replan it creates: - -~~~csharp -var request = new EmPlanningRequest( - bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Job.Vehicle, state, configuration, - segmentIndex, coordinator.PublishedTrajectory, now, now, - sessionId + "-trajectory-" + cycleId, sessionId + "-reference", - coordinator.PublishedTrajectory?.Metadata.TrajectoryId ?? string.Empty, - EmMotionModel.NonholonomicForwardReverse); -return coordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken); -~~~ - -Set configuration.Scheduling.ReplanPeriodSeconds from settings. Initial observation mode always uses segmentIndex 0. Observe must use PublishedTrajectory only; when non-null call UpdateCommand(now, state, trajectory, trajectory.Metadata.Direction, trajectory.Metadata.Direction, true) and return the selected point, command, and executor state for display only. - -Build LS/ST from published data alone: - -~~~csharp -ls.Add(new TrajectoryObservationLsSample( - segment.SourceStartArcLength + projection.ReferenceS, projection.LateralOffset)); -st.Add(new TrajectoryObservationStSample(point.TimeFromStart, point.PathS)); -speed.Add(new TrajectoryObservationSpeedSample(point.TimeFromStart, point.SignedLongitudinalVelocity)); -~~~ - -Use seeded FrenetProjector calls and count failed projections. No pipeline class may reference UI, DetourInterface, BasicPilotBase, or a hardware class. - -- [ ] **Step 4: Run diagnostics and regression checks** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -~~~ - -Expected: every command exits 0. - -- [ ] **Step 5: Commit the pure pipeline** - -~~~powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: add EM observation planning pipeline" -~~~ - -### Task 3: Presentation layers and observation text - -**Files:** - -- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs - -**Interfaces:** - -- Consumes: bootstrap result, observation result, and chart data. -- Produces: TrajectoryObservationPresentation.DrawWorld(TrajectoryObservationBootstrapResult, TrajectoryObservationObservation), DrawLs(TrajectoryObservationCharts), DrawSt(TrajectoryObservationCharts), ClearAll(), and TrajectoryObservationPresentationText.Create(TrajectoryObservationObservation, TrajectoryObservationCharts). - -- [ ] **Step 1: Add a failing presentation-text check** - -Assert that TrajectoryObservationPresentationText.Create(observation, charts) contains the literal OBSERVE_ONLY: no chassis command is sent., selected point time/path-S, signed speed, yaw rate, and LS projection failure count. The check must not instantiate a Painter. - -- [ ] **Step 2: Run the check to verify it fails** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: compilation fails because TrajectoryObservationPresentationText does not exist. - -- [ ] **Step 3: Implement the three painters** - -Create exactly these named layers: - -~~~csharp -worldPainter = UI.GetPainter("TrajectoryObserver.World", true); -lsPainter = UI.GetPainter("TrajectoryObserver.LS", true); -stPainter = UI.GetPainter("TrajectoryObserver.ST", true); -~~~ - -DrawWorld clears only worldPainter then draws map bounds/grid/occupied cells, start, goal, coarse path, Local G2 path, real pose, and latest EM path. Convert every planner position from m to mm before calling DrawLine, DrawCircle, or DrawText. - -DrawLs draws axes plus s-l samples. DrawSt draws t-s and a vertically separated t-v series with a legend. A missing trajectory draws a status string instead of throwing. ClearAll invokes Clear on all three painters and performs no other action. - -- [ ] **Step 4: Run visual-model and compile verification** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -~~~ - -Expected: trajectory-observation passes and the project has zero compile errors. - -- [ ] **Step 5: Commit presentation** - -~~~powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: visualize EM observation diagnostics" -~~~ - -### Task 4: MDCS read-only MovementTest host - -**Files:** - -- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs -- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs - -**Interfaces:** - -- Consumes: DetourInterface.getCartLocation(), BasicPilotBase.Chassis.GetCarSpeed(true), setup/controller/presentation APIs. -- Produces: a [MovementTest(name = "EM轨迹规划观察闭环测试")] entry with Test() and TestStop(). - -- [ ] **Step 1: Write a failing actuator-free source audit** - -Add VerifiesObservationSourceHasNoActuatorCalls() to TrajectoryObservationChecks.Run() and implement it in the verification host. It reads the observation runtime source files and fails on any of these tokens: - -~~~csharp -new[] -{ - ".SendXYThSpeed(", ".SendMotion(", ".SendTh(", ".AccumulateSpeed(", - ".ComputeWheelsGeometrically(", ".DriveStop(", ".PredefinedDriveStop(" -} -~~~ - -The audit strings live only in the test host; none may appear in the new runtime observation files. - -- [ ] **Step 2: Run the audit before the host exists** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: the check fails because MovementTest.TrajectoryObservationTest.cs is missing. - -- [ ] **Step 3: Implement the host and lifecycle** - -Use this discoverable configuration: - -~~~csharp -[MovementTest(name = "EM轨迹规划观察闭环测试")] -public sealed class TrajectoryObservationMovementTest : MovementTest -{ - public double GoalXmm = double.NaN; - public double GoalYmm = double.NaN; - public double GoalYawDeg = 0d; - public double MapPaddingMeters = 2d; - public float MapResolutionMm = 50f; - public double ReplanPeriodSeconds = 0.20d; - public double ObserverPeriodSeconds = 0.05d; - - public override void Test(); - public override void TestStop(); -} -~~~ - -When GoalXmm or GoalYmm is non-finite, prompt for all goal values with the same finite parser/UI.GetInput pattern as CoarsePathPlanningTest. Prompt for 0–20 manual obstacles (circle or rectangle) and freeze all inputs before Task.Run begins. - -The MDCS reader must use only this read path: - -~~~csharp -var location = DetourInterface.getCartLocation(); -if (location == null) throw new InvalidOperationException("Live localization is unavailable."); -if (BasicPilotBase.Chassis == null) throw new InvalidOperationException("Live chassis read interface is unavailable."); -var speed = BasicPilotBase.Chassis.GetCarSpeed(true); -return new VehicleMotionState( - new Pose2D(location.x / 1000d, location.y / 1000d, location.th * Math.PI / 180d), - speed.Vx, null, DateTimeOffset.UtcNow, Interlocked.Increment(ref stateSequence)); -~~~ - -Bootstrap once in a cancellable Task.Run. After success, run Task.Delay(TimeSpan.FromSeconds(ObserverPeriodSeconds), token) between ticks. At each tick capture exactly one state, start a cycle only when controller.ShouldStartCycle(now), observe the latest published trajectory, draw all layers, and print a throttled status. Construct the service as new EmPlanningService(new OsqpNativeSolver()). - -Every status includes: - -~~~text -OBSERVE_ONLY: no chassis command is sent. -~~~ - -When a GearSwitch trajectory reaches its final time, draw and print 等待真实档位/方向确认;观察模式不会推进下一方向段, leave segment index 0, and do not create a direction-change action. Goal and rolling-stop commands may only be logged. - -Use a lock/session id pattern matching CoarsePathPlanningTestRunner: replace the active CancellationTokenSource, cancel the old source without waiting, and allow only the current session to draw or log. TestStop cancels, disposes after task completion, and calls presentation.ClearAll. - -Write README.md with configuration fields/units, obstacle examples, default values, chart interpretations, the VelocityX/VelocityY world-frame warning, and the explicit no-driving limitation. - -- [ ] **Step 4: Verify runner safety and integration build** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -rg -n 'SendXYThSpeed\(|SendMotion\(|SendTh\(|AccumulateSpeed\(|ComputeWheelsGeometrically\(|DriveStop\(|PredefinedDriveStop\(' ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -g '*.cs' -~~~ - -Expected: host and build exit 0. The rg command exits 1 because no runtime observation file calls a forbidden actuator method. - -- [ ] **Step 5: Commit the MovementTest** - -~~~powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: add read-only EM observation movement test" -~~~ - -### Task 5: End-to-end regression and operator handoff - -**Files:** - -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md - -**Interfaces:** - -- Consumes: completed observation-test components and existing EMPlannerVerificationHost checks. -- Produces: a reproducible all-up verification command and a launch/stop checklist. - -- [ ] **Step 1: Add a failing bootstrap regression** - -Use an empty-map setup with start (0.5, 0.5, 0) m and goal (3.5, 0.5, 0) m. Assert that bootstrap returns a successful map, PlanningStatus.Success, a publishable smoothing result, and at least one DirectionSegmentView. This check does not run native OSQP. - -- [ ] **Step 2: Run the check to confirm its failure** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: the assertion identifies a missing or incorrect bootstrap result. - -- [ ] **Step 3: Make the smallest corrective change** - -Correct only TrajectoryObservationSetupFactory or TrajectoryObservationBootstrapper so the empty-map request produces a planning-ready map and publishable Local G2 path. Preserve the exact bounds rule and do not add UI, MDCS, or hardware dependencies to pure classes. - -- [ ] **Step 4: Run all required evidence checks** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -git diff --check -~~~ - -Expected: every command exits 0. In the vehicle UI, the entry appears as EM轨迹规划观察闭环测试 and starting/running/stopping it does not issue any chassis, motor, steering, or brake output. - -- [ ] **Step 5: Commit final verification/documentation** - -~~~powershell -git add -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md -git commit -m "test: verify EM observation movement test" -~~~ - -## Plan self-review - -**Spec coverage:** Task 1 provides configurable start/goal map bounds, vehicle settings, and manual obstacles. Task 2 covers Hybrid A*, Local G2, rolling EM, time sampling, and derived LS/ST. Task 3 creates World/LS/ST painters. Task 4 reads live MDCS state, prints observation diagnostics, handles gear-switch observation, and ensures cancellation/no-write behavior. Task 5 supplies an end-to-end fixture and final evidence. - -**Placeholder scan:** Every task names concrete files, commands, interface names, inputs, expected behavior, and commit content; no deferred implementation markers remain. - -**Type consistency:** Map code produces PlanningMapRequest and CoarsePathPlanningJob; bootstrap produces PathSmoothingResult and DirectionSegmentView; rolling planning consumes VehicleMotionState and EmPlanningRequest; UI consumes EmTrajectory, TrajectoryControlCommand, and chart samples without changing EM contracts. diff --git a/docs/superpowers/plans/2026-08-04-em-planner-readme-implementation.md b/docs/superpowers/plans/2026-08-04-em-planner-readme-implementation.md deleted file mode 100644 index cbf5d4c..0000000 --- a/docs/superpowers/plans/2026-08-04-em-planner-readme-implementation.md +++ /dev/null @@ -1,262 +0,0 @@ -# EM Planner 与 TrajectoryExecution README 实施计划 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 以 `CoarsePath/README.md` 的结构重写 EMPlanner 文档,并为 TrajectoryExecution 新增准确、可操作的模块 README。 - -**Architecture:** README 只描述现有的不可变请求、纯单次规划、滚动协调、轨迹执行和通用控制命令边界。两份文档均从上游空间路径到未来硬件适配器的单向数据流解释职责;不改变任何 C#、测试、项目文件或发布脚本。 - -**Tech Stack:** Markdown、PowerShell、Git、现有 `EMPlannerVerificationHost` 命令。 - -## Global Constraints - -- 仅修改 `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` 和新建的 `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md`。 -- 保留所有既有用户工作区改动;只显式暂存本任务指定的 README,绝不使用 `git add .` 或 `git add -A`。 -- 从仓库根目录运行所有检查;不改动 LS/ST/OSQP、测试夹具、发布脚本、`csproj`、UI 或硬件代码。 -- `EmPlanningService.Plan(EmPlanningRequest, CancellationToken)` 是纯、同步、一次性的规划入口。 -- 只有 `Success` 和 `SuccessWithFallback` 可携带可消费的 `EmTrajectory`;其余状态不可发布部分轨迹。 -- 动态障碍物、时空占用、行为决策、UI/硬件集成、横移、蟹行和原地旋转均明确为未实现范围。 -- 验证命令只引用当前的 `em-core-all`、`coordinator`、`executor`、`plugin-package` 和 `em-all`。 - ---- - -### Task 1: 重写 EMPlanner 模块 README - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` - -**Interfaces:** -- Consumes: `PathSmoothingResult`、`PlanningGridMap`、`VehicleParameters`、`VehicleMotionState`、`EmPlannerConfiguration` 和可选上一条 `EmTrajectory` 快照。 -- Produces: 文档化的 `IEmPlanningService.Plan(EmPlanningRequest request, CancellationToken cancellationToken)` 消费边界,以及仅在 `Success`/`SuccessWithFallback` 时可消费的 `EmTrajectory`。 - -- [ ] **Step 1: 记录旧文档的 RED 基线** - -从仓库根目录运行: - -```powershell -$text = Get-Content ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md -Raw -Encoding UTF8 -if ($text -notmatch '尚未实现 LS 横向优化、\s*ST 纵向优化、滚动协调、轨迹发布') { - throw 'Expected the stale Foundation-only scope marker.' -} -``` - -Expected: 找到一条 Foundation 阶段的旧描述,证明重写前 README 与当前实现不一致。 - -- [ ] **Step 2: 以 CoarsePath 风格重写 README** - -使用 `apply_patch` 以 UTF-8 Markdown 整体替换文件。文档必须按以下顺序覆盖: - -```text -模块说明(职责/非职责与唯一入口) -文件结构(Configuration、Contracts、Segmentation、Frenet、Corridor、Lateral、Longitudinal、Trajectory、Validation、Facade) -规划数据流(校验 -> 方向段 -> 走廊 -> LS -> ST -> 装配 -> 独立验证) -结果、状态与停止 -坐标、单位与前进/倒车符号 -最小调用示例 -详细使用指南 -验证命令 -常见错误 -第一版限制 -``` - -正文必须声明静态走廊、OSQP 通过 solver-neutral `IQpSolver`/`QuadraticProgram`、实际 `PathS`、终端零速安全尾段、世界空间独立复核和纯服务边界。示例只构造 `EmPlanningService` 和调用 `Plan`,不得包含 UI 或硬件对象。模块概览和首版限制都必须以相对链接指向 `../TrajectoryExecution/README.md`,说明滚动执行不属于纯规划服务。 - -- [ ] **Step 3: 运行 GREEN 文档契约检查** - -从仓库根目录运行: - -```powershell -$path = 'ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md' -$text = Get-Content $path -Raw -Encoding UTF8 -$required = @( - '## 模块说明(Module Overview)', - '## 文件结构(File Structure)', - '## 规划数据流(Planning Data Flow)', - 'EmPlanningService.Plan', - 'SuccessWithFallback', - 'em-core-all', - 'em-all', - '动态障碍物' -) -foreach ($item in $required) { - if (-not $text.Contains($item)) { throw "Missing README contract: $item" } -} -if ($text -match '尚未实现 LS 横向优化、\s*ST 纵向优化、滚动协调、轨迹发布') { - throw 'Stale Foundation-only scope remains.' -} -``` - -Expected: exit 0 without `Missing README contract` or stale-scope error. - -- [ ] **Step 4: 检查差异并提交 Task 1** - -```powershell -git diff --check -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md -git diff --cached --check -git diff --cached --name-only -git commit -m "docs: rewrite EM Planner readme" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: staged 和提交范围仅为 EMPlanner README;所有 diff 检查退出 0。 - -### Task 2: 新增 TrajectoryExecution 模块 README - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md` - -**Interfaces:** -- Consumes: `IEmPlanningService`、调用方冻结的 `PlanningCycleInput`、当前 `VehicleMotionState` 与已发布不可变 `EmTrajectory`。 -- Produces: `EmPlanningCoordinator.PlanLatestAsync` 的当前版本发布语义,以及 `TrajectoryExecutor.UpdateCommand` 返回的 `TrajectoryControlCommand`。 - -- [ ] **Step 1: 记录 README 缺失的 RED 基线** - -从仓库根目录运行: - -```powershell -if (Test-Path ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md) { - throw 'TrajectoryExecution README unexpectedly already exists.' -} -throw 'RED: TrajectoryExecution README is absent.' -``` - -Expected: command exits nonzero and prints the RED message. - -- [ ] **Step 2: 新增 CoarsePath 风格 README** - -使用 `apply_patch` 创建 UTF-8 Markdown。按以下顺序说明: - -```text -模块说明(协调、交接、执行与控制适配职责) -文件结构(所有 .cs 文件) -滚动执行数据流 -周期身份、latest-wins 与安全交接 -换向状态机和停止语义 -通用控制命令的字段、单位及硬件边界 -最小调用示例 -详细使用指南 -验证命令与插件部署链接 -常见错误 -第一版限制 -``` - -文件结构必须逐项列出当前文件,且只赋予现有职责: - -```text -TrajectoryExecution/ -├── EmPlanningCoordinator.cs -├── GearSwitchState.cs -├── GearSwitchStateMachine.cs -├── IEmPlanningCycleSink.cs -├── IVehicleStateProvider.cs -├── PlanningCycleIdentity.cs -├── PlanningCycleInput.cs -├── PlanningCycleResult.cs -├── TrajectoryControlAdapter.cs -├── TrajectoryControlCommand.cs -├── TrajectoryExecutionState.cs -├── TrajectoryExecutor.cs -├── TrajectoryHandoffSelector.cs -└── TrajectorySampler.cs -``` - -文档必须给出状态序列 `Following -> ApproachingGearSwitch -> HoldingZero -> RequestingDirectionChange -> AwaitingDirectionConfirmation -> Following`,说明失败重规划不覆盖上一条完整轨迹、轨迹末点不外推,以及 `TrajectoryControlCommand` 没有横向车体速度或原地旋转字段。插件章节必须指向 EMPlanner README 的 Windows x64 打包说明,不能重写发布脚本的细节。 - -- [ ] **Step 3: 运行 GREEN 文档契约检查** - -从仓库根目录运行: - -```powershell -$path = 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md' -$text = Get-Content $path -Raw -Encoding UTF8 -$required = @( - '## 模块说明(Module Overview)', - '## 文件结构(File Structure)', - '## 滚动执行数据流(Rolling Execution Data Flow)', - 'EmPlanningCoordinator.PlanLatestAsync', - 'TrajectoryExecutor.UpdateCommand', - 'Following -> ApproachingGearSwitch -> HoldingZero', - 'TrajectoryControlCommand', - '../EMPlanner/README.md', - 'em-all', - '动态障碍物' -) -foreach ($item in $required) { - if (-not $text.Contains($item)) { throw "Missing README contract: $item" } -} -``` - -Expected: exit 0 without `Missing README contract`. - -- [ ] **Step 4: 执行文档关联与回归验证** - -从仓库根目录运行: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package -git diff --check -``` - -Expected: 四个验证组均退出 0 并输出对应 `PASS` 行;`git diff --check` 退出 0。 - -- [ ] **Step 5: 检查差异并提交 Task 2** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md -git diff --cached --check -git diff --cached --name-only -git commit -m "docs: add trajectory execution readme" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: staged 和提交范围仅为 TrajectoryExecution README;所有 diff 检查退出 0。 - -### Task 3: 最终 README 交叉核查 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`(仅在发现失效相对链接或错误命令时修正) -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md`(仅在发现失效相对链接或错误命令时修正) - -**Interfaces:** -- Consumes: Task 1 的单次规划边界和 Task 2 的执行边界。 -- Produces: 两份边界互不矛盾、命令与仓库现状一致的交接文档。 - -- [ ] **Step 1: 运行最终文本与相对链接检查** - -```powershell -$em = Get-Content ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md -Raw -Encoding UTF8 -$execution = Get-Content ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md -Raw -Encoding UTF8 -if ($em -match '尚未实现 LS 横向优化') { throw 'EMPlanner retains stale completion state.' } -if ($em -notmatch 'TrajectoryExecution') { throw 'EMPlanner does not describe its execution boundary.' } -if ($execution -notmatch '../EMPlanner/README.md') { throw 'Execution README lacks EMPlanner link.' } -if ($execution -match 'MultiVehicleScriptVx|MultiVehicleScriptVy|MultiVehicleScriptVth') { - throw 'Execution README leaks a prohibited hardware/UI field.' -} -``` - -Expected: exit 0. - -- [ ] **Step 2: 运行最终门禁并检查工作区范围** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -git diff --check -git status --short -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md -``` - -Expected: `em-all` exit 0;无 whitespace 错误;两个 README 均已提交,因此范围状态为空。 - -- [ ] **Step 3: 如步骤 1 或 2 要求更正,显式暂存两份 README 并提交** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md -git diff --cached --check -git commit -m "docs: verify EM Planner readme links" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: 仅当发生文字修正时才创建提交;否则不执行此步骤。 diff --git a/docs/superpowers/plans/2026-08-04-planner-comment-overhaul-implementation.md b/docs/superpowers/plans/2026-08-04-planner-comment-overhaul-implementation.md deleted file mode 100644 index 397dde9..0000000 --- a/docs/superpowers/plans/2026-08-04-planner-comment-overhaul-implementation.md +++ /dev/null @@ -1,295 +0,0 @@ -# PathSmoothing 与 TrajectoryExecution 注释改造实施计划 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 为 PathSmoothing 生产规划核心与 TrajectoryExecution 全部生产代码补齐 CoarsePath 风格的中文 XML 文档注释。 - -**Architecture:** 只在现有声明之前增加或替换 `///` 文档注释,不改变任何命名、签名、数值、控制流或项目文件。公开 API 使用完整的职责、参数、返回值和失败语义;关键内部算法边界补充单位、数据所有权和不变量;私有小工具、测试、报告及可视化模块保持不动。 - -**Tech Stack:** C# XML documentation comments、PowerShell、现有 PathSmoothing 验证脚本、`EMPlannerVerificationHost`。 - -## Global Constraints - -- 只修改注释;禁止修改任何可执行 C# 语句、签名、命名空间、`using`、测试、`csproj`、UI、硬件或报告/可视化模块。 -- 注释使用中文,并保留 C# 类型名;每个数值参数或属性都要标明现有单位(m、rad、`1/m`、m/s、m/s²、m/s³、s)或明确其为 ID、索引、布尔值、枚举、快照或集合。 -- 每个公开构造函数和公开方法必须有 ``;有参数时逐项使用 ``;非 `void` 方法使用 ``;`Try...` 方法明确 `true` / `false` 和每个 `out` 参数。 -- 关键内部跨目录算法入口也使用相同规范;不为私有单行数学工具、测试、比较、报告或可视化代码增加注释。 -- 路径、状态和结果的注释必须说明不可变性、集合顺序、失败时的空结果或 `false` 语义;不得宣称代码当前未实现的动态障碍物、UI、硬件、横移或原地旋转能力。 -- 每个任务只能显式暂存该任务列出的文件;禁止 `git add .`、`git add -A`、清理命令或破坏性 Git 命令。 - ---- - -### Task 1: PathSmoothing 契约与公开门面注释 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs` - -**Interfaces:** -- Consumes: `PlanningResult`、`PlanningGridMap`、`VehicleParameters`、不可变 `PathSmoothingRequest` 与 `PathSmoothingConfiguration`。 -- Produces: 只有成功状态才可消费的 `PathSmoothingResult`、严格递增弧长的 `SmoothedPathPoint` 序列,以及包含区域失败和性能信息的 `PathSmoothingDiagnostics`。 - -- [ ] **Step 1: 记录缺失参数/返回值注释的 RED 基线** - -```powershell -$path = 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs' -$text = Get-Content $path -Raw -Encoding UTF8 -if ($text -notmatch 'public PathSmoothingResult Smooth\(') { throw 'Expected public Smooth entry point.' } -if ($text -match '') { throw 'RED baseline unexpectedly already documents request.' } -throw 'RED: PathSmoothingService.Smooth lacks parameter documentation.' -``` - -Expected: command exits nonzero with the RED message. - -- [ ] **Step 2: 为契约类型、构造函数、属性和门面补齐 XML 文档** - -使用 `apply_patch` 在每个公开类型前写职责和边界;在构造函数前写参数类型意义、单位、可空性、集合顺序和防御性复制语义;在关键属性前写数据含义和单位。为 `PathSmoothingService.Smooth`、比较服务入口和结果工厂写完整 `` / ``。 - -关键语义必须在注释中准确出现:`PathSmoothingRequest` 持有输入副本;`PathSmoothingResult` 仅在成功状态发布路径;`SmoothedPathPoint` 的位置为 m、航向 rad、曲率 `1/m`、弧长 m;失败结果不包含部分可消费路径。 - -- [ ] **Step 3: 运行 GREEN XML 契约检查** - -```powershell -$paths = @( - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs', - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs', - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs', - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs' -) -$text = ($paths | ForEach-Object { Get-Content $_ -Raw -Encoding UTF8 }) -join "`n" -foreach ($marker in @('', '', '', 'm', 'rad', '1/m', '不可变')) { - if (-not $text.Contains($marker)) { throw "Missing XML documentation marker: $marker" } -} -if ($text -notmatch '失败.*部分|部分.*路径') { throw 'Missing partial-result failure semantics.' } -Write-Output 'PASS PathSmoothing contract XML documentation' -``` - -Expected: exit 0 and the PASS line. - -- [ ] **Step 4: 检查纯注释差异并提交 Task 1** - -```powershell -git diff --check -git diff -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs -git diff --cached --check -git commit -m "docs: document path smoothing contracts" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: cached diff and commit contain only the listed files, with no non-comment code changes. - -### Task 2: PathSmoothing 处理、校验与 LocalG2 阶段注释 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/CurvatureLimitPolicy.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs` - -**Interfaces:** -- Consumes: 已准备的方向段、车辆约束、地图快照、局部 G2 配置和候选几何。 -- Produces: 连续且严格递增弧长的平滑段、候选评估/拒绝原因、验证结果和不可变区域报告。 - -- [ ] **Step 1: 记录关键 `Try...` 入口缺失返回语义的 RED 基线** - -```powershell -$path = 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs' -$text = Get-Content $path -Raw -Encoding UTF8 -if ($text -notmatch 'TryPrepare\(') { throw 'Expected TryPrepare entry point.' } -if ($text -match '.*true.*false') { throw 'RED baseline unexpectedly documents TryPrepare outcomes.' } -throw 'RED: TryPrepare lacks true/false and out-result documentation.' -``` - -Expected: command exits nonzero with the RED message. - -- [ ] **Step 2: 补齐处理/验证/LocalG2 的阶段边界注释** - -使用 `apply_patch` 为公开类型与关键内部入口增加 ``、``、``。所有 `Try...` 方法必须说明成功/失败、`out` 值和原因字符串;几何方法必须说明世界坐标 m、航向 rad、曲率 `1/m`、弧长 m 和严格递增约束;校验器必须说明碰撞、曲率、换向锚点和无部分发布规则。 - -LocalG2 注释必须说明候选构建、预平滑、窗口选择、拼接和评估的输入/输出关系,不得将内部启发式误描述为动态障碍或控制功能。 - -- [ ] **Step 3: 运行 GREEN 内部算法注释检查** - -```powershell -$paths = @( - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs', - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs', - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs', - 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs' -) -$text = ($paths | ForEach-Object { Get-Content $_ -Raw -Encoding UTF8 }) -join "`n" -foreach ($marker in @('', '', 'true', 'false', 'm', 'rad', '1/m')) { - if (-not $text.Contains($marker)) { throw "Missing algorithm XML marker: $marker" } -} -if ($text -notmatch '严格递增|不发布部分') { throw 'Missing geometric or publication invariant.' } -Write-Output 'PASS PathSmoothing algorithm XML documentation' -``` - -Expected: exit 0 and the PASS line. - -- [ ] **Step 4: 运行既有 PathSmoothing 验证并提交 Task 2** - -```powershell -& .\ClumsyPilot\tests\verify_path_smoothing_geometry.ps1 -& .\ClumsyPilot\tests\verify_path_smoothing_validation.ps1 -& .\ClumsyPilot\tests\verify_path_smoothing_local_g2_integration.ps1 -git diff --check -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/CurvatureLimitPolicy.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs -git diff --cached --check -git commit -m "docs: document path smoothing pipeline" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: three existing validation scripts exit 0; cached diff and commit contain only listed production files and comment-only changes. - -### Task 3: TrajectoryExecution 全部生产边界注释 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchState.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchStateMachine.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/IEmPlanningCycleSink.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/IVehicleStateProvider.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleIdentity.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleInput.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlAdapter.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlCommand.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutionState.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutor.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryHandoffSelector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectorySampler.cs` - -**Interfaces:** -- Consumes: `IEmPlanningService`、调用方时钟与 `VehicleMotionState` 快照、已验证的不可变 `EmTrajectory`、方向确认状态和 `EmPlannerConfiguration`。 -- Produces: latest-wins `PlanningCycleResult`、安全交接选择、不可变 `TrajectoryExecutionState` 和控制器中立的 `TrajectoryControlCommand`。 - -- [ ] **Step 1: 记录协调器方法缺失完整契约的 RED 基线** - -```powershell -$path = 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs' -$text = Get-Content $path -Raw -Encoding UTF8 -if ($text -notmatch 'PlanLatestAsync\(') { throw 'Expected PlanLatestAsync entry point.' } -if ($text -match '') { throw 'RED baseline unexpectedly already documents rolling input.' } -throw 'RED: PlanLatestAsync lacks caller-clocked input and cancellation documentation.' -``` - -Expected: command exits nonzero with the RED message. - -- [ ] **Step 2: 补齐执行、交接与控制契约注释** - -使用 `apply_patch` 为目录内所有公开类型、枚举、构造函数、属性和公开方法补齐 XML 文档;为 `CompleteCycle`、`SelectPoint`、交接/采样和换向状态机等跨职责内部入口补齐语义。 - -`EmPlanningCoordinator.PlanLatestAsync` 必须解释 caller-clocked `now`、取消旧周期、版本/身份检查和只发布完整成功轨迹。交接注释必须列出同段同方向、年龄、追踪、边界和末点拒绝。`UpdateCommand` 与 `TrajectoryControlCommand` 必须写明 m/s、rad/s、零速/制动、一次换向请求、无横向速度和无原地旋转。`TrySample` 必须明确区间外或跨边界时返回 `false`。 - -- [ ] **Step 3: 运行 GREEN 执行层 XML 契约检查** - -```powershell -$paths = @( - 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs', - 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryHandoffSelector.cs', - 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutor.cs', - 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchStateMachine.cs', - 'ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlCommand.cs' -) -$text = ($paths | ForEach-Object { Get-Content $_ -Raw -Encoding UTF8 }) -join "`n" -foreach ($marker in @('', '', '', 'm/s', 'rad/s', 'latest-wins', '零速', 'false')) { - if (-not $text.Contains($marker)) { throw "Missing execution XML marker: $marker" } -} -if ($text -match 'MultiVehicleScriptVx|MultiVehicleScriptVy|MultiVehicleScriptVth') { - throw 'Documentation must not leak UI/hardware command fields.' -} -Write-Output 'PASS TrajectoryExecution XML documentation' -``` - -Expected: exit 0 and the PASS line. - -- [ ] **Step 4: 运行执行层回归并提交 Task 3** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -git diff --check -git add -- ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/EmPlanningCoordinator.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchState.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/GearSwitchStateMachine.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/IEmPlanningCycleSink.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/IVehicleStateProvider.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleIdentity.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleInput.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/PlanningCycleResult.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlAdapter.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryControlCommand.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutionState.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryExecutor.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectoryHandoffSelector.cs ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/TrajectorySampler.cs -git diff --cached --check -git commit -m "docs: document trajectory execution contracts" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: both execution gates exit 0; cached diff and commit contain only listed files and comment-only changes. - -### Task 4: 最终注释审查与完整回归 - -**Files:** -- Modify: Task 1 至 Task 3 的文件(仅在发现缺失、错误单位或文档与实际行为矛盾时修正)。 - -**Interfaces:** -- Consumes: CoarsePath 风格的 XML 文档约定、PathSmoothing 的纯规划边界和 TrajectoryExecution 的 caller-clocked 执行边界。 -- Produces: 与实际代码一致、可由 IDE XML 文档显示的中文 API 说明。 - -- [ ] **Step 1: 检查差异仅包含注释** - -```powershell -$diff = git diff 297186a..HEAD -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution -$codeLines = $diff | Where-Object { $_ -match '^[+-](?![+-/\s])' } -if ($codeLines.Count -gt 0) { $codeLines; throw 'Non-comment C# changes detected.' } -Write-Output 'PASS comment-only diff review' -``` - -Expected: exit 0 and the PASS line. Ignore `+++` / `---` headers and `///` documentation lines. - -- [ ] **Step 2: 运行最终构建与门禁** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -git diff --check -git status --short -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution -``` - -Expected: build 和 `em-all` 退出 0;无 whitespace 错误;两个目标目录没有未提交状态。 - -- [ ] **Step 3: 仅在最终审查发现需修正时,显式暂存和提交** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2 ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution -git diff --cached --check -git commit -m "docs: verify planner XML comments" -git diff-tree --no-commit-id --name-only -r HEAD -``` - -Expected: 仅在文字修正发生时创建提交;否则不执行此步骤。 diff --git a/docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md b/docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md deleted file mode 100644 index 0f044ed..0000000 --- a/docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md +++ /dev/null @@ -1,213 +0,0 @@ -# EM 纵向滚动规划四阶段执行路线图 - -日期:2026-08-05 - -## 目标 - -把原有的 10 个实现任务拆成 4 个可以在不同 Codex 窗口中依次完成的阶段。每个阶段必须独立验证、记录实际结果,并根据真实代码状态生成下一阶段的精细提示词。 - -本路线图只负责阶段划分和跨窗口交接。技术设计与逐步实现细节仍以下列文件为准: - -- 设计规格:`docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md` -- 十任务实施计划:`docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md` - -如三份文档发生冲突,设计规格决定行为语义,十任务实施计划决定具体实现步骤,本路线图决定阶段边界和停止位置。 - -## 不可破坏的全局约束 - -- `DistanceHorizonMeters` 是 LS 空间前视范围,不是车辆在一个 ST 周期内必须走完的距离。 -- `TimeHorizonSeconds` 是单次 ST 优化覆盖的未来时间,不是观察循环周期。 -- 只有真实 `Goal` 或 `GearSwitchApproach` 边界允许触发精确停车。 -- `RollingContinuation` 和 `ApproachStopBoundary` 不得被强制设置末速度为零。 -- `ExactStopAtBoundary` 必须在 QP 时间范围结束前达到 `S=S_end、U=0、A=0`,并在 QP 内至少保留一个完整 `OutputTimeStepSeconds` 的静止稳定区间。 -- 不通过放宽速度、加速度、jerk、QP 残差或轨迹发布验证阈值掩盖逻辑错误。 -- `OBSERVE_ONLY` 必须保持不变。本路线图不授权增加底盘命令、轨迹跟踪器或超时看门狗。 -- 工作树内已有且与本任务无关的修改全部视为用户资产。禁止清理、回退或提交这些修改。 -- 每次只实施当前阶段;完成交接文件后立即停止,不得提前执行下一阶段。 - -## 阶段总览 - -| 阶段 | 原计划任务 | 核心结果 | 阶段验收 | -|---|---:|---|---| -| 1. 纵向基础与窗口语义 | 1–2 | jerk 受限停车数学、三种纵向模式、空间窗口与真实停车边界分离、输入与配置验证 | `foundation`、`longitudinal-model` | -| 2. ST 优化核心 | 3–5 | 模式化速度包络、条件化 QP 终端约束、独立解验证、按模式生成初始解 | `longitudinal-model`、`longitudinal-integration` | -| 3. 发布与跨周期连续性 | 6–7 | 条件化轨迹采样、停车锚点和稳定尾段、发布验证、上一周期轨迹软参考 | `trajectory`、`em-planning-service`、`coordinator`、`executor` | -| 4. 端到端集成与诊断 | 8–10 | 模式流转、多周期 Goal/GearSwitch 回归、MovementTest 诊断、全量回归和目标机 OSQP 验证 | `em-core-all`、观察测试、构建、目标机探针 | - -## 阶段一:纵向基础与窗口语义 - -对应十任务计划中的 Task 1 和 Task 2。 - -### 实施范围 - -- 新建完整 jerk 受限停车数学,统一停车距离、停车时间、末速度和末加速度语义。 -- 新建 `EmLongitudinalMode`: - - `RollingContinuation` - - `ApproachStopBoundary` - - `ExactStopAtBoundary` -- 新建 `LongitudinalTerminalSchedule`,计算精确停车稳定尾段的起始 knot。 -- 让 `PlanningHorizonSelector` 分离: - - LS 空间窗口末端; - - 真实 Goal/GearSwitch 停车边界; - - 当前纵向模式。 -- 扩展 `LongitudinalPlanningInput` 和请求验证,拒绝不一致的模式、边界和稳定时间配置。 - -### 允许修改的主要文件 - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalTerminalSchedule.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs` -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` - -若编译修复必须触及直接调用者,可以进行最小兼容修改,但必须在阶段交接中逐项解释。 - -### 完成条件 - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -两项均须输出 `PASS`。阶段内按原十任务计划保留 Task 1、Task 2 两个独立提交。 - -## 阶段二:ST 优化核心 - -对应十任务计划中的 Task 3、Task 4 和 Task 5。 - -### 实施范围 - -- 按纵向模式构造 PathS 速度包络。 -- Rolling 模式的局部路径末端不再出现人为零速边界。 -- 仅为 `ExactStopAtBoundary` 添加停车位置、速度、加速度以及稳定尾段硬约束。 -- 让 QP 行数、上下界和验证器随模式与稳定尾段 knot 数量变化。 -- 使用与 QP 相同的恒 jerk 离散动力学做精确停车可达性验证。 -- 为 Rolling、ApproachStop 和 ExactStop 构造各自合理的初始解和外层迭代行为。 - -### 阶段输入 - -- 阶段一完成后的提交。 -- `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md`。 -- 阶段一生成的阶段二精细提示词。 - -### 完成条件 - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -两项均须输出 `PASS`,并保留 Task 3、Task 4、Task 5 的独立提交边界。 - -## 阶段三:轨迹发布与跨周期连续性 - -对应十任务计划中的 Task 6 和 Task 7。 - -### 实施范围 - -- 在轨迹元数据中保存纵向模式和真实停车边界身份。 -- Rolling 和 ApproachStop 只发布时间范围内的运动轨迹,不追加虚假零速保持点。 -- ExactStop 发布 QP 内部静止稳定段,并按配置追加外部零速保持点。 -- 轨迹验证器按模式检查终端状态,同时保持普通运动学、碰撞和边界验证。 -- 将上一条已发布轨迹按绝对时间重采样,并映射到新路径的 PathS,作为软参考而不是硬约束。 - -### 阶段输入 - -- 阶段二完成后的提交和交接文档。 -- 阶段二根据实际结果生成的阶段三精细提示词。 - -### 完成条件 - -至少运行并通过: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -``` - -保留 Task 6 和 Task 7 的独立提交边界。 - -## 阶段四:端到端集成与实测诊断 - -对应十任务计划中的 Task 8、Task 9 和 Task 10。 - -### 实施范围 - -- 完成 `RollingContinuation → ApproachStopBoundary → ExactStopAtBoundary` 的服务流转。 -- 覆盖真实 Goal、GearSwitchApproach、多周期滚动和停车稳定尾段。 -- 完善 MovementTest 终端/UI 诊断,但继续保持 `OBSERVE_ONLY`。 -- 更新 EMPlanner 文档。 -- 完成全量回归、构建和目标运行环境中的 OSQP 探针。 - -### 阶段输入 - -- 阶段三完成后的提交和交接文档。 -- 阶段三根据实际结果生成的阶段四精细提示词。 - -### 完成条件 - -严格执行十任务计划 Task 10 中列出的全部验证命令。至少包含: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet build ClumsyPilot/ClumsyPilot.csproj -``` - -目标机 OSQP 验证必须在实际部署/测试机器上运行;若当前窗口不在目标机,只能明确记录为“待目标机验证”,不得声称已通过。 - -## 跨窗口交接协议 - -### 每个阶段开始时 - -1. 阅读设计规格、十任务计划、本路线图、上一阶段交接文件和当前阶段提示词。 -2. 运行 `git status --short` 和 `git log -8 --oneline`,记录阶段开始基线。 -3. 确认上一阶段要求的提交存在;若不存在或测试不通过,先停止并报告,不得假设已完成。 -4. 只执行当前阶段对应的原计划任务。 - -### 每个阶段结束时 - -1. 重新运行本阶段全部验收命令,不能只引用较早的结果。 -2. 检查每个提交只包含当前任务文件,不得使用 `git add .` 或 `git add -A`。 -3. 创建交接文件: - - `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-N.md` - -4. 交接文件必须记录: - - - 阶段开始和结束提交; - - 每个任务的提交号; - - 实际修改和新增的文件; - - 新增或改变的公开/内部接口; - - 每条验证命令、退出码和关键输出; - - 未解决问题、目标机限制和与计划的偏差; - - 下一阶段必须继承的具体事实。 - -5. 根据交接文件和当前仓库状态创建下一阶段提示词: - - `docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-(N+1).md` - -6. 下一阶段提示词不得只写“继续原计划”,必须包含实际提交号、实际接口、准确任务边界、允许修改文件、测试命令、提交边界和完成后的交接要求。 -7. 提交交接文件与下一阶段提示词,然后停止,不执行下一阶段代码。 - -阶段四没有下一阶段提示词,改为生成最终验收报告: - -`docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-final.md` - -## 阶段状态 - -- [ ] 阶段一:纵向基础与窗口语义 -- [ ] 阶段二:ST 优化核心 -- [ ] 阶段三:轨迹发布与跨周期连续性 -- [ ] 阶段四:端到端集成与实测诊断 - -当前入口提示词:`docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-1.md` diff --git a/docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md b/docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md deleted file mode 100644 index 5a489ec..0000000 --- a/docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md +++ /dev/null @@ -1,1435 +0,0 @@ -# EM Longitudinal Rolling Planning Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 修复 EM 纵向 ST 规划,使局部空间窗口能够连续滚动,只在真实 Goal 或 GearSwitchApproach 边界以 jerk 受限的 `S=S_end、U=0、A=0` 状态停车。 - -**Architecture:** 将 LS 空间窗口、ST 时间范围和真实停车边界拆成独立概念,由 `EmLongitudinalMode` 统一驱动速度包络、QP终端约束、初始迭代、轨迹采样和发布验证。使用与 QP 恒 jerk 动力学一致的完整 S 曲线停车模型,并将上一条已发布轨迹按绝对时间重采样为纵向软参考。 - -**Tech Stack:** C# 10、.NET Standard 2.0、OSQP QP接口、`EMPlannerVerificationHost`(.NET 10 Windows 控制台验证宿主)、PowerShell、Git。 - -## Global Constraints - -- 设计规格以 `docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md` 为唯一行为依据。 -- `DistanceHorizonMeters` 是 LS 空间前视范围,不是 ST 必须在本周期走完的距离。 -- `TimeHorizonSeconds` 是 ST 优化时间范围,不包含 `ZeroSpeedHoldSeconds`。 -- 只有真实 `Goal` 或 `GearSwitchApproach` 可以触发准确停车;局部窗口截断点不能触发零速保持。 -- 准确停车必须在 ST 时间范围结束前到达 `S=S_end、U=0、A=0`,并在 QP 内保留至少一个完整 `OutputTimeStepSeconds` 的静止稳定尾;不得放宽速度、加速度、jerk、QP残差或发布验证阈值。 -- 滚动和接近模式允许非零末点速度、加速度及 yaw rate,但仍必须满足所有普通运动学和碰撞限制。 -- `OBSERVE_ONLY` 保持不变;不增加底盘命令、不增加轨迹执行器超时看门狗。 -- 保留工作树中与本计划无关的用户修改;每次只暂存当前任务列出的文件。 - ---- - -## File Structure - -### 新建文件 - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs`:定义滚动、接近停车边界、准确停车三种纵向行为。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs`:完整 jerk 受限停车曲线、停止距离反求和时间范围内最大可停车行驶距离。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalTerminalSchedule.cs`:统一计算准确停车锚点和 QP 内部稳定尾 knot 范围。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs`:将上一条世界坐标轨迹按新时间 knot 重采样并映射到新 LateralPath 的 PathS。 - -### 主要修改文件 - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs`:空间窗口和停车可达性选择。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs`:拆分路径上界、真实停车边界和纵向模式。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimit.cs`:允许滚动路径末端非零速度上限。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs`:按模式构建 jerk 受限停车速度包络。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs`:仅准确停车添加 `S/U/A` 终端硬约束。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs`:按模式验证终端状态及逐点可停车域。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs`:按模式生成初始迭代并取消滚动末点强制固定。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs`:在发布轨迹中记录纵向模式。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs`:只为准确停车追加零速保持点。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs`:只为准确停车标记真实边界锚点。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs`:条件化终端验证并新增终点加速度校验。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs`:验证空间前视的停车余量。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`:传递纵向模式、上一轨迹种子和模式诊断。 -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs`:显示模式、停车能力和成功末态。 -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`:记录新的窗口和终端语义。 - -### 测试文件 - -- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - ---- - -### Task 1: 完整 jerk 受限停车数学 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` - -**Interfaces:** -- Produces: `JerkLimitedStoppingMath.TryCalculate(double, double, double, double, out JerkLimitedStoppingProfile, out string)`、`MaximumInitialSpeedForDistance(double, double, double, double, double)`、`CalculateMaximumStoppedDistance(double, double, double, double, double, double, double)`。 -- Produces: `JerkLimitedStoppingProfile`,包含距离、持续时间、末速和末加速度。 -- Consumers: Tasks 2、3、4、8。 - -- [ ] **Step 1: 写出停车末态和可达距离失败测试** - -在 `LongitudinalModelChecks.Run()` 开头加入: - -```csharp -VerifiesJerkLimitedStoppingProfileEndsAtRest(); -VerifiesStoppedReachabilityUsesTheSameJerkModel(); -``` - -新增完整测试: - -```csharp -private static void VerifiesJerkLimitedStoppingProfileEndsAtRest() -{ - Verification.True(JerkLimitedStoppingMath.TryCalculate( - 0.20d, 0d, 0.30d, 0.50d, - out JerkLimitedStoppingProfile profile, out string failure), - "jerk-limited stop builds: " + failure); - Verification.True(profile.DistanceMeters > 0d, "stop distance is positive"); - Verification.True(profile.DurationSeconds > 0d, "stop duration is positive"); - Verification.NearlyEqual(0d, profile.FinalSpeedMetersPerSecond, - "stop ends at zero speed"); - Verification.NearlyEqual(0d, profile.FinalAccelerationMetersPerSecondSquared, - "stop releases acceleration to zero"); - - Verification.True(JerkLimitedStoppingMath.TryCalculate( - 0.20d, 0.20d, 0.30d, 0.50d, - out JerkLimitedStoppingProfile accelerating, out failure), - "positive-acceleration stop builds: " + failure); - Verification.True(accelerating.DistanceMeters > profile.DistanceMeters, - "positive initial acceleration needs more stopping distance"); - Verification.NearlyEqual(0d, accelerating.FinalAccelerationMetersPerSecondSquared, - "positive-acceleration stop also releases acceleration"); -} - -private static void VerifiesStoppedReachabilityUsesTheSameJerkModel() -{ - double maximumDistance = JerkLimitedStoppingMath.CalculateMaximumStoppedDistance( - 0d, 0d, 0.20d, 0.20d, 0.30d, 0.50d, 2d); - Verification.True(maximumDistance > 0d && maximumDistance < 0.40d, - "two-second stopped reach is finite and below unconstrained cruise distance"); - - double cap = JerkLimitedStoppingMath.MaximumInitialSpeedForDistance( - maximumDistance, 0.20d, 0.30d, 0.50d, 0.20d); - Verification.True(cap >= 0d && cap <= 0.20d, - "distance inversion stays inside the direction speed range"); -} -``` - -- [ ] **Step 2: 运行测试并确认因类型不存在而失败** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: 编译失败,指出 `JerkLimitedStoppingMath` 和 `JerkLimitedStoppingProfile` 不存在。 - -- [ ] **Step 3: 新建停车数学并迁移旧类型** - -在新文件定义: - -```csharp -public sealed class JerkLimitedStoppingProfile -{ - internal JerkLimitedStoppingProfile(double distanceMeters, double durationSeconds, - double finalSpeedMetersPerSecond, double finalAccelerationMetersPerSecondSquared) - { - DistanceMeters = distanceMeters; - DurationSeconds = durationSeconds; - FinalSpeedMetersPerSecond = finalSpeedMetersPerSecond; - FinalAccelerationMetersPerSecondSquared = finalAccelerationMetersPerSecondSquared; - } - - public double DistanceMeters { get; } - public double DurationSeconds { get; } - public double FinalSpeedMetersPerSecond { get; } - public double FinalAccelerationMetersPerSecondSquared { get; } -} - -public static class JerkLimitedStoppingMath -{ - private const double NumericTolerance = 1e-12d; - - public static bool TryCalculate(double speed, double acceleration, - double maximumDeceleration, double maximumJerk, - out JerkLimitedStoppingProfile profile, out string failureReason) - { - profile = null; - failureReason = string.Empty; - if (!IsFinite(speed) || speed < 0d || !IsFinite(acceleration) || - !IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) || - acceleration < -maximumDeceleration - NumericTolerance) - { - failureReason = "Stopping inputs are outside finite longitudinal bounds."; - return false; - } - if (speed <= NumericTolerance && Math.Abs(acceleration) <= NumericTolerance) - { - profile = new JerkLimitedStoppingProfile(0d, 0d, 0d, 0d); - return true; - } - - double unavoidableReleaseLoss = acceleration < 0d - ? acceleration * acceleration / (2d * maximumJerk) - : 0d; - if (speed + NumericTolerance < unavoidableReleaseLoss) - { - failureReason = "The current negative acceleration cannot be released before speed crosses zero."; - return false; - } - - double peakDeceleration = Math.Sqrt(maximumJerk * speed + - 0.5d * acceleration * acceleration); - double downDuration; - double plateauDuration; - double upDuration; - if (peakDeceleration <= maximumDeceleration + NumericTolerance) - { - peakDeceleration = Math.Min(peakDeceleration, maximumDeceleration); - downDuration = (acceleration + peakDeceleration) / maximumJerk; - plateauDuration = 0d; - upDuration = peakDeceleration / maximumJerk; - } - else - { - peakDeceleration = maximumDeceleration; - downDuration = (acceleration + peakDeceleration) / maximumJerk; - upDuration = peakDeceleration / maximumJerk; - double speedAfterDown = speed + acceleration * downDuration - - 0.5d * maximumJerk * downDuration * downDuration; - double releaseLoss = peakDeceleration * peakDeceleration / - (2d * maximumJerk); - plateauDuration = (speedAfterDown - releaseLoss) / peakDeceleration; - } - if (downDuration < -NumericTolerance || plateauDuration < -NumericTolerance) - { - failureReason = "No monotone three-phase jerk-limited stop exists for the current state."; - return false; - } - - downDuration = Math.Max(0d, downDuration); - plateauDuration = Math.Max(0d, plateauDuration); - double s = 0d; - double u = speed; - double a = acceleration; - Integrate(ref s, ref u, ref a, -maximumJerk, downDuration); - Integrate(ref s, ref u, ref a, 0d, plateauDuration); - Integrate(ref s, ref u, ref a, maximumJerk, upDuration); - if (Math.Abs(u) > 1e-9d || Math.Abs(a) > 1e-9d || s < -NumericTolerance) - { - failureReason = "The jerk-limited stop did not end at zero speed and acceleration."; - return false; - } - profile = new JerkLimitedStoppingProfile(Math.Max(0d, s), - downDuration + plateauDuration + upDuration, 0d, 0d); - return true; - } - - public static double MaximumInitialSpeedForDistance(double availableDistance, - double conservativeInitialAcceleration, double maximumDeceleration, - double maximumJerk, double directionMaximumSpeed) - { - if (!IsFinite(availableDistance) || availableDistance < 0d || - !IsFinite(conservativeInitialAcceleration) || - !IsPositiveFinite(directionMaximumSpeed)) - throw new ArgumentOutOfRangeException(nameof(availableDistance)); - double lower = 0d; - double upper = directionMaximumSpeed; - for (int iteration = 0; iteration < 64; iteration++) - { - double candidate = 0.5d * (lower + upper); - bool fits = TryCalculate(candidate, conservativeInitialAcceleration, - maximumDeceleration, maximumJerk, - out JerkLimitedStoppingProfile stop, out _) && - stop.DistanceMeters <= availableDistance + NumericTolerance; - if (fits) lower = candidate; - else upper = candidate; - } - return lower; - } - - public static double CalculateMaximumStoppedDistance(double initialSpeed, - double initialAcceleration, double maximumSpeed, double maximumAcceleration, - double maximumDeceleration, double maximumJerk, double timeHorizon) - { - if (!IsFinite(initialSpeed) || initialSpeed < 0d || - !IsFinite(initialAcceleration) || !IsPositiveFinite(maximumSpeed) || - !IsPositiveFinite(maximumAcceleration) || - !IsPositiveFinite(maximumDeceleration) || - !IsPositiveFinite(maximumJerk) || !IsPositiveFinite(timeHorizon)) - throw new ArgumentOutOfRangeException(nameof(timeHorizon)); - double lower = 0d; - double upper = timeHorizon; - double bestDistance = 0d; - for (int iteration = 0; iteration < 64; iteration++) - { - double driveDuration = 0.5d * (lower + upper); - AdvanceTowardMaximumSpeed(initialSpeed, initialAcceleration, - maximumSpeed, maximumAcceleration, maximumJerk, driveDuration, - out double driveDistance, out double speed, out double acceleration); - bool fits = TryCalculate(speed, acceleration, maximumDeceleration, - maximumJerk, out JerkLimitedStoppingProfile stop, out _) && - driveDuration + stop.DurationSeconds <= timeHorizon + NumericTolerance; - if (fits) - { - lower = driveDuration; - bestDistance = Math.Max(bestDistance, driveDistance + stop.DistanceMeters); - } - else - { - upper = driveDuration; - } - } - return bestDistance; - } - - private static void AdvanceTowardMaximumSpeed(double initialSpeed, - double initialAcceleration, double maximumSpeed, double maximumAcceleration, - double maximumJerk, double duration, out double distance, - out double speed, out double acceleration) - { - distance = 0d; - speed = initialSpeed; - acceleration = initialAcceleration; - double remaining = duration; - while (remaining > NumericTolerance) - { - double dt = Math.Min(0.001d, remaining); - double speedNeededToReleaseAcceleration = acceleration > 0d - ? acceleration * acceleration / (2d * maximumJerk) - : 0d; - double jerk = speed + speedNeededToReleaseAcceleration >= maximumSpeed - ? (acceleration > 0d ? -maximumJerk : 0d) - : (acceleration < maximumAcceleration ? maximumJerk : 0d); - Integrate(ref distance, ref speed, ref acceleration, jerk, dt); - if (speed > maximumSpeed && speed - maximumSpeed <= 1e-6d) - { - speed = maximumSpeed; - acceleration = 0d; - } - remaining -= dt; - } - } - - private static void Integrate(ref double s, ref double u, ref double a, - double jerk, double duration) - { - s += u * duration + 0.5d * a * duration * duration + - jerk * duration * duration * duration / 6d; - u += a * duration + 0.5d * jerk * duration * duration; - a += jerk * duration; - } - - private static bool IsPositiveFinite(double value) - { - return IsFinite(value) && value > 0d; - } - - private static bool IsFinite(double value) - { - return !double.IsNaN(value) && !double.IsInfinity(value); - } -} -``` - -按上述方法实现;不保留 `PathSpeedLimitBuilder.cs` 文件末尾旧的 `LongitudinalStoppingProfile` 和 `LongitudinalStoppingMath`,避免同名实现并存。 - -- [ ] **Step 4: 运行纵向模型测试并确认通过** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: `PASS longitudinal-model`。 - -- [ ] **Step 5: 独立提交停车数学** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs -git commit -m "feat: add complete jerk-limited stopping math" -``` - ---- - -### Task 2: 纵向模式、空间窗口和输入契约 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalTerminalSchedule.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` - -**Interfaces:** -- Consumes: Task 1 `JerkLimitedStoppingMath`。 -- Produces: `EmLongitudinalMode`、`LongitudinalTerminalSchedule.GetStabilizationStartIndex(IReadOnlyList, double)`、扩展后的 `PlanningHorizonSelection`、扩展后的 `LongitudinalPlanningInput`。 -- Consumers: Tasks 3–8。 - -- [ ] **Step 1: 写出三种模式和固定空间窗口测试** - -替换 `VerifiesReferenceHorizonSelectionKeepsTheCurrentSegmentBoundary()` 的断言并拆成三个场景: - -```csharp -private static void VerifiesReferenceHorizonSelectionSeparatesSpaceAndTime() -{ - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - configuration.Scheduling.DistanceHorizonMeters = 1d; - configuration.Scheduling.TimeHorizonSeconds = 2d; - - DirectionSegmentView longSegment = CreateSegment(4d, EmBoundaryType.Goal); - EmPlanningStatus status = new PlanningHorizonSelector().Select( - longSegment, 0d, 0d, 0d, configuration, - out PlanningHorizonSelection rolling, out string failure); - Verification.Equal(EmPlanningStatus.Success, status, "rolling selection: " + failure); - Verification.NearlyEqual(1d, rolling.WindowEndReferenceS, - "distance horizon defines the LS window"); - Verification.Equal(EmLongitudinalMode.RollingContinuation, - rolling.LongitudinalMode, "far boundary rolls"); - - DirectionSegmentView visibleButFar = CreateSegment(0.35d, EmBoundaryType.Goal); - status = new PlanningHorizonSelector().Select( - visibleButFar, 0d, 0d, 0d, configuration, - out PlanningHorizonSelection approach, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "approach selection: " + failure); - Verification.Equal(EmLongitudinalMode.ApproachStopBoundary, - approach.LongitudinalMode, "visible unreachable boundary approaches"); - - DirectionSegmentView reachableGoal = CreateSegment(0.10d, EmBoundaryType.Goal); - status = new PlanningHorizonSelector().Select( - reachableGoal, 0d, 0d, 0d, configuration, - out PlanningHorizonSelection exact, out failure); - Verification.Equal(EmPlanningStatus.Success, status, "exact selection: " + failure); - Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, - exact.LongitudinalMode, "reachable goal stops exactly"); - Verification.Equal(EmTerminalType.Goal, exact.TerminalType, - "exact stop preserves Goal identity"); - - IReadOnlyList regularTimes = LongitudinalCandidate.CreateKnotTimes(2d, 0.1d); - Verification.Equal(19, LongitudinalTerminalSchedule.GetStabilizationStartIndex( - regularTimes, 0.1d), "regular exact stop reserves t=1.9..2.0"); - Verification.Equal(1, LongitudinalTerminalSchedule.GetStabilizationStartIndex( - new[] { 0d, 0.1d, 0.2d, 0.25d }, 0.1d), - "short final interval moves the stop anchor earlier"); -} -``` - -在 `FoundationChecks` 添加配置失败场景,设置 `DistanceHorizonMeters` 小于“最大状态停车距离 + 一周期行驶余量”,断言 `InvalidInput` 且原因包含 `DistanceHorizonMeters`、`required` 和 `configured`。 - -- [ ] **Step 2: 运行失败测试** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -``` - -Expected: 编译失败,指出新枚举和选择结果属性不存在;旧选择器也不会返回三种预期模式。 - -- [ ] **Step 3: 增加模式和选择结果** - -创建: - -```csharp -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -public enum EmLongitudinalMode -{ - RollingContinuation, - ApproachStopBoundary, - ExactStopAtBoundary, -} -``` - -将 `PlanningHorizonSelection` 定义调整为: - -```csharp -internal PlanningHorizonSelection(double windowEndReferenceS, - EmTerminalType terminalType, EmLongitudinalMode longitudinalMode, - double stopBoundaryReferenceS, bool hasStopBoundary) -{ - WindowEndReferenceS = windowEndReferenceS; - TerminalType = terminalType; - LongitudinalMode = longitudinalMode; - StopBoundaryReferenceS = stopBoundaryReferenceS; - HasStopBoundary = hasStopBoundary; -} - -public double WindowEndReferenceS { get; } -public double TerminalReferenceS => WindowEndReferenceS; -public EmTerminalType TerminalType { get; } -public EmLongitudinalMode LongitudinalMode { get; } -public double StopBoundaryReferenceS { get; } -public bool HasStopBoundary { get; } -``` - -保留只读 `TerminalReferenceS` 兼容别名,避免同时修改不相关调用方;新代码只使用 `WindowEndReferenceS`。 - -同时创建统一稳定尾选择器: - -```csharp -public static class LongitudinalTerminalSchedule -{ - public static int GetStabilizationStartIndex(IReadOnlyList knotTimes, - double minimumStabilizationDurationSeconds) - { - if (knotTimes == null || knotTimes.Count < 3 || - double.IsNaN(minimumStabilizationDurationSeconds) || - double.IsInfinity(minimumStabilizationDurationSeconds) || - minimumStabilizationDurationSeconds <= 0d) - throw new ArgumentException("A positive stabilization tail and at least three knots are required."); - double previous = double.NegativeInfinity; - for (int index = 0; index < knotTimes.Count; index++) - { - if (double.IsNaN(knotTimes[index]) || double.IsInfinity(knotTimes[index]) || - knotTimes[index] <= previous) - throw new ArgumentException("Terminal-schedule knots must be finite and strictly increasing."); - previous = knotTimes[index]; - } - double finalTime = knotTimes[knotTimes.Count - 1]; - for (int index = knotTimes.Count - 2; index >= 1; index--) - { - if (finalTime - knotTimes[index] >= minimumStabilizationDurationSeconds - 1e-12d) - return index; - } - throw new ArgumentException("The time horizon cannot contain a full terminal stabilization interval."); - } -} -``` - -- [ ] **Step 4: 重写窗口选择器** - -核心分支必须等价于: - -```csharp -double remaining = segment.LengthMeters - currentSegmentReferenceS; -if (!JerkLimitedStoppingMath.TryCalculate(initialSpeed, initialAcceleration, - longitudinal.MaximumDecelerationMetersPerSecondSquared, - longitudinal.MaximumJerkMetersPerSecondCubed, - out JerkLimitedStoppingProfile stop, out failureReason)) - return EmPlanningStatus.StoppingDistanceInsufficient; - -if (stop.DistanceMeters > remaining + BoundaryTolerance) -{ - failureReason = "StoppingDistanceInsufficient: remaining=" + remaining + - ";required=" + stop.DistanceMeters + - ";shortage=" + (stop.DistanceMeters - remaining); - return EmPlanningStatus.StoppingDistanceInsufficient; -} - -double windowEnd = Math.Min(currentSegmentReferenceS + - scheduling.DistanceHorizonMeters, segment.LengthMeters); -bool containsBoundary = windowEnd >= segment.LengthMeters - BoundaryTolerance; -if (!containsBoundary) -{ - selection = new PlanningHorizonSelection(windowEnd, - EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, - segment.LengthMeters, false); - return EmPlanningStatus.Success; -} - -IReadOnlyList knotTimes = LongitudinalCandidate.CreateKnotTimes( - scheduling.TimeHorizonSeconds, scheduling.OutputTimeStepSeconds); -int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - knotTimes, scheduling.OutputTimeStepSeconds); -double availableMotionTime = knotTimes[stabilizationStart]; -double maximumStoppedDistance = - JerkLimitedStoppingMath.CalculateMaximumStoppedDistance( - initialSpeed, initialAcceleration, directionMaximum, - longitudinal.MaximumAccelerationMetersPerSecondSquared, - longitudinal.MaximumDecelerationMetersPerSecondSquared, - longitudinal.MaximumJerkMetersPerSecondCubed, - availableMotionTime); -EmLongitudinalMode mode = remaining <= maximumStoppedDistance + BoundaryTolerance - ? EmLongitudinalMode.ExactStopAtBoundary - : EmLongitudinalMode.ApproachStopBoundary; -selection = new PlanningHorizonSelection(segment.LengthMeters, - ToTerminalType(segment.EndBoundary.BoundaryType), mode, - segment.LengthMeters, true); -return EmPlanningStatus.Success; -``` - -删除旧 `CalculateReachableDistance()` 中扣除 `ZeroSpeedHoldSeconds` 的逻辑。 - -- [ ] **Step 5: 扩展纵向输入并验证组合** - -给 `LongitudinalPlanningInput` 构造函数增加 `EmLongitudinalMode mode`,并增加: - -```csharp -public EmLongitudinalMode Mode { get; } -public double PathUpperBoundS => Path.Points[Path.Points.Count - 1].PathS; -public bool HasStopBoundary => TerminalType != EmTerminalType.RollingSafetyStop; -public double StopBoundaryPathS => HasStopBoundary ? PathUpperBoundS : double.NaN; -[Obsolete("Use PathUpperBoundS or StopBoundaryPathS explicitly.")] -public double TerminalPathS => PathUpperBoundS; -``` - -构造函数拒绝以下矛盾组合: - -```csharp -if (mode == EmLongitudinalMode.RollingContinuation && - terminalType != EmTerminalType.RollingSafetyStop) - throw new ArgumentException("Rolling continuation requires a rolling window boundary."); -if (mode != EmLongitudinalMode.RollingContinuation && - terminalType == EmTerminalType.RollingSafetyStop) - throw new ArgumentException("Stop-boundary modes require Goal or GearSwitch."); -``` - -- [ ] **Step 6: 增加前视停车余量配置验证** - -在 `EmPlanningRequestValidator.TryValidateConfiguration` 中计算正向和反向最大速度下的保守停车距离,取较大值并增加: - -```text -previewReserve = maxSpeed * ReplanPeriodSeconds -requiredDistanceHorizon = worstStoppingDistance + previewReserve -``` - -若配置不足,返回包含配置值和所需值的具体原因。配置验证还调用 `LongitudinalTerminalSchedule.GetStabilizationStartIndex`,要求预留一个完整 `OutputTimeStepSeconds` 稳定尾之后仍至少有一个正运动 interval;停车时间超过剩余运动时间由 `ApproachStopBoundary` 处理,不拒绝配置。 - -- [ ] **Step 7: 更新所有 `LongitudinalPlanningInput` 构造调用并运行测试** - -所有现有 Goal/GearSwitch 测试先传 `EmLongitudinalMode.ExactStopAtBoundary`;原滚动测试传 `RollingContinuation`。`EmPlanningService` 创建纵向输入时传 `horizon.LongitudinalMode`,切片上界改用 `horizon.WindowEndReferenceS`。随后运行: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: `PASS foundation` 和 `PASS longitudinal-model`。 - -- [ ] **Step 8: 独立提交模式与窗口语义** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalTerminalSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs -git commit -m "feat: separate rolling horizons from stop boundaries" -``` - ---- - -### Task 3: 模式化 PathS 速度包络 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimit.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` - -**Interfaces:** -- Consumes: Task 1 停车距离反求;Task 2 `LongitudinalPlanningInput.Mode`。 -- Produces: 末端可为非零的 `PathSpeedLimit`,以及面向真实边界的停止速度上限。 -- Consumers: Tasks 4、5。 - -- [ ] **Step 1: 写出滚动末端非零和真实边界零速测试** - -加入: - -```csharp -private static void VerifiesRollingEnvelopeDoesNotStopAtWindowEnd() -{ - EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); - LateralPath path = CreatePath(new[] - { - new PathFixture(0d, 0d, 0d, 0d), - new PathFixture(1d, 1d, 0d, 0d), - }); - var rolling = new LongitudinalPlanningInput(path, TravelDirection.Forward, - 0d, 0d, EmTerminalType.RollingSafetyStop, - EmLongitudinalMode.RollingContinuation, configuration, - Array.Empty(), Array.Empty()); - EmPlanningStatus status = new PathSpeedLimitBuilder().Build( - rolling, out PathSpeedLimit envelope, out string failure); - Verification.Equal(EmPlanningStatus.Success, status, "rolling envelope: " + failure); - Verification.True(envelope.MaximumSpeedAt(rolling.PathUpperBoundS) > 0d, - "rolling window end keeps a nonzero speed allowance"); - Verification.True(!envelope.HasStopBoundary, - "rolling envelope has no stop boundary"); -} -``` - -保留 Goal 测试,但改为 `ExactStopAtBoundary` 并断言真实边界速度严格为零。增加 `ApproachStopBoundary` 测试,断言末端停止上限为零、时间末点不由该类固定。 - -- [ ] **Step 2: 运行测试并确认滚动末端仍被置零** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: `rolling window end keeps a nonzero speed allowance` 失败。 - -- [ ] **Step 3: 修改 `PathSpeedLimit` 契约** - -构造函数增加 `bool hasStopBoundary`,移除无条件末项为零校验,改为: - -```csharp -if (hasStopBoundary && - (MaximumSpeedMetersPerSecond[MaximumSpeedMetersPerSecond.Count - 1] != 0d || - StoppingSpeedLimitsMetersPerSecond[StoppingSpeedLimitsMetersPerSecond.Count - 1] != 0d)) - throw new ArgumentException("A real stop boundary must have an exact zero speed limit."); - -HasStopBoundary = hasStopBoundary; -``` - -公开: - -```csharp -public bool HasStopBoundary { get; } -public double PathUpperBoundS => PathS[PathS.Count - 1]; -[Obsolete("Use PathUpperBoundS.")] -public double TerminalPathS => PathUpperBoundS; -``` - -- [ ] **Step 4: 按模式构建停止速度上限** - -`RollingContinuation` 的 `stopping` 数组全部填 `directionMaximum`,最终组合速度不再因局部窗口结束而变成零。 - -`ApproachStopBoundary` 和 `ExactStopAtBoundary` 对每个 sample 使用: - -```csharp -double remaining = input.StopBoundaryPathS - samplePathS; -double stoppingValue = JerkLimitedStoppingMath.MaximumInitialSpeedForDistance( - Math.Max(0d, remaining), maximumAcceleration, - maximumDeceleration, maximumJerk, directionMaximum); -``` - -真实边界采样严格赋值零。删除旧的 `sqrt(2d * maximumDeceleration * remaining)` 和旧离散减速尾部公式;采样加密站根据新停车模型的距离反求点生成。 - -- [ ] **Step 5: 运行测试并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimit.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs -git commit -m "feat: keep rolling speed envelopes open" -``` - -Expected: `PASS longitudinal-model`。 - ---- - -### Task 4: 条件化 ST QP 终端约束与独立验证 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` - -**Interfaces:** -- Consumes: `LongitudinalPlanningInput.Mode`、`PathUpperBoundS`、`StopBoundaryPathS`。 -- Produces: 滚动/接近无终端等式、准确停车对边界锚点和内部稳定尾逐 knot 固定 `S/U/A` 的 QP。 -- Consumers: Task 5 optimizer。 - -- [ ] **Step 1: 扩展约束结构测试** - -在现有 `VerifiesTimeKnotLayoutDynamicsObjectiveAndHardConstraints` 中分别构建滚动和准确停车输入并断言: - -```csharp -Verification.Equal(0, CountExactEqualityRows(rollingProblem, - new Dictionary { { layout.S(4), 1d } }, rollingInput.PathUpperBoundS), - "rolling has no exact terminal S"); -Verification.Equal(0, CountExactEqualityRows(rollingProblem, - new Dictionary { { layout.U(4), 1d } }, 0d), - "rolling has no exact terminal U"); -Verification.Equal(0, CountExactEqualityRows(rollingProblem, - new Dictionary { { layout.A(4), 1d } }, 0d), - "rolling has no exact terminal A"); - -int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - integrated.KnotTimes, configuration.Scheduling.OutputTimeStepSeconds); -for (int index = stabilizationStart; index < layout.KnotCount; index++) -{ - Verification.Equal(1, CountExactEqualityRows(stopProblem, - new Dictionary { { layout.S(index), 1d } }, stopInput.StopBoundaryPathS), - "stop tail exact S " + index); - Verification.Equal(1, CountExactEqualityRows(stopProblem, - new Dictionary { { layout.U(index), 1d } }, 0d), - "stop tail exact U " + index); - Verification.Equal(1, CountExactEqualityRows(stopProblem, - new Dictionary { { layout.A(index), 1d } }, 0d), - "stop tail exact A " + index); -} -``` - -增加验证器测试:滚动候选末速非零应通过;准确停车候选在稳定尾任一 knot 出现 `S/U/A` 偏差应失败;Approach 候选任一点违反 `S + stoppingDistance(U,A) <= StopBoundaryPathS` 应失败。 - -- [ ] **Step 2: 运行失败测试** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: 旧 QP 仍为所有模式添加终端 `S/U`,且没有终端 `A`,测试失败。 - -- [ ] **Step 3: 动态计算约束行并条件化终端约束** - -在 `TryBuild` 中使用: - -```csharp -int stabilizationStart = input.Mode == EmLongitudinalMode.ExactStopAtBoundary - ? LongitudinalTerminalSchedule.GetStabilizationStartIndex(expectedTimes, - input.Configuration.Scheduling.OutputTimeStepSeconds) - : layout.KnotCount; -int stationaryKnotCount = layout.KnotCount - stabilizationStart; -int terminalRows = 3 * stationaryKnotCount; -int expectedRows = 8 * layout.KnotCount - 2 + terminalRows; -var constraints = new SparseTripletBuilder(expectedRows, layout.VariableCount); -var lower = new List(expectedRows); -var upper = new List(expectedRows); -``` - -将 `AddExactStartAndTerminal` 拆成: - -```csharp -AddExactStart(input, layout, constraints, lower, upper, ref row); -if (input.Mode == EmLongitudinalMode.ExactStopAtBoundary) - AddExactStopTail(input, layout, stabilizationStart, - constraints, lower, upper, ref row); -``` - -`AddExactStopTail` 对 `stabilizationStart..KnotCount-1` 的每个 knot 依次添加 `S=StopBoundaryPathS`、`U=0`、`A=0` 三个等式。 - -- [ ] **Step 4: 条件化候选验证和规范化** - -始终验证普通范围和动力学。仅准确停车执行: - -```csharp -int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - candidate.KnotTimes, input.Configuration.Scheduling.OutputTimeStepSeconds); -for (int index = stabilizationStart; index < candidate.S.Count; index++) -{ - if (!AreClose(candidate.S[index], input.StopBoundaryPathS, tolerance) || - !AreClose(candidate.U[index], 0d, tolerance) || - !AreClose(candidate.A[index], 0d, tolerance)) - { - failureReason = "ST candidate does not satisfy the exact stabilized S/U/A stop tail at knot " + index + "."; - return false; - } -} -``` - -Approach 和 ExactStop 逐点执行: - -```csharp -if (!JerkLimitedStoppingMath.TryCalculate(candidate.U[index], candidate.A[index], - maximumDeceleration, maximumJerk, out JerkLimitedStoppingProfile stop, out _) || - candidate.S[index] + stop.DistanceMeters > input.StopBoundaryPathS + tolerance) -{ - failureReason = "ST candidate leaves the jerk-limited stoppable set at knot " + index + "."; - return false; -} -``` - -只在准确停车模式将 canonical 的 `stabilizationStart..last` 全部写成 `StopBoundaryPathS/0/0`。 - -- [ ] **Step 5: 运行测试并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs -git commit -m "feat: apply ST stop constraints only at real boundaries" -``` - -Expected: `PASS longitudinal-model`。 - ---- - -### Task 5: 按模式构造纵向初始解和外层迭代 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` - -**Interfaces:** -- Consumes: Tasks 1–4 的停车模型、速度包络和条件化约束。 -- Produces: 可供 OSQP 热启动的滚动、接近和准确停车候选。 - -- [ ] **Step 1: 写出滚动初始解和 point-21 回归测试** - -在 `LongitudinalIntegrationChecks.Run()` 增加: - -```csharp -VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed(); -VerifiesExactStopIncludesAStabilizationTail(); -``` - -滚动测试使用直线 `LateralPath`、`TimeHorizonSeconds=2d`、`OutputTimeStepSeconds=0.1d`、`PathUpperBoundS=5d`,由现有 fake solver 返回优化器初始/严格 fallback,断言: - -```csharp -Verification.True(result.Candidate.S[result.Candidate.S.Count - 1] < input.PathUpperBoundS, - "two-second ST does not consume the five-metre LS window"); -Verification.True(result.Candidate.U[result.Candidate.U.Count - 1] > 0d, - "rolling ST keeps nonzero terminal speed"); -``` - -准确停车测试计算 `stabilizationStart` 并断言整个内部稳定尾: - -```csharp -int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - result.Candidate.KnotTimes, input.Configuration.Scheduling.OutputTimeStepSeconds); -for (int index = stabilizationStart; index < result.Candidate.S.Count; index++) -{ - Verification.NearlyEqual(input.StopBoundaryPathS, result.Candidate.S[index], - "stop-tail S " + index); - Verification.NearlyEqual(0d, result.Candidate.U[index], "stop-tail U " + index); - Verification.NearlyEqual(0d, result.Candidate.A[index], "stop-tail A " + index); -} -``` - -- [ ] **Step 2: 运行失败测试** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -Expected: 滚动候选仍被强制到路径末端零速,测试失败。 - -- [ ] **Step 3: 将初始迭代分派到三个构造器** - -调用点先改为 `CreateInitialIterate(input, speedLimit)`,方法按模式分派: - -```csharp -switch (input.Mode) -{ - case EmLongitudinalMode.RollingContinuation: - return CreateRollingSeed(input, times, speedLimit); - case EmLongitudinalMode.ApproachStopBoundary: - return CreateApproachSeed(input, times, speedLimit); - case EmLongitudinalMode.ExactStopAtBoundary: - return CreateExactStopSeed(input, times); - default: - throw new ArgumentOutOfRangeException(nameof(input.Mode)); -} -``` - -`CreateRollingSeed` 从 `S0=0,U0,A0` 开始,在每个 interval 内选择使速度向 `speedLimit.MaximumSpeedAt(S[k])` 靠近的 jerk,并使用 `LongitudinalCandidate.Integrate` 相同公式积分;遇到速度、加速度或 PathUpperBoundS 时缩小 jerk,不写死末项。 - -`CreateApproachSeed` 生成“允许巡航段 + 完整 jerk 受限停车段”,然后仅保留 `TimeHorizonSeconds` 内的前缀;每个 knot 的参考速度不超过 stopping envelope。 - -`CreateExactStopSeed` 先通过 `LongitudinalTerminalSchedule` 得到 `stabilizationStart`,只使用 `times[0..stabilizationStart]` 安排受限制的加速/巡航和 Task 1 三阶段停车,使 `stabilizationStart` 准确达到 `S_end,U=0,A=0`。随后将其余 knot 填充为同一 `S_end` 上的 `U=0,A=0,J=0` 内部稳定尾。 - -- [ ] **Step 4: 移除外层迭代中的无条件末点固定** - -`TryCreateEnvelopeIterate` 中只在准确停车模式执行: - -```csharp -int stabilizationStart = input.Mode == EmLongitudinalMode.ExactStopAtBoundary - ? LongitudinalTerminalSchedule.GetStabilizationStartIndex(candidate.KnotTimes, - input.Configuration.Scheduling.OutputTimeStepSeconds) - : candidate.S.Count; -if (input.Mode == EmLongitudinalMode.ExactStopAtBoundary && - index >= stabilizationStart) - candidateProgress = input.StopBoundaryPathS; -``` - -所有对 `input.TerminalPathS` 的上界用途改为 `PathUpperBoundS`。终点用途改为 `StopBoundaryPathS`,并且必须位于准确停车分支中。 - -`LongitudinalObjectiveBuilder` 保留速度、加速度、jerk 和上一轨迹目标;`TerminalAcceleration` 软目标只对非准确停车模式添加,准确停车的锚点和内部稳定尾已经由硬约束保证 `A=0`。 - -- [ ] **Step 5: 运行纵向模型与集成测试并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs -git commit -m "feat: seed rolling and exact-stop ST profiles" -``` - -Expected: `PASS longitudinal-model`、`PASS longitudinal-integration`。 - ---- - -### Task 6: 条件化轨迹采样、边界锚点和发布验证 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs` - -**Interfaces:** -- Consumes: `EmLongitudinalMode` 和经过验证的纵向候选。 -- Produces: 滚动轨迹不带保持点;准确停车轨迹带安全保持点和真实边界锚点。 - -- [ ] **Step 1: 写出滚动采样和终端 jerk 回归测试** - -在 `TrajectoryChecks.Run()` 增加: - -```csharp -VerifiesRollingTrajectoryHasNoSyntheticStopTail(); -VerifiesExactStopHoldHasNoJerkDiscontinuity(); -``` - -滚动测试使用 21 个 `0.1s` knot、非零末速和非零但受限的末加速度,断言点数仍为 21、末点 `BoundaryType.None`、末速非零。 - -准确停车回归构造的速度有限差分状态: - -```text -t=1.8: U=0.0025, A=-0.05 -t=1.9: U=0, A=0, exact stop anchor -t=2.0: U=0, A=0, QP stabilization point -t=2.1: U=0, A=0, first external hold -``` - -然后运行 `EmTrajectoryValidator`,断言通过,并显式计算: - -```csharp -double terminalFiniteDifferenceAcceleration = - (anchor.SignedLongitudinalVelocity - previousMoving.SignedLongitudinalVelocity) / - (anchor.TimeFromStart - previousMoving.TimeFromStart); -double stabilizationAcceleration = - (stabilization.SignedLongitudinalVelocity - anchor.SignedLongitudinalVelocity) / - (stabilization.TimeFromStart - anchor.TimeFromStart); -double externalHoldAcceleration = - (firstExternalHold.SignedLongitudinalVelocity - stabilization.SignedLongitudinalVelocity) / - (firstExternalHold.TimeFromStart - stabilization.TimeFromStart); -double jerkIntoStabilization = (stabilizationAcceleration - - terminalFiniteDifferenceAcceleration) / - (stabilization.TimeFromStart - anchor.TimeFromStart); -double jerkIntoExternalHold = (externalHoldAcceleration - stabilizationAcceleration) / - (firstExternalHold.TimeFromStart - stabilization.TimeFromStart); -Verification.True(Math.Abs(jerkIntoStabilization) <= 0.5d, - "jerk into QP stabilization stays within the limit"); -Verification.NearlyEqual(0d, jerkIntoExternalHold, - "jerk into the external hold is zero"); -``` - -再构造内部稳定尾任一 knot 的 `S/U/A` 非零偏差,断言采样器或发布验证器拒绝;构造真实边界锚点存储加速度非零的轨迹,断言发布验证器以 `TerminalAccelerationNotZero` 拒绝。 - -- [ ] **Step 2: 运行测试并确认旧采样器拒绝滚动末速** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -``` - -Expected: 旧 `TrajectorySampleSchedule` 抛出“requires an exact zero-speed terminal candidate”或点数/边界断言失败。 - -- [ ] **Step 3: 给元数据增加纵向模式** - -`EmTrajectoryMetadata` 构造函数末尾增加 `EmLongitudinalMode longitudinalMode`,验证枚举并公开: - -```csharp -public EmLongitudinalMode LongitudinalMode { get; } -``` - -`EmPlanningService` 构造元数据时传 `horizon.LongitudinalMode`。`TrajectoryChecks` 中 Goal/GearSwitch 元数据传 `ExactStopAtBoundary`;`TrajectoryObservationChecks`、`ExecutorChecks` 和 `CoordinatorChecks` 中代表运动中轨迹的 fixture 传 `RollingContinuation`,代表完成停车的 fixture 传 `ExactStopAtBoundary`。 - -- [ ] **Step 4: 按模式构建采样计划** - -`TrajectorySampleSchedule` 构造函数增加 mode: - -```csharp -if (mode != EmLongitudinalMode.ExactStopAtBoundary) -{ - Samples = new ReadOnlyCollection(candidateSamples); - TerminalAnchorSampleIndex = -1; - return; -} -int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex( - candidate.KnotTimes, outputTimeStepSeconds); -double stopPathS = candidate.S[stabilizationStart]; -for (int index = stabilizationStart; index < candidate.S.Count; index++) -{ - if (Math.Abs(candidate.S[index] - stopPathS) > ZeroTolerance || - Math.Abs(candidate.U[index]) > ZeroTolerance || - Math.Abs(candidate.A[index]) > ZeroTolerance || - (index < candidate.J.Count && Math.Abs(candidate.J[index]) > ZeroTolerance)) - throw new ArgumentException("An exact stop requires a stationary S/U/A/J tail."); -} -TerminalAnchorSampleIndex = stabilizationStart; -AppendHoldSamples(candidateSamples, outputTimeStepSeconds, holdDurationSeconds); -``` - -公开只读 `TerminalAnchorSampleIndex`。保持段从优化末点之后开始,不复制同时间戳点。 - -- [ ] **Step 5: 只为准确停车标记边界锚点** - -`EmTrajectoryAssembler` 使用: - -```csharp -bool isTerminalAnchor = metadata.LongitudinalMode == - EmLongitudinalMode.ExactStopAtBoundary && - index == schedule.TerminalAnchorSampleIndex; -EmBoundaryType boundaryType = isTerminalAnchor - ? ToBoundaryType(metadata.TerminalType) - : EmBoundaryType.None; -``` - -滚动时间末点和 Approach 时间末点都保持 `BoundaryType.None`。 - -- [ ] **Step 6: 条件化世界坐标发布验证** - -在 `EmTrajectoryValidationFailure` 增加: - -```csharp -TerminalAccelerationNotZero, -``` - -始终执行普通运动学、碰撞和有限差分 jerk 检查。仅准确停车模式寻找真实边界锚点,检查锚点存储的 `LongitudinalAcceleration`、速度和 yaw rate 近零,并验证从锚点到轨迹末尾保持同一 PathS、零速和零状态加速度。滚动/Approach 不调用 `FindTerminalAnchor`,也不检查零末态。 - -- [ ] **Step 7: 运行轨迹测试并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs -git commit -m "feat: publish rolling trajectories without stop tails" -``` - -Expected: `PASS trajectory`、`PASS coordinator`、`PASS executor`、`PASS trajectory-observation`,point-21 回归通过。 - ---- - -### Task 7: 上一轨迹纵向软参考 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -**Interfaces:** -- Produces: `LongitudinalPreviousTrajectorySeed`,包含相同数量的 PathS 和进度速度样本。 -- Consumes: 上一条 `EmTrajectory`、新 `LateralPath`、新轨迹生效时间和新 knot 时间。 - -- [ ] **Step 1: 写出时间对齐、方向过滤和 PathS 映射测试** - -添加测试:当前 LateralPath 是 `X/PathS=0..1m` 的直线;上一轨迹 `EffectiveAtUtc=10s`,在 `t=0,0.1,0.2,0.3,0.4s` 分别位于 `X=0,0.1,0.2,0.3,0.4m`;以 `newEffectiveAtUtc=10.2s`、新 knot `{0,0.1,0.2}` 重采样。断言: - -```csharp -Verification.Equal(3, seed.PathS.Count, "previous seed path-S count"); -Verification.Equal(3, seed.ProgressSpeedMetersPerSecond.Count, - "previous seed speed count"); -Verification.NearlyEqual(0.20d, seed.PathS[0], - "previous seed begins at new absolute effective time"); -Verification.True(seed.PathS[1] >= seed.PathS[0] && seed.PathS[2] >= seed.PathS[1], - "previous seed progress is monotone"); -``` - -再用不同方向、不同 segment 和已超出上一轨迹时间范围的输入,断言返回空 seed。 - -- [ ] **Step 2: 运行失败测试** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: 编译失败,seed builder 不存在。 - -- [ ] **Step 3: 实现上一轨迹重采样器** - -公开只读结果: - -```csharp -public sealed class LongitudinalPreviousTrajectorySeed -{ - public IReadOnlyList PathS { get; } - public IReadOnlyList ProgressSpeedMetersPerSecond { get; } - public static LongitudinalPreviousTrajectorySeed Empty { get; } -} -``` - -builder 的入口: - -```csharp -public LongitudinalPreviousTrajectorySeed Build(EmTrajectory previous, - LateralPath currentPath, DateTimeOffset newEffectiveAtUtc, - IReadOnlyList newKnotTimes, int segmentIndex, - TravelDirection direction) -``` - -逐 knot 使用绝对时间: - -```text -sampleUtc = newEffectiveAtUtc + newKnotTime -previousTime = sampleUtc - previous.Metadata.EffectiveAtUtc -``` - -在线性插值上一轨迹世界坐标和 signed speed 后,把世界点投影到 `currentPath.Points[i] -> Points[i+1]` 的折线段,取距离最小且不小于上一投影 PathS 的结果。进度速度使用 `abs(interpolatedSignedSpeed)`。任一输入不兼容时返回 `Empty`,不得抛出影响规划发布的异常。 - -- [ ] **Step 4: 在服务中传递模式和纵向 seed** - -`EmPlanningService` 在 lateral path 成功后创建 knot 时间并调用 builder: - -```csharp -IReadOnlyList knotTimes = LongitudinalCandidate.CreateKnotTimes( - configuration.Scheduling.TimeHorizonSeconds, - configuration.Scheduling.OutputTimeStepSeconds); -LongitudinalPreviousTrajectorySeed previous = - new LongitudinalPreviousTrajectorySeedBuilder().Build( - request.PreviousTrajectory, lateral.Path, request.EffectiveAtUtc, - knotTimes, segment.SegmentIndex, segment.Direction); - -var longitudinalInput = new LongitudinalPlanningInput( - lateral.Path, segment.Direction, initialProgressSpeed, - initialAcceleration, horizon.TerminalType, - horizon.LongitudinalMode, configuration, - previous.PathS, previous.ProgressSpeedMetersPerSecond); -``` - -元数据同时写入 `horizon.LongitudinalMode`。服务切片使用 `horizon.WindowEndReferenceS`。 - -- [ ] **Step 5: 验证服务不再传空 seed** - -在 `EmPlanningServiceChecks` 的 scripted solver/capture helper 中记录纵向 QP目标项。带有效上一轨迹时断言 previous-S 和 previous-U 权重产生非零对应 Hessian/linear cost;无效上一轨迹时仍然成功且无该项。 - -- [ ] **Step 6: 运行并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs -git commit -m "feat: reuse prior trajectory in longitudinal planning" -``` - -Expected: `PASS longitudinal-model`、`PASS em-planning-service`。 - ---- - -### Task 8: 服务模式流转、Goal/GearSwitch 与多周期回归 - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs` - -**Interfaces:** -- Consumes: Tasks 1–7 的完整规划链路。 -- Produces: 证明 `RollingContinuation -> ApproachStopBoundary -> ExactStopAtBoundary` 的服务级行为。 - -- [ ] **Step 1: 写多周期模式流转测试** - -创建一条足够长的无障碍直线路径和三组车辆状态:边界在空间窗口外、边界在窗口内但不在时间停车可达范围内、边界在准确停车可达范围内。每次调用 `EmPlanningService.Plan`,断言: - -```csharp -Verification.Equal(EmLongitudinalMode.RollingContinuation, - rolling.Trajectory.Metadata.LongitudinalMode, "cycle 1 rolls"); -Verification.True(rolling.Trajectory.Points[rolling.Trajectory.Points.Count - 1] - .SignedLongitudinalVelocity != 0d, "cycle 1 has nonzero terminal speed"); - -Verification.Equal(EmLongitudinalMode.ApproachStopBoundary, - approach.Trajectory.Metadata.LongitudinalMode, "cycle 2 approaches"); - -Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, - exact.Trajectory.Metadata.LongitudinalMode, "cycle 3 stops"); -Verification.NearlyEqual(0d, exact.Trajectory.Points[exactAnchor] - .SignedLongitudinalVelocity, "goal speed is zero"); -Verification.True(exact.Trajectory.Points.Count > exactAnchor + 1, - "goal anchor is followed by a QP stabilization point"); -Verification.NearlyEqual(exact.Trajectory.Points[exactAnchor].PathS, - exact.Trajectory.Points[exactAnchor + 1].PathS, - "goal stabilization keeps the stop position"); -Verification.NearlyEqual(0d, exact.Trajectory.Points[exactAnchor + 1] - .SignedLongitudinalVelocity, "goal stabilization speed is zero"); -``` - -为 GearSwitch 使用 `CreateGearPairReferencePath()` 重复准确停车检查,并确认发布轨迹不包含下一方向段点。 - -- [ ] **Step 2: 写五米空间窗口/两秒时间范围回归** - -配置: - -```csharp -configuration.Scheduling.DistanceHorizonMeters = 5d; -configuration.Scheduling.TimeHorizonSeconds = 2d; -configuration.Scheduling.OutputTimeStepSeconds = 0.1d; -configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.2d; -``` - -断言发布点数为 21,最后一个 PathS 小于 5m,末速非零,并且发布验证通过。 - -- [ ] **Step 3: 运行失败测试并完成最小服务修正** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -``` - -Expected before fixture updates: service fixture 或 fake solver 仍假定每条轨迹零速停车,至少一个新断言失败。若失败来自 Tasks 1–7 的生产实现,返回所属任务修正并重新执行其测试;本任务只修改测试 fixture,不放宽生产校验。 - -- [ ] **Step 4: 更新旧 rolling service fixture** - -旧测试通过 `DistanceHorizonMeters=0.003m` 人工制造 rolling terminal;改为使用大于配置停车前视下限的 DistanceHorizon 和更长的参考路径。scripted solver 按 `EmLongitudinalMode` 返回: - -- rolling/approach:非零末速、无保持尾; -- exact stop:真实边界锚点及其后的 QP 内部稳定尾 `S/U/A` 准确,允许继续追加外部保持尾。 - -- [ ] **Step 5: 运行并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -git add -- ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs -git commit -m "test: cover rolling-to-stop EM planning flow" -``` - -Expected: `PASS longitudinal-model`、`PASS longitudinal-integration`、`PASS trajectory`、`PASS em-planning-service`。 - ---- - -### Task 9: 观察日志和文档 - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` - -**Interfaces:** -- Consumes: 轨迹元数据中的 `LongitudinalMode` 和 Task 1 停车能力计算。 -- Produces: 启动配置停车能力说明,以及每条成功轨迹的模式和末态摘要。 - -- [ ] **Step 1: 写日志失败测试** - -在配置诊断断言中增加: - -```text -maximumJerkLimitedStopDistance= -maximumJerkLimitedStopDuration= -requiredDistanceHorizon= -``` - -在成功轨迹摘要断言中增加: - -```text -longitudinalMode=RollingContinuation -terminalSpeed= -terminalAcceleration= -``` - -服务失败原因断言至少覆盖: - -```text -longitudinalMode= -remainingToBoundary= -minimumStoppingDistance= -minimumStoppingDuration= -maximumStoppedReachableDistance= -``` - -- [ ] **Step 2: 运行并确认缺少字段** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: 新字段断言失败。 - -- [ ] **Step 3: 扩展一次性配置和每周期摘要** - -`CreateConfiguration` 使用正向/反向最大速度的较坏情况调用停车模型,并打印: - -```text -maximumJerkLimitedStopDistance=0.259m -maximumJerkLimitedStopDuration=1.800s -requiredDistanceHorizon=0.299m -``` - -`CreateTrajectorySummary` 增加: - -```text -longitudinalMode=RollingContinuation -terminalSpeed=0.180m/s -terminalAcceleration=0.000m/s2 -``` - -这些配置仍只在 session 启动时打印一次,不加入 UI 每帧重绘块。 - -- [ ] **Step 4: 更新 README** - -在 EMPlanner README 明确: - -```text -DistanceHorizonMeters controls the LS reference window. -TimeHorizonSeconds controls the ST output duration. -Only Goal/GearSwitchApproach may require an exact zero-speed terminal. -RollingContinuation and ApproachStopBoundary may publish nonzero terminal speed. -``` - -在 MovementTest README 明确仍为 `OBSERVE_ONLY`,滚动轨迹不会下发底盘。 - -- [ ] **Step 5: 运行并提交** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md -git commit -m "docs: explain rolling longitudinal planning diagnostics" -``` - -Expected: `PASS trajectory-observation`。 - ---- - -### Task 10: 全量回归和目标机 OSQP 验证 - -**Files:** -- No file changes planned; this task is verification-only。 -- Do not modify: unrelated dirty PathSmoothing、CoarsePath、Map or user configuration files。 - -**Interfaces:** -- Consumes: all prior tasks。 -- Produces: verified implementation ready for observation-only target-machine testing。 - -- [ ] **Step 1: 运行核心回归** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: 每条命令分别输出它在 `Program.cs` 中定义的 `PASS` 行;不出现新的编译错误或验证失败。 - -- [ ] **Step 2: 构建主工程** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -``` - -Expected: Build succeeded。已有 obsolete warning 可以保留,但不得新增本计划相关 warning。 - -- [ ] **Step 3: 在装有正确 OSQP 原生依赖的目标机运行真实求解探针** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-real-osqp-probe -``` - -Expected: `PASS longitudinal-real-osqp`。若原生 DLL 无法加载,只记录为目标机依赖问题,不通过修改纵向逻辑或放宽残差阈值绕过。 - -- [ ] **Step 4: 运行 MovementTest observation-only 实测** - -使用当前手工参数启动 `TrajectoryObservationMovementTest`,确认: - -1. 启动时只打印一次规划配置和最大 jerk 受限停车能力; -2. 远离 Goal 时发布 `RollingContinuation`,末速非零且无 hold; -3. 接近 Goal 时出现 `ApproachStopBoundary`; -4. 最终出现 `ExactStopAtBoundary`,终端 `U=0,A=0`; -5. 不再出现 point 21 `JerkLimitExceeded`; -6. 终端持续打印 `OBSERVE_ONLY: no chassis command is sent.`。 - -- [ ] **Step 5: 检查只包含计划内改动** - -```powershell -git status --short -git log --oneline -12 -``` - -逐个核对 Tasks 1–9 的提交;不得暂存或提交用户现有 PathSmoothing、CoarsePath、Map 和 MovementTest 手工配置改动。 - -- [ ] **Step 6: 处理最终回归结果** - -若 Step 1–4 失败,返回引入该行为的 Task 1–9,在该任务列出的文件范围和测试命令内修正并追加对应任务提交;不得用一个无法确定所有权的“最终修复”提交混合多个模块。全部通过时不创建空提交。 diff --git a/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md b/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md deleted file mode 100644 index 56131e8..0000000 --- a/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md +++ /dev/null @@ -1,334 +0,0 @@ -# EM Observation Diagnostics Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make every EM planning-cycle failure visible in the host terminal and the World/L-S/S-T observation canvases without changing observation-only safety behavior. - -**Architecture:** Add a pure diagnostic formatter that keeps `EmPlanningStatus` and the original `FailureReason` from `PlanningCycleResult`. Extend the observation-loop tick with explicit start/completion events so the runner writes one pending line and one completed-cycle line per cycle, while the painters receive the current diagnostic every tick. - -**Tech Stack:** C# / .NET Standard 2.0 plugin, `Hedingben.ToastText`, `UI.GetPainter`, EM planner contracts, .NET verification host. - -## Global Constraints - -- The MovementTest remains observe-only: do not add any chassis, brake, wheel, or actuator call. -- Terminal output uses `Console.WriteLine` and starts with `[TrajectoryObserver]`. -- Every completed cycle reports raw `EmPlanningStatus`, `published`, version, elapsed time, and the original nonempty `FailureReason`. -- A 50 ms observer tick must not emit a duplicate terminal record. -- World, L-S, and S-T painters must show the diagnostic even when `PublishedTrajectory` is null. - ---- - -### Task 1: Add a pure planning diagnostic formatter - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** -- Consumes: `PlanningCycleResult`, `EmTrajectory`, `TimeSpan`, and the in-flight flag. -- Produces: `TrajectoryObservationDiagnostic.Text`, a compact multi-line operator string. - -- [ ] **Step 1: Write the failing test** - -Add `VerifiesPlanningDiagnosticsKeepRawFailureReason();` to `Run()`, then add: - -```csharp -private static void VerifiesPlanningDiagnosticsKeepRawFailureReason() -{ - var failed = new PlanningCycleResult( - 4L, - new PlanningCycleIdentity(3L, "diagnostic-reference", 7L, string.Empty, 0), - new EmPlanningResult(EmPlanningStatus.CorridorInfeasible, null, - "map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"), - false, - "map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"); - - TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create( - failed, TimeSpan.FromMilliseconds(18d), false, null); - - Verification.True(diagnostic.Text.Contains("cycle=4"), "diagnostic has cycle version"); - Verification.True(diagnostic.Text.Contains("status=CorridorInfeasible"), "diagnostic preserves raw status"); - Verification.True(diagnostic.Text.Contains("published=False"), "diagnostic preserves publish state"); - Verification.True(diagnostic.Text.Contains("elapsed=18ms"), "diagnostic preserves elapsed time"); - Verification.True(diagnostic.Text.Contains("reason=map=3;reference=diagnostic-reference"), - "diagnostic preserves planner failure reason"); - - TrajectoryObservationDiagnostic pending = TrajectoryObservationDiagnostics.Create( - null, TimeSpan.Zero, true, null); - Verification.Equal("planning status=pending", pending.Text, "diagnostic reports pending before completion"); -} -``` - -- [ ] **Step 2: Run the test to verify RED** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: build failure because `TrajectoryObservationDiagnostic` and `TrajectoryObservationDiagnostics` do not exist. - -- [ ] **Step 3: Write the minimal formatter** - -Create `TrajectoryObservationDiagnostics.cs`: - -```csharp -using System; -using System.Globalization; -using MultiWheelC.TrajectoryPlanning.EMPlanner; - -namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; - -public sealed class TrajectoryObservationDiagnostic -{ - public TrajectoryObservationDiagnostic(string text) - { - Text = text ?? string.Empty; - } - - public string Text { get; } -} - -public static class TrajectoryObservationDiagnostics -{ - public static TrajectoryObservationDiagnostic Create(PlanningCycleResult latestCycle, - TimeSpan elapsed, bool planningInFlight, EmTrajectory publishedTrajectory) - { - if (latestCycle == null) - return new TrajectoryObservationDiagnostic(planningInFlight - ? "planning status=pending" - : "planning status=not-started"); - - string text = "planning cycle=" + latestCycle.Version.ToString(CultureInfo.InvariantCulture) + - " status=" + latestCycle.Result.Status + - " published=" + latestCycle.Published + - " elapsed=" + Math.Max(0d, elapsed.TotalMilliseconds).ToString("F0", CultureInfo.InvariantCulture) + "ms"; - if (planningInFlight) - text += "\nreplan=pending"; - if (publishedTrajectory != null) - text += "\ntrajectory=" + publishedTrajectory.Metadata.TrajectoryId; - if (!string.IsNullOrWhiteSpace(latestCycle.Result.FailureReason)) - text += "\nreason=" + latestCycle.Result.FailureReason; - return new TrajectoryObservationDiagnostic(text); - } -} -``` - -- [ ] **Step 4: Run the test to verify GREEN** - -Run the Step 2 command. - -Expected: `PASS trajectory-observation`. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: format EM observation diagnostics" -``` - -### Task 2: Report once at the start and completion of every planning cycle - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** -- Produces: `TrajectoryObservationLoopTick.PlanningStarted` and `.PlanningCompleted`. -- Consumes: those flags in the MovementTest to issue one terminal/UI status record per lifecycle event. - -- [ ] **Step 1: Write the failing test** - -In `VerifiesObserverTicksWhilePlanningIsDelayed`, after the first tick, add: - -```csharp -Verification.True(firstTick.PlanningStarted, "observer first tick reports a planning-cycle start"); -Verification.True(!firstTick.PlanningCompleted, "observer first tick has no completed cycle"); -``` - -After `finalTick` is created, add: - -```csharp -Verification.True(finalTick.PlanningCompleted, "observer completion tick reports cycle completion"); -``` - -In `VerifiesObservationSourceUsesRequiredOperatorText`, add: - -```csharp -Verification.True(source.Contains("Console.WriteLine(\"[TrajectoryObserver] \" + text);"), - "observer status is mirrored to the host terminal"); -``` - -- [ ] **Step 2: Run the test to verify RED** - -Run the Task 1 test command. - -Expected: assertions fail because lifecycle flags and terminal output do not exist. - -- [ ] **Step 3: Implement lifecycle flags and output** - -Change `TrajectoryObservationLoopTick` to accept and expose: - -```csharp -bool planningStarted, bool planningCompleted - -public bool PlanningStarted { get; } - -public bool PlanningCompleted { get; } -``` - -In `TrajectoryObservationLoop.Tick`, use: - -```csharp -bool planningCompleted = ConsumeCompletedPlanning(now); -bool planningStarted = false; -if (planningTask == null && controller.ShouldStartCycle(now)) -{ - planningStarted = true; - planningStartedAtUtc = now; - planningTask = controller.StartCycle(now, state, cancellationToken); - planningCompleted |= ConsumeCompletedPlanning(now); -} -``` - -Change `ConsumeCompletedPlanning` to return `false` when no completed Task is available and `true` after it assigns `latestCycle`, updates elapsed time, and clears `planningTask`. Pass both flags to the tick constructor. - -In `RunSessionAsync`, after the tick is created, make one diagnostic and only log event records: - -```csharp -TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create( - tick.LatestCycle, tick.LatestPlanningElapsed, tick.PlanningInFlight, - observation.PublishedTrajectory); -if (tick.PlanningStarted) - LogIfCurrent(sessionId, "planning status=pending"); -if (tick.PlanningCompleted) - LogIfCurrent(sessionId, diagnostic.Text); -``` - -Remove the unconditional `if (tick.ShouldLog)` status call. Keep the existing session-start, stop, bootstrap-failure, and runtime-fault calls. - -Change `PrintStatus` to: - -```csharp -private static void PrintStatus(string message) -{ - string text = ObserveOnlyNotice + "\n" + message; - Hedingben.ToastText(text, StatusChannel); - Console.WriteLine("[TrajectoryObserver] " + text); -} -``` - -- [ ] **Step 4: Run the test to verify GREEN** - -Run the Task 1 test command. - -Expected: `PASS trajectory-observation`; the delayed-planner test still proves the observer does not wait for planning. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: report EM observation planning cycles" -``` - -### Task 3: Persist planning diagnostics in all three painter layers - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** -- Consumes: `TrajectoryObservationDiagnostic.Text`. -- Produces: World/L-S/S-T empty states that show the precise planning status and reason. - -- [ ] **Step 1: Write the failing test** - -Add `VerifiesEmptyChartsReceivePersistentPlanningDiagnostic();` to `Run()` and add: - -```csharp -private static void VerifiesEmptyChartsReceivePersistentPlanningDiagnostic() -{ - string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", - "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); - string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); - - Verification.True(source.Contains("DrawLs(TrajectoryObservationCharts charts, string diagnosticText)"), - "LS painter accepts planning diagnostic input"); - Verification.True(source.Contains("DrawSt(TrajectoryObservationCharts charts, string diagnosticText)"), - "ST painter accepts planning diagnostic input"); - Verification.True(source.Contains("No published trajectory available for L-S chart.\n"), - "LS empty state includes diagnostic after chart label"); - Verification.True(source.Contains("No published trajectory available for T-S/T-V charts.\n"), - "ST empty state includes diagnostic after chart label"); -} -``` - -- [ ] **Step 2: Run the test to verify RED** - -Run the Task 1 test command. - -Expected: source checks fail because painter methods have no diagnostic parameter. - -- [ ] **Step 3: Add painter parameters and wire the diagnostic** - -Change signatures to: - -```csharp -public void DrawWorld(TrajectoryObservationBootstrapResult bootstrap, - TrajectoryObservationObservation observation, TrajectoryObservationRuntimeState runtimeState, - string diagnosticText) - -public void DrawLs(TrajectoryObservationCharts charts, string diagnosticText) - -public void DrawSt(TrajectoryObservationCharts charts, string diagnosticText) -``` - -Add this helper in `TrajectoryObservationPresentation`: - -```csharp -private static string EmptyChartMessage(string label, string diagnosticText) -{ - return string.IsNullOrWhiteSpace(diagnosticText) - ? label - : label + "\n" + diagnosticText; -} -``` - -For World, draw `EmptyChartMessage("No published trajectory available.", diagnosticText)` at `bootstrap.Map.Bounds.XMin + 100f, bootstrap.Map.Bounds.YMin + 300f` before returning from the empty trajectory path. For L-S and S-T, draw `EmptyChartMessage` with their existing label at `0f, 0f`. - -Change `DrawIfCurrent` to accept `TrajectoryObservationDiagnostic diagnostic` and call: - -```csharp -Presentation.DrawWorld(bootstrap, observation, runtimeState, diagnostic?.Text ?? string.Empty); -Presentation.DrawLs(charts, diagnostic?.Text ?? string.Empty); -Presentation.DrawSt(charts, diagnostic?.Text ?? string.Empty); -``` - -Pass the diagnostic created in Task 2 from `RunSessionAsync`. For the bootstrap-failure path, pass: - -```csharp -new TrajectoryObservationDiagnostic("bootstrap failed: " + bootstrap.FailureReason) -``` - -- [ ] **Step 4: Verify focused test, build, and diff** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -git diff --check -``` - -Expected: `PASS trajectory-observation`, zero build errors, and no diff whitespace errors. - -- [ ] **Step 5: Commit** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: show EM observation failure diagnostics" -``` - diff --git a/docs/superpowers/plans/2026-08-05-em-observation-planning-diagnostics.md b/docs/superpowers/plans/2026-08-05-em-observation-planning-diagnostics.md deleted file mode 100644 index d6aa580..0000000 --- a/docs/superpowers/plans/2026-08-05-em-observation-planning-diagnostics.md +++ /dev/null @@ -1,296 +0,0 @@ -# EM Observation Planning Diagnostics Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. - -**Goal:** Print one effective EM planning configuration snapshot at observation-session startup and make trajectory validation failures and successes numerically diagnosable without changing planning behavior. - -**Architecture:** EmTrajectoryValidator enriches its existing jerk rejection reason with the values used in the decision. TrajectoryObservationDiagnostics owns pure invariant-culture formatters for configuration and successful trajectory metrics. The MovementTest runner prints the configuration formatter once after it creates the session-local controller. - -**Tech Stack:** C#/.NET, existing EMPlannerVerificationHost, OSQP-backed EM planner. - -## Global Constraints - -- Keep the MovementTest OBSERVE_ONLY; do not add actuator calls. -- Do not change solver configuration, motion limits, QP formulation, horizon selection, or acceptance rules. -- Print configuration once per session; do not put it in the recurring UI diagnostic or every planning cycle. -- Format values with CultureInfo.InvariantCulture. -- Failed publications keep returning no EmTrajectory; their numeric evidence belongs in the failure reason. - -## Preflight: Preserve manual MovementTest parameter tuning - -`TrajectoryObservationMovementTest` exposes fields precisely so an operator can tune a test session. The verification host must not require those source literals to equal the defaults of `TrajectoryObservationSettings`; it must continue to verify that the fields exist, are copied into settings, and that `TrajectoryObservationSettings` itself retains its independently tested defaults. - -- [ ] Run `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation` and observe the current source-literal failure. -- [ ] In `TrajectoryObservationChecks.VerifiesMovementTestVehicleInputsAndSettingsSnapshot`, replace only the four source-text checks that require `0.50d`, `12000`, `6d`, and `0.10d` with checks for `public` fields named `SolverTimeoutSeconds`, `MaximumOsqpIterations`, `TimeHorizonSeconds`, and `OutputTimeStepSeconds`. Preserve the separate `new TrajectoryObservationSettings()` default assertions and all field-to-settings propagation checks. -- [ ] Re-run the same command and require exit code `0` before adding the Task 1 regression test. - ---- - -### Task 1: Detail a jerk-limit rejection with computed evidence - -**Files:** - -- Modify: ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs:132-199 -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs:58-110 - -**Interfaces:** - -- Consumes: the existing EmTrajectoryValidator.Validate(...) method. -- Produces: an unchanged JerkLimitExceeded result whose Message contains time, dt, previousAcceleration, acceleration, jerk, limit, excess, storedPreviousJerk, and storedCurrentJerk. - -- [ ] **Step 1: Write the failing test** - -In TrajectoryChecks.VerifiesWorldSpacePublicationMutationsAreRejected, retain the current jerkMutated trajectory and add: - - EmTrajectoryValidationResult jerkResult = new EmTrajectoryValidator().Validate( - jerkMutated, context.EmptyMap, context.Vehicle, context.Configuration, 2, 0.0055d, EmBoundaryType.Goal); - Verification.Equal(EmTrajectoryValidationFailure.JerkLimitExceeded, jerkResult.Failure, - "jerk diagnostic failure code"); - Verification.Equal(2, jerkResult.PointIndex, "jerk diagnostic point index"); - foreach (string field in new[] - { - "time=", "dt=", "previousAcceleration=", "acceleration=", "jerk=", "limit=", "excess=", - "storedPreviousJerk=", "storedCurrentJerk=", - }) - Verification.True(jerkResult.Message.Contains(field), "jerk diagnostic includes " + field); - -- [ ] **Step 2: Run the test to confirm RED** - -Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory - -Expected: the new assertion fails because the current message contains only Trajectory finite-difference jerk exceeds its limit. - -- [ ] **Step 3: Implement the message enrichment** - -Add using System.Globalization; to EmTrajectoryValidator.cs. Calculate finiteDifferenceJerk before the condition and preserve the existing inequality: - - double finiteDifferenceJerk = (acceleration - previousAcceleration) / dt; - if (hasPreviousAcceleration && Math.Abs(finiteDifferenceJerk) > limits.MaximumJerk + limits.KinematicTolerance) - { - double storedPreviousJerk = index >= 2 ? trajectory.Points[index - 2].LongitudinalJerk : 0d; - double storedCurrentJerk = previous.LongitudinalJerk; - return Reject(EmTrajectoryValidationFailure.JerkLimitExceeded, index, - "Trajectory finite-difference jerk exceeds its limit" + - " (time=" + Format(point.TimeFromStart) + "s, dt=" + Format(dt) + "s" + - ", previousAcceleration=" + Format(previousAcceleration) + "m/s2" + - ", acceleration=" + Format(acceleration) + "m/s2" + - ", jerk=" + Format(finiteDifferenceJerk) + "m/s3" + - ", limit=" + Format(limits.MaximumJerk) + "m/s3" + - ", excess=" + Format(Math.Abs(finiteDifferenceJerk) - limits.MaximumJerk) + "m/s3" + - ", storedPreviousJerk=" + Format(storedPreviousJerk) + "m/s3" + - ", storedCurrentJerk=" + Format(storedCurrentJerk) + "m/s3)."); - } - -Add this private method to the same class: - - private static string Format(double value) - { - return value.ToString("G17", CultureInfo.InvariantCulture); - } - -storedPreviousJerk and storedCurrentJerk are evidence only and do not influence the validation decision. - -- [ ] **Step 4: Confirm GREEN and commit** - -Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory - -Expected: host exit code 0. - -Run: - - git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs - git commit -m "feat: detail trajectory jerk validation failures" - -### Task 2: Format and print the effective session configuration once - -**Files:** - -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs:17-39 -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs:188-241 -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs:269-305 -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs:18-39,96-191 - -**Interfaces:** - -- Consumes: the effective EmPlannerConfiguration held privately by TrajectoryObservationController. -- Produces: TrajectoryObservationDiagnostics.CreateConfiguration(EmPlannerConfiguration) and TrajectoryObservationController.CreateConfigurationDiagnostic(). -- Produces: one terminal planning configuration block before the observation loop begins. - -- [ ] **Step 1: Write failing configuration tests** - -Add VerifiesPlanningConfigurationDiagnosticUsesEffectiveConfiguration() to TrajectoryObservationChecks.Run(). Bootstrap a controller with: - - var settings = new TrajectoryObservationSettings - { - ReplanPeriodSeconds = 0.25d, - SolverTimeoutSeconds = 1.25d, - MaximumOsqpIterations = 54321, - TimeHorizonSeconds = 3.5d, - OutputTimeStepSeconds = 0.10d, - }; - var controller = new TrajectoryObservationController(bootstrap, settings, - new FixedTrajectoryPlanningService(CreatePublishedTrajectory(effectiveAt)), "config-diagnostic"); - string text = controller.CreateConfigurationDiagnostic().Text; - foreach (string expected in new[] - { - "planning configuration:", "timeHorizon=3.50s", "distanceHorizon=5.00m", "outputTimeStep=0.10s", - "outputFrequency=10.00Hz", "trajectoryKnots=36", "maximumOsqpIterations=54321", - "solverTimeout=1.25s", "replanPeriod=0.25s", - }) - Verification.True(text.Contains(expected), "configuration diagnostic includes " + expected); - -Source-read MovementTest.TrajectoryObservationTest.cs and assert the runner call is singular: - - int firstCall = runnerSource.IndexOf("CreateConfigurationDiagnostic()", StringComparison.Ordinal); - Verification.True(firstCall >= 0, "runner prints configuration diagnostic"); - Verification.Equal(-1, runnerSource.IndexOf("CreateConfigurationDiagnostic()", firstCall + 1, - StringComparison.Ordinal), "runner prints configuration diagnostic once"); - -- [ ] **Step 2: Run the test to confirm RED** - -Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation - -Expected: compilation fails because CreateConfigurationDiagnostic does not exist. - -- [ ] **Step 3: Implement pure formatting and the one runner call** - -Add to TrajectoryObservationDiagnostics: - - public static TrajectoryObservationDiagnostic CreateConfiguration(EmPlannerConfiguration configuration) - { - if (configuration == null || configuration.Scheduling == null || configuration.Solver == null) - throw new ArgumentNullException(nameof(configuration)); - double horizon = configuration.Scheduling.TimeHorizonSeconds; - double step = configuration.Scheduling.OutputTimeStepSeconds; - int knots = checked((int)Math.Ceiling(horizon / step) + 1); - return new TrajectoryObservationDiagnostic("planning configuration:\n" + - "timeHorizon=" + Format(horizon, "F2") + "s\n" + - "distanceHorizon=" + Format(configuration.Scheduling.DistanceHorizonMeters, "F2") + "m\n" + - "outputTimeStep=" + Format(step, "F2") + "s\n" + - "outputFrequency=" + Format(1d / step, "F2") + "Hz\n" + - "trajectoryKnots=" + knots.ToString(CultureInfo.InvariantCulture) + "\n" + - "maximumOsqpIterations=" + configuration.Solver.MaximumOsqpIterations.ToString(CultureInfo.InvariantCulture) + "\n" + - "solverTimeout=" + Format(configuration.Scheduling.SolverTimeoutSeconds, "F2") + "s\n" + - "replanPeriod=" + Format(configuration.Scheduling.ReplanPeriodSeconds, "F2") + "s"); - } - - private static string Format(double value, string format) - { - return value.ToString(format, CultureInfo.InvariantCulture); - } - -Add to TrajectoryObservationController: - - public TrajectoryObservationDiagnostic CreateConfigurationDiagnostic() - { - return TrajectoryObservationDiagnostics.CreateConfiguration(configuration); - } - -Immediately after constructing controller in RunSessionAsync, before new TrajectoryObservationLoop(controller), add exactly: - - LogIfCurrent(sessionId, controller.CreateConfigurationDiagnostic().Text); - -Do not call this method from the while loop or the UI drawing methods. - -- [ ] **Step 4: Confirm GREEN and commit** - -Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation - -Expected: host exit code 0. - -Run: - - git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs - git commit -m "feat: log EM observation session planning configuration" - -### Task 3: Summarize each successfully published trajectory - -**Files:** - -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs:17-39 -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs:538-561 - -**Interfaces:** - -- Consumes: latestCycle.Result.Trajectory only when latestCycle.Published is true. -- Produces: a trajectory summary with ID, point count, duration, PathS length, max speed, max finite-difference acceleration, and max finite-difference jerk. - -- [ ] **Step 1: Write the failing summary test** - -Add VerifiesPublishedPlanningDiagnosticsIncludeTrajectorySummary() to TrajectoryObservationChecks.Run(): - - EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt); - var succeeded = new PlanningCycleResult(9L, - new PlanningCycleIdentity(1L, "summary-reference", 2L, string.Empty, 0), - new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty), true, string.Empty); - string text = TrajectoryObservationDiagnostics.Create( - succeeded, TimeSpan.FromMilliseconds(12d), false, trajectory).Text; - foreach (string expected in new[] - { - "trajectory summary:", "trajectoryId=observer-published", "points=2", "duration=1.000s", - "pathLength=1.000m", "maxSpeed=0.400m/s", "maxAcceleration=0.200m/s2", "maxJerk=0.000m/s3", - }) - Verification.True(text.Contains(expected), "published diagnostic includes " + expected); - -Also add this assertion to the existing failed-cycle test: - - Verification.True(!diagnostic.Text.Contains("trajectory summary:"), "failed diagnostic has no stale trajectory summary"); - -- [ ] **Step 2: Run the test to confirm RED** - -Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation - -Expected: summary assertions fail because the formatter only writes trajectory=. - -- [ ] **Step 3: Implement summary formatting** - -Replace the publishedTrajectory != null branch in TrajectoryObservationDiagnostics.Create(...) with a branch requiring latestCycle.Published && latestCycle.Result.Trajectory != null. Add CreateTrajectorySummary(EmTrajectory trajectory), which: - -- reads immutable trajectory points; -- calculates maximum absolute signed speed; -- calculates acceleration and jerk by the same direction-aware finite-difference convention as EmTrajectoryValidator; -- ignores a nonpositive or non-finite dt only in logging; -- reports first and last PathS difference as actual pathLength; -- uses Format(value, "F3") and CultureInfo.InvariantCulture; -- returns exactly these two lines: - - trajectory summary: - trajectoryId=, points=, duration=s, pathLength=m - maxSpeed=m/s, maxAcceleration=m/s2, maxJerk=m/s3 - -Do not call this helper for an unsuccessful cycle, even when an older published trajectory is still observed. - -- [ ] **Step 4: Confirm GREEN, inspect, and commit** - -Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation - git diff --check - -Expected: host exit code 0 and no whitespace errors. - -Run: - - git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs - git commit -m "feat: summarize published EM observation trajectories" - -## Final verification - -- [ ] Run: - - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory - dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation - git diff --check - git log -3 --oneline - -Expected: verification exits 0, no whitespace errors exist, and three focused diagnostics commits follow the already committed design document. diff --git a/docs/superpowers/plans/2026-08-05-em-observation-web-integration.md b/docs/superpowers/plans/2026-08-05-em-observation-web-integration.md deleted file mode 100644 index 98470f2..0000000 --- a/docs/superpowers/plans/2026-08-05-em-observation-web-integration.md +++ /dev/null @@ -1,694 +0,0 @@ -# EM Observation Web Visualization Integration Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Upgrade `TrajectoryObservationMovementTest` into a complete multi-segment, observe-only EM planning experiment that publishes effective configuration, global/active path geometry, LS/ST and kinematic evidence to the reusable local scientific dashboard. - -**Architecture:** A pure segment tracker advances only after a real stop and stable signed-speed confirmation. The controller owns one coordinator per active direction segment, while pure snapshot builders convert existing EM/map/smoothing data into the generic visualization contracts. A thin MovementTest host conditionally owns web and Painter sessions and isolates every visualization failure from planning. - -**Tech Stack:** C# 10, `netstandard2.0`, existing EMPlanner/TrajectoryExecution/Map/PathSmoothing modules, `TrajectoryPlanningVisualization`, OSQP, MDCS read interfaces, existing console verification host. - -## Prerequisite - -Complete and review `docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md` first. This plan consumes its exact `PlanningVisualizationSession`, snapshot, chart, geometry, and configuration contracts. - -## Global Constraints - -- Continue `OBSERVE_ONLY`; do not call chassis, motor, steering, brake, wheel, gear, or controller write APIs. -- Use actual MDCS pose and signed longitudinal speed only as read inputs. -- Preserve user defaults exactly: solver timeout `5.0 s`, OSQP maximum iterations `100000`, ST horizon `2.0 s`, output step `0.10 s`. -- Default web visualization is off; default native Painter visualization is off. -- Web refresh defaults to `10 Hz`; history defaults to `60`; port defaults to `0`. -- Segment transitions are strictly `N -> N+1`; never skip, infer at zero speed, or reuse a previous-direction trajectory as an EM seed. -- Current and previous cycles may compare world position and shared direction-segment `ReferenceS`; never compare their independent local `PathS` origins. -- `j[i]` represents `[t_i, t_i+1)`; publish exactly `trajectory.Points.Count - 1` jerk samples and no synthetic terminal sample. -- Static geometry/configuration is built once after successful bootstrap. Dynamic snapshots are built at most at `WebRefreshRateHz`. -- Visualization exceptions are logged once and disable only the web output. -- Preserve unrelated worktree changes and stage only task files. - ---- - -## File Structure - -| File | Responsibility | -| --- | --- | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` | Runtime, web and direction-confirmation settings with frozen validation. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs` | Pure stop/direction/projection state machine. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` | Active-segment controller and asynchronous loop integration. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` | Map/global paths/direction segments/effective configuration. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` | LS/ST/curvature/v/a/j/yaw-rate generic charts. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs` | Absolute-time interpolation and shared-segment handoff deltas. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` | World overlays, status, charts and cycle summary. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs` | 10 Hz gating and exception fuse. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` | Public fields, lifecycle, browser launch and conditional Painter/web ownership. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` | Lazily created optional native Painter fallback. | -| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` | Operator workflow and chart interpretation. | -| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` | Defaults, validation and frozen-setting checks. | -| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` | Pure multi-segment transition checks. | -| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` | Static/dynamic adapter and web isolation checks. | -| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` | Existing regression entry; calls the new focused groups. | -| `ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs` | New managed DLL packaging contract. | -| `ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1` | Copies the visualization class library beside `ClumsyPilot.dll`. | -| `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` | Updated plugin tree and observation link. | - ---- - -### Task 1: Synchronize effective settings and visualization controls - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` - -**Interfaces:** - -- Extends `TrajectoryObservationSettings` with web/Painter and direction-confirmation properties. -- `CreateValidatedSnapshot()` freezes every new property. -- MovementTest public fields map one-to-one into settings before background work begins. - -- [ ] **Step 1: Write failing defaults, copy, and validation checks** - -Add `TrajectoryObservationSettingsChecks.Run()` and invoke it from `TrajectoryObservationChecks.Run()`. - -```csharp -var defaults = new TrajectoryObservationSettings(); -Verification.NearlyEqual(5d, defaults.SolverTimeoutSeconds, "observer solver timeout default"); -Verification.Equal(100000, defaults.MaximumOsqpIterations, "observer OSQP default"); -Verification.NearlyEqual(2d, defaults.TimeHorizonSeconds, "observer ST horizon default"); -Verification.True(!defaults.EnableWebVisualization, "web defaults off"); -Verification.True(!defaults.EnableNativePainterVisualization, "Painter defaults off"); -Verification.Equal(0, defaults.WebVisualizationPort, "dynamic port default"); -Verification.NearlyEqual(10d, defaults.WebRefreshRateHz, "web refresh default"); -Verification.Equal(60, defaults.VisualizationHistoryCycleLimit, "history default"); -Verification.NearlyEqual(0.02d, defaults.DirectionConfirmationSpeedMetersPerSecond, - "direction speed threshold"); -Verification.Equal(3, defaults.DirectionConfirmationSamples, "direction sample default"); -Verification.NearlyEqual(0.50d, defaults.GearSwitchProjectionToleranceMeters, - "gear projection default"); -Verification.NearlyEqual(0.20d, defaults.GearSwitchStopHoldSeconds, "gear stop hold default"); -``` - -Mutate all source values after `CreateValidatedSnapshot()` and assert the snapshot retains the originals. Add invalid cases for port `-1/1/1023/65536`, refresh `0`, history `0`, speed threshold `0`, sample count `0`, projection tolerance `0`, and hold `0`. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: failure because settings still use `0.50 / 12000 / 6.0` and the new properties do not exist. - -- [ ] **Step 3: Implement settings and MovementTest field mapping** - -Add exact get/set defaults to settings and matching public fields to MovementTest: - -```csharp -public bool EnableWebVisualization { get; set; } = false; -public bool AutoOpenWebVisualization { get; set; } = true; -public int WebVisualizationPort { get; set; } = 0; -public double WebRefreshRateHz { get; set; } = 10d; -public int VisualizationHistoryCycleLimit { get; set; } = 60; -public bool EnableNativePainterVisualization { get; set; } = false; -public double DirectionConfirmationSpeedMetersPerSecond { get; set; } = 0.02d; -public int DirectionConfirmationSamples { get; set; } = 3; -public double GearSwitchProjectionToleranceMeters { get; set; } = 0.50d; -public double GearSwitchStopHoldSeconds { get; set; } = 0.20d; -``` - -Use inclusive validation for port `0` and `1024..65535`. Update the Chinese XML summaries so the ST default says `2 s`, not `6 s`. Update the README configuration table with all solver, ST, web, Painter and direction-confirmation fields. - -- [ ] **Step 4: Run GREEN** - -Run `trajectory-observation`. Expected: PASS and no assertion still expects the superseded defaults. - -- [ ] **Step 5: Commit settings** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: configure observation visualization" -``` - ---- - -### Task 2: Pure sequential direction-segment tracker - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** - -- Produces: `TrajectoryObservationSegmentPhase`, `TrajectoryObservationSegmentState`, and `TrajectoryObservationSegmentTracker.Update(...)`. -- Consumes: frozen settings, ordered `DirectionSegmentView` list, EM stop tolerance, caller time, measured `VehicleMotionState`, and current published trajectory. - -- [ ] **Step 1: Write failing state-machine checks** - -Build a two-segment forward/reverse fixture with a duplicated world pose at the gear switch. Exercise this exact sequence: - -```csharp -var tracker = new TrajectoryObservationSegmentTracker(segments, settings, stopSpeedTolerance: 0.01d); -Verification.Equal(0, tracker.State.ActiveSegmentIndex, "tracker begins on segment zero"); - -tracker.Update(t0, StateAtSwitch(0d, t0, 1L), GearTerminal(t0)); -Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForStop, tracker.State.Phase, - "first zero sample begins stop hold"); - -tracker.Update(t0.AddSeconds(0.21d), StateAtSwitch(0d, t0.AddSeconds(0.21d), 2L), GearTerminal(t0)); -Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForDirection, tracker.State.Phase, - "continuous stop arms next direction"); - -tracker.Update(t0.AddSeconds(0.25d), StateAtSwitch(-0.03d, t0.AddSeconds(0.25d), 3L), GearTerminal(t0)); -tracker.Update(t0.AddSeconds(0.30d), StateAtSwitch(-0.03d, t0.AddSeconds(0.30d), 4L), GearTerminal(t0)); -TrajectoryObservationSegmentUpdate advanced = tracker.Update( - t0.AddSeconds(0.35d), StateAtSwitch(-0.03d, t0.AddSeconds(0.35d), 5L), GearTerminal(t0)); -Verification.True(advanced.Advanced && tracker.State.ActiveSegmentIndex == 1, - "three stable reverse samples advance exactly one segment"); -``` - -Separate checks prove that wrong sign, a zero sample, a sequence-id repeat, excessive switch distance, a non-gear terminal, and a discontinuous timestamp reset confirmation. A one-segment path reaches `Completed` rather than indexing past the end. - -- [ ] **Step 2: Run RED** - -Run `trajectory-observation`. Expected: compile failure because tracker types do not exist. - -- [ ] **Step 3: Implement the tracker** - -Use these stable phases: - -```csharp -public enum TrajectoryObservationSegmentPhase -{ - Planning, - WaitingForStop, - WaitingForDirection, - Completed, -} -``` - -`Update` first validates strictly increasing `SequenceId` and nondecreasing caller time. It only leaves `Planning` when the current published trajectory has matching segment/direction, `TerminalType.GearSwitch`, and the caller time is at or beyond `trajectory.Metadata.EffectiveAtUtc + trajectory.Points[^1].TimeFromStart`. Use `FrenetProjector` to confirm the measured pose is within the configured tolerance of both the current-segment end and next-segment start. - -The expected speed sign is positive for `Forward` and negative for `Reverse`. A successful update changes the active index once, resets counters/timers, and returns a Chinese transition diagnostic. It must not write to coordinator, browser, UI, or hardware. - -- [ ] **Step 4: Run GREEN** - -Run `trajectory-observation` twice. Expected: PASS both times with deterministic state transitions. - -- [ ] **Step 5: Commit tracker** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: track observed direction segments" -``` - ---- - -### Task 3: Active-segment rolling controller and loop - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** - -- `TrajectoryObservationController.ActiveSegment`, `SegmentState`, `PreviousTrajectoryForVisualization`, and `TryAdvanceSegment(...)` become the single active-segment source. -- `TrajectoryObservationController.CreateEffectiveConfigurationSnapshot()` returns `configuration.Copy()` so adapters never read mutable MovementTest fields or retain the controller's private configuration object. -- `StartCycle` builds a request for `ActiveSegment.SegmentIndex`. -- `TrajectoryObservationLoop.Tick` may call `TryAdvanceSegment` only when no planning task is in flight. - -- [ ] **Step 1: Write failing controller integration checks** - -Use a recording `IEmPlanningService` and two-segment bootstrap: - -```csharp -controller.StartCycle(t0, forwardState, CancellationToken.None).GetAwaiter().GetResult(); -Verification.Equal(0, service.Requests[0].SegmentIndex, "first request uses segment zero"); - -AdvanceTrackerThroughRealStopAndReverse(controller, gearTrajectory, t0); -controller.StartCycle(t0.AddSeconds(1d), reverseState, CancellationToken.None).GetAwaiter().GetResult(); -Verification.Equal(1, service.Requests[1].SegmentIndex, "next request uses segment one"); -Verification.True(service.Requests[1].PreviousTrajectory == null, - "new direction does not reuse old segment trajectory"); -``` - -Also prove two cycles on the same segment retain the exact published previous trajectory reference and ID, and prove an in-flight task prevents a segment reset. - -- [ ] **Step 2: Run RED** - -Expected: checks fail because the controller hardcodes `segmentIndex = 0`. - -- [ ] **Step 3: Refactor controller ownership** - -Store the planning service and make coordinator/executor replaceable per segment: - -```csharp -private readonly IEmPlanningService planningService; -private EmPlanningCoordinator coordinator; -private TrajectoryExecutor executor; -private readonly TrajectoryObservationSegmentTracker segmentTracker; -private EmTrajectory previousTrajectoryForVisualization; -``` - -Add: - -```csharp -internal EmPlannerConfiguration CreateEffectiveConfigurationSnapshot() -{ - return configuration.Copy(); -} -``` - -On a confirmed transition, preserve the old published trajectory only in `previousTrajectoryForVisualization`, then construct a new coordinator and executor. Same-segment `StartCycle` uses `coordinator.PublishedTrajectory`; cross-segment starts with null. Continue monotonic session cycle IDs across coordinator replacement. - -`Observe` supplies the tracker-confirmed current direction to `TrajectoryExecutor`. While waiting at a gear switch, desired direction is the next segment direction and `directionConfirmed=false`; after transition both current and desired are the new direction. - -- [ ] **Step 4: Update the async loop** - -In `Tick`, after consuming a completed task and before starting a new task: - -```csharp -bool segmentAdvanced = planningTask == null && controller.TryAdvanceSegment(now, state); -if (segmentAdvanced) - latestCycle = null; -``` - -Expose `SegmentAdvanced` and the immutable segment state in `TrajectoryObservationLoopTick`. Never cancel a running planner solely to advance a segment. - -- [ ] **Step 5: Run GREEN and existing coordinator/executor regressions** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -``` - -Expected: all three PASS. - -- [ ] **Step 6: Commit controller integration** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: observe EM planning across gear segments" -``` - ---- - -### Task 4: Static world geometry and effective configuration adapter - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** - -- Produces: `PlanningVisualizationStaticSnapshot Build(bootstrap, effectiveConfiguration, settings, obstacleSnapshotVersion)`. -- Static polylines use metres; map source bounds/resolution remain named with their original millimetre units in configuration values. - -- [ ] **Step 1: Write failing static snapshot checks** - -Assert a forward/reverse bootstrap produces every segment and switch marker: - -```csharp -PlanningVisualizationStaticSnapshot snapshot = builder.Build(bootstrap, configuration, settings, 17L); -Verification.Equal(bootstrap.Segments.Count, snapshot.DirectionSegments.Count, - "all direction segments exported"); -Verification.Equal("Forward", snapshot.DirectionSegments[0].Direction, "forward direction exported"); -Verification.Equal("Reverse", snapshot.DirectionSegments[1].Direction, "reverse direction exported"); -Verification.True(snapshot.StaticMarkers.Any(x => x.Kind == "gear-switch"), - "gear switch marker exported"); -Verification.Equal((bootstrap.GridMap.Rows * bootstrap.GridMap.Cols + 7) / 8, - Convert.FromBase64String(snapshot.OccupancyGrid.OccupancyBitsBase64).Length, - "occupancy bitset has exact compact length"); -Verification.True(IsOccupiedBitSet(snapshot.OccupancyGrid, occupiedRow, occupiedColumn), - "occupied map cell uses row-major least-significant-bit-first encoding"); -``` - -Flatten configuration entries by raw field name and assert exact values for `TimeHorizonSeconds=2`, `DistanceHorizonMeters=5`, `MaximumOsqpIterations=100000`, all solver tolerances, all LS/ST weights, vehicle fields, map snapshot/resolution/bounds, web settings, and direction-confirmation settings. - -- [ ] **Step 2: Run RED** - -Expected: compile failure because builder does not exist. - -- [ ] **Step 3: Implement static conversion** - -Build configuration groups in this stable order: `调度`, `OSQP`, `车辆与安全`, `纵向限制`, `ST 权重`, `横向限制`, `LS 权重`, `走廊与投影`, `地图`, `可视化与换向确认`. - -Use raw field names exactly as C# properties and invariant values. Encode `PlanningGridMap.IsOccupied(row, column)` into the generic row-major occupancy bitset using bit index `row * map.Cols + column`; do not create one DTO, SVG element, or polyline per occupied cell. Build global coarse and Local G2 polylines once. Direction segments must retain segment index, direction and switch topology; dynamic state decides completed/current/future highlighting. - -- [ ] **Step 4: Run GREEN** - -Run `trajectory-observation`. Expected: PASS with all effective values read from the controller's frozen `EmPlannerConfiguration`, not editable MovementTest fields. - -- [ ] **Step 5: Commit static adapter** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: export EM observation configuration" -``` - ---- - -### Task 5: Kinematic charts, rolling semantics, and handoff evidence - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` - -**Interfaces:** - -- Produces chart IDs `ls`, `st`, `curvature-s`, `curvature-t`, `velocity-t`, `acceleration-t`, `jerk-t`, `yaw-rate-t`. -- Produces `TrajectoryObservationHandoffMetrics` with availability, `DeltaPositionMeters`, `DeltaReferenceSMeters`, `DeltaVelocityMetersPerSecond`, and `DeltaAccelerationMetersPerSecondSquared`. -- Produces one `PlanningVisualizationDynamicSnapshot` per accepted publication tick. - -- [ ] **Step 1: Write failing jerk and chart checks** - -For a 21-point rolling trajectory: - -```csharp -IReadOnlyList charts = chartBuilder.Build(trajectory, segment, configuration); -VisualizationChart jerk = FindChart(charts, "jerk-t"); -Verification.Equal(20, jerk.Series[0].Points.Count, "jerk has one real sample per interval"); -Verification.True(jerk.NoteChinese.Contains("末点后无时间区间"), "jerk explains terminal interval"); -Verification.Equal(21, FindChart(charts, "velocity-t").Series[0].Points.Count, - "velocity remains knot based"); -Verification.Equal("ReferenceS (m)", FindChart(charts, "ls").XAxisLabel, - "LS uses shared segment station"); -Verification.Equal("PathS (m)", FindChart(charts, "st").YAxisLabel, - "ST uses local planned PathS"); -``` - -Assert acceleration, jerk, curvature and signed-speed limit series are present with `VisualizationLineStyle.Limit`. Do not invent a yaw-rate limit: the effective configuration has no independent maximum yaw-rate field. - -- [ ] **Step 2: Write failing rolling/exact-stop semantic checks** - -Build dynamic snapshots for `RollingContinuation` and `ExactStopAtBoundary`. Assert Chinese status values respectively contain `滚动末端停车硬约束:未启用` and `精确停车锚点`, and that the displayed terminal speed/acceleration equal the actual final point. - -- [ ] **Step 3: Write failing handoff coordinate checks** - -Create two trajectories whose local `PathS` both start at zero but whose world points project to different positions on the same full segment. Align at `current.Metadata.EffectiveAtUtc`; assert: - -```csharp -Verification.NearlyEqual(expectedWorldDelta, metrics.DeltaPositionMeters, "handoff world delta"); -Verification.NearlyEqual(expectedReferenceDelta, metrics.DeltaReferenceSMeters, - "handoff compares shared ReferenceS"); -Verification.True(expectedReferenceDelta != current.Points[0].PathS - previous.Points[0].PathS, - "handoff does not compare local PathS origins"); -``` - -Projection failure must return unavailable `DeltaReferenceS` while preserving world/velocity/acceleration deltas. - -- [ ] **Step 4: Run RED** - -Expected: compilation failure because builders and metrics do not exist. - -- [ ] **Step 5: Implement chart and handoff builders** - -For jerk, iterate only while `index + 1 < trajectory.Points.Count` and use the interval start time/value. For LS, project every world pose to the complete active `DirectionSegmentView` and use `segment.SourceStartArcLength + projection.ReferenceS`. For ST, retain `point.TimeFromStart` and the plan-local `point.PathS`. - -Use `TrajectorySampler.TrySample` at: - -```csharp -double previousTime = (current.Metadata.EffectiveAtUtc - previous.Metadata.EffectiveAtUtc).TotalSeconds; -``` - -Compare the sampled previous point with current point zero. Project both to the same full segment for `DeltaReferenceS`; never subtract local `PathS`. - -- [ ] **Step 6: Implement dynamic snapshot composition** - -Dynamic world overlays contain real pose, current trajectory, previous trajectory, active-segment highlight and current projected horizon. Status values include active segment/direction, planning status, mode, terminal type, elapsed, trajectory age, projection failures, rolling constraint semantics, terminal `v/a`, handoff deltas, and unmet gear-confirmation condition. Cycle summary contains no full point arrays. - -- [ ] **Step 7: Run GREEN** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -``` - -Expected: both commands PASS; the existing rolling service regression still has 21 knots and nonzero rolling terminal speed. - -- [ ] **Step 8: Commit the visualization adapter** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs -git commit -m "feat: visualize EM rolling kinematics" -``` - ---- - -### Task 6: MovementTest web lifecycle and optional Painter fallback - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` - -**Interfaces:** - -- `TrajectoryObservationVisualizationPublisher.Start(...)` creates the generic session and returns the URI. -- Internal `ITrajectoryObservationVisualizationSink` defines `Start`, `Publish`, and `Stop`; the production adapter wraps `PlanningVisualizationSession`, while tests inject a throwing sink without mocking the library. -- `TryPublish(...)` enforces the configured cadence and fuses on the first adapter/server fault. -- `Stop()` is idempotent. -- Native Painter objects are constructed only when `EnableNativePainterVisualization=true`. - -- [ ] **Step 1: Write failing lifecycle/isolation checks** - -Add a fake visualization sink whose `Publish` throws. Verify the wrapper logs/disables once, later calls are no-ops, and the `TrajectoryObservationLoop` continues producing ticks. Add a source audit asserting the MovementTest has no static eager `new TrajectoryObservationPresentation()` and conditionally creates it only under the native Painter flag. - -Add a cadence check: observer ticks at 20 Hz with web refresh at 10 Hz produce no more than 11 snapshots over one second, including the initial snapshot. - -- [ ] **Step 2: Run RED** - -Expected: failures because the publisher does not exist and Painter creation is eager. - -- [ ] **Step 3: Implement publisher cadence and fuse** - -Use this internal seam and facade surface: - -```csharp -internal interface ITrajectoryObservationVisualizationSink -{ - PlanningVisualizationSessionInfo Start(PlanningVisualizationOptions options, - PlanningVisualizationStaticSnapshot snapshot); - void Publish(PlanningVisualizationDynamicSnapshot snapshot); - void Stop(); -} - -internal sealed class TrajectoryObservationVisualizationPublisher -{ - internal TrajectoryObservationVisualizationPublisher(TrajectoryObservationSettings settings, - ITrajectoryObservationVisualizationSink sink, Action log); - internal PlanningVisualizationSessionInfo Start(PlanningVisualizationStaticSnapshot snapshot); - internal bool TryPublish(DateTimeOffset now, - Func snapshotFactory); - internal void Stop(); - internal string FaultReason { get; } -} -``` - -The production sink constructs and disposes one `PlanningVisualizationSession`; the fake sink records calls or throws. - -The publisher stores `nextPublishAtUtc`. When enabled and healthy: - -```csharp -if (now < nextPublishAtUtc) return false; -nextPublishAtUtc = now + TimeSpan.FromSeconds(1d / settings.WebRefreshRateHz); -session.Publish(dynamicBuilder.Build(...)); -return true; -``` - -Catch any exception from build/start/publish, call `session.Stop()`, set one immutable Chinese fault reason, invoke the supplied log callback once, and return false thereafter. - -- [ ] **Step 4: Integrate successful bootstrap and browser launch** - -After bootstrap succeeds and before the loop begins, build the static snapshot and start the web session only when enabled. Log the full tokenized URI. If auto-open is enabled, call: - -```csharp -Process.Start(new ProcessStartInfo -{ - FileName = sessionInfo.Uri.AbsoluteUri, - UseShellExecute = true, -}); -``` - -Catch browser-launch exceptions separately and keep the web session running. - -- [ ] **Step 5: Make Painter lazy and conditional** - -Remove static eager `Presentation`. Create a session-local presentation only when enabled, pass null otherwise, and guard all `DrawWorld/DrawLs/DrawSt/ClearAll` calls. Stopping or replacing a session stops its web publisher and clears only an existing Painter. Bootstrap failure still logs through UI/console even when both visualization modes are off. - -- [ ] **Step 6: Publish dynamic snapshots from real ticks** - -Use `controller.ActiveSegment` for LS/ST building and world highlight. Pass the completed cycle, elapsed time, in-flight flag, segment state, current/previous trajectories, live state and current sample into `TryPublish`. Do not serialize or wait in the observer loop. - -- [ ] **Step 7: Run GREEN and source audit** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -``` - -Expected: check PASS and `rg` finds no actuator call in runtime observer sources. - -- [ ] **Step 8: Commit MovementTest lifecycle** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -git commit -m "feat: host EM observation dashboard" -``` - ---- - -### Task 7: Plugin packaging and operator documentation - -**Files:** - -- Modify: `ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` - -**Interfaces:** - -- Plugin output adds `plugins/TrajectoryPlanningVisualization.dll` beside `ClumsyPilot.dll`. -- The publisher resolves the visualization DLL only as a sibling of the explicit `ManagedDll`; it does not search `PATH` or current directory. - -- [ ] **Step 1: Write failing package-tree check** - -Change the expected sorted tree to: - -```text -plugins/ClumsyPilot.dll| -plugins/TrajectoryPlanningVisualization.dll| -plugins/licenses/OSQP-LICENSE.txt| -plugins/licenses/OSQP-NOTICE.txt| -plugins/licenses/OSQP-VERSION.txt| -plugins/osqp.dll -``` - -Also use `AssemblyName.GetAssemblyName` to verify the new file is a managed assembly named `TrajectoryPlanningVisualization`. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package -``` - -Expected: failure because the publish script omits the visualization DLL. - -- [ ] **Step 3: Update the atomic publisher** - -Resolve: - -```powershell -$visualizationDll = Join-Path ([System.IO.Path]::GetDirectoryName($managedDllPath)) 'TrajectoryPlanningVisualization.dll' -if (-not [System.IO.File]::Exists($visualizationDll)) { - throw "Visualization DLL does not exist beside managed DLL: $visualizationDll" -} -``` - -Copy it into staging before the atomic directory swap. Keep all existing drive-root/workspace-root protections, OSQP hash validation and recovery behavior unchanged. - -- [ ] **Step 4: Run package GREEN** - -Run `plugin-package` twice. Expected: PASS both times and no stale staging/backup directories. - -- [ ] **Step 5: Update both READMEs** - -The MovementTest README must document: - -- how to enable web/Painter modes; -- localhost/token URL behavior; -- Chinese scientific chart conventions; -- `ReferenceS` versus local `PathS`; -- jerk interval semantics; -- rolling/approach/exact-stop labels; -- active/completed/future segment colors; -- direction-confirmation conditions; -- browser closure and TestStop behavior. - -The EM README plugin tree must include `TrajectoryPlanningVisualization.dll` and link to the observation README. - -- [ ] **Step 6: Commit packaging and docs** - -```powershell -git add -- ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1 ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md -git commit -m "docs: package EM observation dashboard" -``` - ---- - -### Task 8: Full automated and manual acceptance - -**Files:** - -- No planned source changes. If a command fails, return to the owning task, add a focused failing regression there, implement the minimal correction, and rerun this acceptance task from Step 1. - -**Interfaces:** - -- Consumes: both verification hosts, the packaged plugin output, the deterministic smoke mode, and the deployed MovementTest entry. -- Produces: fresh automated evidence plus an explicit completed-or-pending vehicle checklist; it does not introduce a new runtime API. - -- [ ] **Step 1: Run the complete automated suite** - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -git diff --check -``` - -Expected: visualization host PASS; every `em-all` component PASS; main build exits `0`; no new whitespace errors. Record any pre-existing warnings separately rather than claiming they were introduced here. - -- [ ] **Step 2: Run a local dashboard smoke session** - -Run the deterministic smoke mode delivered by Plan 1: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 30 -``` - -Open the printed tokenized URI during the 30-second window and verify: - -- Chinese titles/status and English/scientific axes; -- thin white-background scientific plots; -- all four tabs and all required charts; -- active segment/world horizon highlight; -- effective configuration values; -- stale-state display after sample publication stops; -- port release after the host exits. - -This smoke command must publish synthetic snapshots only and must not reference MDCS or hardware. - -- [ ] **Step 3: Perform the deployed vehicle observation checklist** - -In a supervised safe environment: - -1. Enable web and leave native Painter disabled. -2. Confirm `OBSERVE_ONLY` appears in UI/console/page. -3. Observe rolling, approach and exact-stop modes; confirm no forced rolling parking label. -4. Confirm jerk has `N-1` interval samples and no point-21 successor interval or `JerkLimitExceeded`. -5. Confirm current/previous handoff uses `Δposition/ΔReferenceS/Δv/Δa`. -6. At a real gear switch, confirm stop hold and three signed-speed samples precede `N -> N+1` highlight. -7. Close the browser and confirm planning cadence continues. -8. Stop MovementTest and confirm server/port/Painter cleanup and no actuator output. - -- [ ] **Step 4: Commit only acceptance-driven fixes** - -If Step 1–3 required code changes, commit each regression and fix with exact file paths. If no changes were required, do not create an empty commit. - -Plan 2 is complete only after automated evidence is fresh and the vehicle-only checklist is explicitly reported as completed or, if the vehicle is unavailable, explicitly reported as pending rather than silently treated as passed. diff --git a/docs/superpowers/plans/2026-08-05-movementtest-osqp-iteration-limit.md b/docs/superpowers/plans/2026-08-05-movementtest-osqp-iteration-limit.md deleted file mode 100644 index c11b686..0000000 --- a/docs/superpowers/plans/2026-08-05-movementtest-osqp-iteration-limit.md +++ /dev/null @@ -1,110 +0,0 @@ -# MovementTest OSQP Iteration Limit Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. - -**Goal:** Expose a 12000-iteration OSQP cap for observation-only MovementTest sessions. - -**Architecture:** TrajectoryObservationSettings owns an integer iteration limit, snapshots and validates it. The public MovementTest field maps into those settings, and the observation controller maps it into the session-local EM solver configuration. Production EM defaults remain unchanged. - -**Tech Stack:** C# 10, .NET, EMPlannerVerificationHost. - -## Global Constraints - -- Default MaximumOsqpIterations is exactly 12000. -- SolverTimeoutSeconds remains at its existing 0.50d MovementTest default. -- AbsoluteTolerance, RelativeTolerance, and StrictResidualTolerance remain 1e-5d. -- Do not change production EM defaults, vehicle/map constraints, or observe-only command behavior. - ---- - -### Task 1: Propagate the test iteration limit into an EM request - -**Files:** - -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs:95-176,384-416 -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs:9-50 -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs:25-71 -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs:198-215 - -**Interfaces:** - -- Consumes: TrajectoryObservationSettings.CreateValidatedSnapshot() and TrajectoryObservationController.StartCycle(...). -- Produces: TrajectoryObservationSettings.MaximumOsqpIterations, copied to EmPlanningRequest.Configuration.Solver.MaximumOsqpIterations. - -- [x] **Step 1: Write the failing test** - -Add assertions proving the public field uses the default and a custom setting reaches the request: - -~~~csharp -Verification.True(source.Contains("public int MaximumOsqpIterations = 12000;"), - "observer MovementTest exposes the test OSQP iteration default"); - -var settings = new TrajectoryObservationSettings -{ - MaximumOsqpIterations = 9000, -}; -// Start one controller cycle. -Verification.Equal(9000, planningService.Requests[0].Configuration.Solver.MaximumOsqpIterations, - "observer configured OSQP iteration limit"); -~~~ - -Assert 0 and negative iteration limits are rejected. - -- [x] **Step 2: Run focused verification to verify it fails** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: FAIL because MaximumOsqpIterations is absent. - -- [x] **Step 3: Write the minimal implementation** - -Add this setting and snapshot mapping: - -~~~csharp -public int MaximumOsqpIterations { get; set; } = 12000; -~~~ - -Validate it with: - -~~~csharp -if (MaximumOsqpIterations <= 0) - throw new ArgumentOutOfRangeException(nameof(MaximumOsqpIterations), "Value must be positive."); -~~~ - -Map it into the MovementTest settings and session configuration: - -~~~csharp -configuration.Solver.MaximumOsqpIterations = settings.MaximumOsqpIterations; -~~~ - -- [x] **Step 4: Run focused verification to verify it passes** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: PASS trajectory-observation. - -- [x] **Step 5: Run full regression and build** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -~~~ - -Expected: all suites pass and the build reports zero errors. - -- [x] **Step 6: Commit implementation** - -~~~powershell -git add ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs -git commit -m "feat: configure MovementTest OSQP iterations" -~~~ diff --git a/docs/superpowers/plans/2026-08-05-movementtest-solver-timeout.md b/docs/superpowers/plans/2026-08-05-movementtest-solver-timeout.md deleted file mode 100644 index dc5edac..0000000 --- a/docs/superpowers/plans/2026-08-05-movementtest-solver-timeout.md +++ /dev/null @@ -1,102 +0,0 @@ -# MovementTest Solver Timeout Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make the observation-only MovementTest expose a 0.50-second default OSQP time budget without relaxing solver accuracy. - -**Architecture:** `TrajectoryObservationSettings` owns the test-facing time-budget value and validates/copies it. The MovementTest maps its public field into those settings, and `TrajectoryObservationController` maps the snapshot into the per-session EM configuration. The production EM defaults remain unchanged. - -**Tech Stack:** C# 10, .NET, existing EMPlannerVerificationHost test host. - -## Global Constraints - -- Default test solver timeout is exactly `0.50d` seconds. -- `AbsoluteTolerance`, `RelativeTolerance`, and `StrictResidualTolerance` remain `1e-5d`. -- Do not change vehicle constraints, map construction, speed limits, or observe-only command behavior. -- Do not alter `EmPlannerConfiguration.CreateDefault()`; the override belongs only to the observation test. - ---- - -### Task 1: Propagate the test solver budget into an EM request - -**Files:** - -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs:122-170,384-405` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs:9-49` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs:25-70` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs:198-214` - -**Interfaces:** - -- Consumes: `TrajectoryObservationSettings.CreateValidatedSnapshot()` and `TrajectoryObservationController.StartCycle(...)`. -- Produces: `TrajectoryObservationSettings.SolverTimeoutSeconds`, copied to `EmPlanningRequest.Configuration.Scheduling.SolverTimeoutSeconds`. - -- [x] **Step 1: Write the failing test** - -In `VerifiesControllerBuildsSegmentZeroRequest`, set a non-default observation setting and assert it reaches the request: - -```csharp -var settings = new TrajectoryObservationSettings -{ - SolverTimeoutSeconds = 0.42d, -}; -// Start one controller cycle. -Verification.NearlyEqual(0.42d, planningService.Requests[0].Configuration.Scheduling.SolverTimeoutSeconds, - "observer configured solver timeout"); -``` - -Also extend `VerifiesSettingsSnapshotAndVehicle` to assert that a default snapshot retains `0.50d`, and extend `RejectsInvalidSettings` with `settings => settings.SolverTimeoutSeconds = 0d`. - -- [x] **Step 2: Run the focused verification to verify it fails** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: FAIL because `SolverTimeoutSeconds` does not yet exist or its value is not propagated to the request. - -- [x] **Step 3: Write the minimal implementation** - -Add and copy/validate the settings property: - -```csharp -public double SolverTimeoutSeconds { get; set; } = 0.50d; -``` - -Map it from the public MovementTest field, then set the session-local EM configuration: - -```csharp -configuration.Scheduling.SolverTimeoutSeconds = settings.SolverTimeoutSeconds; -``` - -Do not alter the EM default configuration or any tolerance property. - -- [x] **Step 4: Run focused verification to verify it passes** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: all trajectory-observation checks pass, including timeout propagation and invalid-setting rejection. - -- [x] **Step 5: Run the related EM regression suite** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -``` - -Expected: both commands finish successfully with no errors. - -- [x] **Step 6: Commit the implementation** - -```powershell -git add ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs -git commit -m "feat: configure MovementTest solver timeout" -``` diff --git a/docs/superpowers/plans/2026-08-05-movementtest-st-time-controls.md b/docs/superpowers/plans/2026-08-05-movementtest-st-time-controls.md deleted file mode 100644 index 8c07aab..0000000 --- a/docs/superpowers/plans/2026-08-05-movementtest-st-time-controls.md +++ /dev/null @@ -1,115 +0,0 @@ -# MovementTest ST Time Controls Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. - -**Goal:** Expose configurable ST horizon and trajectory timestamp spacing for observation-only MovementTest sessions. - -**Architecture:** TrajectoryObservationSettings owns TimeHorizonSeconds and OutputTimeStepSeconds, preserves them in a validated snapshot, and enforces a time step no larger than the horizon. Public MovementTest fields feed those settings, then TrajectoryObservationController copies both values into its session-local EM schedule. - -**Tech Stack:** C# 10, .NET, EMPlannerVerificationHost. - -## Global Constraints - -- TimeHorizonSeconds defaults to 6d. -- OutputTimeStepSeconds defaults to 0.10d. -- OutputTimeStepSeconds must be positive and no greater than TimeHorizonSeconds. -- ObserverPeriodSeconds stays 0.05d. -- SolverTimeoutSeconds stays 0.50d, MaximumOsqpIterations stays 12000, and all solver tolerances stay 1e-5d. -- Do not change production EM defaults, vehicle/map constraints, or observe-only command behavior. - ---- - -### Task 1: Propagate ST time controls into an EM request - -**Files:** - -- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs -- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs - -**Interfaces:** - -- Consumes: TrajectoryObservationSettings.CreateValidatedSnapshot() and TrajectoryObservationController.StartCycle(...). -- Produces: TimeHorizonSeconds and OutputTimeStepSeconds in EmPlanningRequest.Configuration.Scheduling. - -- [x] **Step 1: Write the failing test** - -Add assertions for the visible defaults and request propagation: - -~~~csharp -Verification.True(source.Contains("public double TimeHorizonSeconds = 6d;"), - "observer MovementTest exposes the ST time-horizon default"); -Verification.True(source.Contains("public double OutputTimeStepSeconds = 0.10d;"), - "observer MovementTest exposes the ST timestamp-spacing default"); - -var settings = new TrajectoryObservationSettings -{ - TimeHorizonSeconds = 4d, - OutputTimeStepSeconds = 0.20d, -}; -// Start one controller cycle. -Verification.NearlyEqual(4d, request.Configuration.Scheduling.TimeHorizonSeconds, - "observer configured ST time horizon"); -Verification.NearlyEqual(0.20d, request.Configuration.Scheduling.OutputTimeStepSeconds, - "observer configured ST timestamp spacing"); -~~~ - -Assert zero horizon, zero step, and a step larger than the horizon are rejected. - -- [x] **Step 2: Run focused verification to verify it fails** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: FAIL because the ST time-control properties are absent. - -- [x] **Step 3: Write the minimal implementation** - -Add public settings and comments: - -~~~csharp -/// 单次 ST 轨迹覆盖的未来时长,单位 s;不等于观察循环周期。 -public double TimeHorizonSeconds { get; set; } = 6d; - -/// 发布 Trajectory 相邻 TimeFromStart 时间戳的间隔,单位 s;不等于观察循环周期。 -public double OutputTimeStepSeconds { get; set; } = 0.10d; -~~~ - -Validate both values and require OutputTimeStepSeconds to be no larger than TimeHorizonSeconds. Copy them through the MovementTest and use: - -~~~csharp -configuration.Scheduling.TimeHorizonSeconds = settings.TimeHorizonSeconds; -configuration.Scheduling.OutputTimeStepSeconds = settings.OutputTimeStepSeconds; -~~~ - -- [x] **Step 4: Run focused verification to verify it passes** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -~~~ - -Expected: PASS trajectory-observation. - -- [x] **Step 5: Run full regression and build** - -Run: - -~~~powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore -~~~ - -Expected: all suites pass and the build reports zero errors. - -- [x] **Step 6: Commit implementation** - -~~~powershell -git add ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs -git commit -m "feat: configure MovementTest ST time controls" -~~~ diff --git a/docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md b/docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md deleted file mode 100644 index 1432093..0000000 --- a/docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md +++ /dev/null @@ -1,660 +0,0 @@ -# Trajectory Planning Visualization Library Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a reusable `netstandard2.0` library that publishes immutable planning snapshots through a bounded, non-blocking loopback HTTP/SSE service and renders a self-contained Chinese scientific-style dashboard. - -**Architecture:** The library owns generic visualization contracts, an atomic latest-frame exchange, bounded cycle summaries, a minimal `TcpListener` HTTP/1.1 server, embedded web assets, and one `PlanningVisualizationSession` facade. It has no dependency on EMPlanner, MDCS, MovementTest, ClumsyCore, Painter, ASP.NET Core, Node.js, or an external CDN. - -**Tech Stack:** C# 10, `netstandard2.0`, `TcpListener`, Server-Sent Events, Newtonsoft.Json 13.0.4, embedded HTML/CSS/JavaScript, a `net10.0-windows` console verification host. - -## Global Constraints - -- Bind only `IPAddress.Loopback`; never bind `IPAddress.Any`, `0.0.0.0`, a LAN address, or a hostname prefix. -- Accept only HTTP `GET`; cap request headers at `16 KiB` and request reads at `2 s`. -- Require the per-session random token on the page, assets, bootstrap, and event endpoints. -- `Publish` may only perform validation plus `Interlocked.Exchange`; it must not serialize, write a socket, wait for a client, or mutate caller-owned collections. -- Latest dynamic-frame capacity is exactly `1`; full trajectory data exists only in that latest frame. -- Cycle history is bounded by `HistoryCycleLimit` and stores summaries only. -- Default refresh rate is `10 Hz`, default history limit is `60`, default port is `0`, and maximum clients is fixed at `2`. -- Web assets are embedded and self-contained; no CDN, package manager, web build, or runtime file lookup. -- Chinese is used for descriptions and state text; axis variables, SI units, and mathematical symbols retain scientific notation. -- Preserve all unrelated dirty-worktree changes and stage only files named by each task. - ---- - -## File Structure - -| File | Responsibility | -| --- | --- | -| `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` | Standalone class library, Newtonsoft dependency, and (from Task 4 onward) embedded assets. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs` | Points, poses, bounds, key/value data and defensive collection-copy helpers. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` | World polylines, markers, segments and static world snapshot. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` | Charts, axes, series and line-style contracts. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs` | Static snapshot, dynamic snapshot, cycle summary and session status. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs` | Validated transport and retention options. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs` | Atomic capacity-one dynamic-frame exchange. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs` | Service-side deduplicated cycle-summary ring. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs` | Stable camel-case invariant JSON serialization. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs` | Bounded GET request parser. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs` | Per-client capacity-one outbound payload and bounded writer. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` | TCP accept loop, route authorization and SSE clients. | -| `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs` | Public `Start/Publish/Stop` facade and fault isolation. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` | Chinese dashboard structure. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` | Thin-line scientific visual system. | -| `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` | Snapshot consumption, SVG charts, tabs and stale-state handling. | -| `ClumsyPilot/TrajectoryPlanningVisualization/README.md` | Public API, endpoints, resource guarantees and integration example. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` | Independent executable verification host. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` | Adds each check group as its task lands and provides the final timed smoke mode. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` | Deterministic synthetic dashboard sample for browser smoke testing. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs` | Minimal assertion helpers. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` | Contract immutability and validation checks. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs` | Latest-frame, history and JSON checks. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs` | Real loopback HTTP/SSE lifecycle checks. | -| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` | Embedded Chinese scientific dashboard checks. | -| `ClumsyPilot/ClumsyPilot.csproj` | Excludes the new library/test sources from default recursive compilation and references the library project. | - ---- - -### Task 1: Standalone project and immutable visualization contracts - -**Files:** - -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` -- Modify: `ClumsyPilot/ClumsyPilot.csproj` - -**Interfaces:** - -- Produces: `VisualizationPoint`, `VisualizationPose`, `VisualizationBounds`, `VisualizationValue`, `VisualizationConfigurationGroup`, `VisualizationPolyline`, `VisualizationMarker`, `VisualizationDirectionSegment`, `VisualizationOccupancyGrid`, `VisualizationChart`, `VisualizationSeries`, `PlanningVisualizationStaticSnapshot`, `PlanningVisualizationDynamicSnapshot`, and `VisualizationCycleSummary`. -- Collection-bearing constructors must reject null elements and copy source collections into `ReadOnlyCollection`. -- Numeric constructors must reject NaN/infinity. Timestamps must be carried as `DateTimeOffset`. - -- [ ] **Step 1: Create the independent verification host and failing contract checks** - -Add a host that initially calls only the contract group and returns `1` on exceptions: - -```csharp -internal static class Program -{ - private static int Main() - { - try - { - ContractChecks.Run(); - Console.WriteLine("PASS trajectory-planning-visualization"); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine(exception); - return 1; - } - } -} -``` - -Start `ContractChecks.Run()` with a defensive-copy test: - -```csharp -var points = new List { new VisualizationPoint(1d, 2d) }; -var series = new VisualizationSeries("current", "当前轨迹", VisualizationLineStyle.Solid, points); -points[0] = new VisualizationPoint(9d, 9d); -Verification.NearlyEqual(1d, series.Points[0].X, "series copies points"); -Verification.Throws( - () => new VisualizationPoint(double.NaN, 0d), "point rejects NaN"); - -byte[] bits = { 0x01 }; -var grid = new VisualizationOccupancyGrid(new VisualizationBounds(0d, 1d, 0d, 1d), - 0.5d, rows: 2, columns: 2, bits); -bits[0] = 0x00; -Verification.Equal("AQ==", grid.OccupancyBitsBase64, "occupancy grid copies compact bits"); -``` - -The verification host project must reference only the standalone visualization library: - -```xml - - - Exe - net10.0-windows - 10 - - - - - -``` - -- [ ] **Step 2: Run the host to verify RED** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: compilation fails because `TrajectoryPlanningVisualization` contracts do not exist. - -- [ ] **Step 3: Add project boundaries and contract implementations** - -The library project must contain: - -```xml - - - netstandard2.0 - 10 - - - - - -``` - -Update the main project so nested project/test sources are not compiled twice: - -```xml - - - -``` - -Define the stable chart contract exactly as follows: - -```csharp -public enum VisualizationLineStyle { Solid, Dashed, Limit } - -public sealed class VisualizationSeries -{ - public VisualizationSeries(string id, string legend, VisualizationLineStyle lineStyle, - IReadOnlyList points); - public string Id { get; } - public string Legend { get; } - public VisualizationLineStyle LineStyle { get; } - public IReadOnlyList Points { get; } -} - -public sealed class VisualizationChart -{ - public VisualizationChart(string id, string chineseTitle, string xAxisLabel, string yAxisLabel, - IReadOnlyList series, string noteChinese = ""); - public string Id { get; } - public string ChineseTitle { get; } - public string XAxisLabel { get; } - public string YAxisLabel { get; } - public IReadOnlyList Series { get; } - public string NoteChinese { get; } -} -``` - -The two top-level snapshots must expose no setters: - -```csharp -public sealed class PlanningVisualizationStaticSnapshot -{ - public PlanningVisualizationStaticSnapshot(string sessionNameChinese, VisualizationBounds worldBounds, - VisualizationOccupancyGrid occupancyGrid, - IReadOnlyList staticPolylines, - IReadOnlyList staticMarkers, - IReadOnlyList directionSegments, - IReadOnlyList configurationGroups); - public string SessionNameChinese { get; } - public VisualizationBounds WorldBounds { get; } - public VisualizationOccupancyGrid OccupancyGrid { get; } - public IReadOnlyList StaticPolylines { get; } - public IReadOnlyList StaticMarkers { get; } - public IReadOnlyList DirectionSegments { get; } - public IReadOnlyList ConfigurationGroups { get; } -} - -public sealed class PlanningVisualizationDynamicSnapshot -{ - public PlanningVisualizationDynamicSnapshot(long sequence, DateTimeOffset observedAtUtc, - string sessionStateChinese, int activeSegmentIndex, string activeDirection, - VisualizationPose vehiclePose, IReadOnlyList dynamicPolylines, - IReadOnlyList dynamicMarkers, IReadOnlyList charts, - IReadOnlyList statusValues, VisualizationCycleSummary cycleSummary); - public long Sequence { get; } - public DateTimeOffset ObservedAtUtc { get; } - public string SessionStateChinese { get; } - public int ActiveSegmentIndex { get; } - public string ActiveDirection { get; } - public VisualizationPose VehiclePose { get; } - public IReadOnlyList DynamicPolylines { get; } - public IReadOnlyList DynamicMarkers { get; } - public IReadOnlyList Charts { get; } - public IReadOnlyList StatusValues { get; } - public VisualizationCycleSummary CycleSummary { get; } -} -``` - -Use these exact primitive/geometry fields so later adapters do not invent parallel DTOs: - -```text -VisualizationPoint: X, Y -VisualizationPose: X, Y, HeadingRadians -VisualizationBounds: XMin, XMax, YMin, YMax -VisualizationValue: ChineseName, RawName, Value, Unit, Severity -VisualizationConfigurationGroup: ChineseTitle, Entries -VisualizationPolyline: Id, LegendChinese, Kind, LineStyle, Points -VisualizationMarker: Id, Kind, LabelChinese, Position -VisualizationDirectionSegment: SegmentIndex, Direction, StartsAtGearSwitch, EndsAtGearSwitch, Points -VisualizationOccupancyGrid: Bounds, ResolutionMeters, Rows, Columns, OccupancyBitsBase64 -VisualizationCycleSummary: CycleVersion, OccurredAtUtc, Status, Published, - PlanningElapsedMilliseconds, SegmentIndex, Direction, LongitudinalMode, - TerminalType, TerminalVelocity, TerminalAcceleration, FailureReason -``` - -`TerminalVelocity` and `TerminalAcceleration` are nullable doubles; all other numeric fields are non-null. `Severity` is one of `normal`, `notice`, or `failure`, validated as an exact string so the generic library does not depend on an EM enum. - -`VisualizationOccupancyGrid` accepts a row-major bit array of exactly `ceil(Rows * Columns / 8)` bytes, makes a defensive copy, and exposes it as Base64 JSON through `OccupancyBitsBase64`. Bit index `row * Columns + column` uses the least-significant bit first within each byte. A null occupancy grid is allowed for non-map visualizations. - -- [ ] **Step 4: Run the focused contract host GREEN** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -``` - -Expected: both exit `0`; the host prints `PASS trajectory-planning-visualization` and the main build has no duplicate-type errors. - -- [ ] **Step 5: Commit the project and contracts** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost ClumsyPilot/ClumsyPilot.csproj -git commit -m "feat: add planning visualization contracts" -``` - ---- - -### Task 2: Capacity-one frame exchange, bounded history, and JSON - -**Files:** - -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs` - -**Interfaces:** - -- Produces: `PlanningVisualizationOptions.CreateValidatedSnapshot()`, `LatestVisualizationFrameStore.Publish(...)`, `TryReadAfter(...)`, `BoundedCycleHistory.Add(...)`, `Snapshot()`, and `VisualizationJson.Serialize(...)`. -- `VisualizationFrame` contains a monotonically increasing store version and one dynamic snapshot. - -- [ ] **Step 1: Write failing store, history, and JSON checks** - -Create `RuntimeChecks`, add `RuntimeChecks.Run()` immediately after `ContractChecks.Run()` in `Program`, and add these behaviors: - -```csharp -var store = new LatestVisualizationFrameStore(); -store.Publish(Snap(1)); -store.Publish(Snap(2)); -Verification.True(store.TryReadAfter(0, out VisualizationFrame frame), "latest frame exists"); -Verification.Equal(2L, frame.Snapshot.Sequence, "latest frame replaces old frame"); -Verification.True(!store.TryReadAfter(frame.Version, out _), "same frame is not replayed"); - -var history = new BoundedCycleHistory(2); -history.Add(Cycle(1)); history.Add(Cycle(2)); history.Add(Cycle(3)); history.Add(Cycle(3)); -Verification.Equal("2|3", string.Join("|", history.Snapshot().Select(x => x.CycleVersion)), - "history is bounded and deduplicated"); - -string json = VisualizationJson.Serialize(Snap(2)); -Verification.True(json.Contains("\"sessionStateChinese\"") && json.Contains("\"observedAtUtc\""), - "JSON uses stable camel case names"); -``` - -Also verify invalid options: negative port, refresh rate `0`, history `0`, and port `65536` throw. - -- [ ] **Step 2: Run to verify RED** - -Run the visualization host. Expected: compilation fails because runtime types do not exist. - -- [ ] **Step 3: Implement the non-blocking stores and options** - -`LatestVisualizationFrameStore.Publish` must contain no lock or callback: - -```csharp -public void Publish(PlanningVisualizationDynamicSnapshot snapshot) -{ - if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); - long version = Interlocked.Increment(ref nextVersion); - Interlocked.Exchange(ref latest, new VisualizationFrame(version, snapshot)); -} -``` - -`TryReadAfter` reads with `Volatile.Read`, and the history uses a private lock only on the service-consumer thread. `BoundedCycleHistory.Add` ignores a repeated `CycleVersion`, removes from the head while count exceeds capacity, and `Snapshot` returns a fresh read-only copy. - -Use Newtonsoft settings with `CamelCasePropertyNamesContractResolver`, `InvariantCulture`, `DateFormatHandling.IsoDateFormat`, and `Formatting.None`. - -- [ ] **Step 4: Run to verify GREEN** - -Run the visualization host twice. Expected: both runs print the PASS line and never depend on working-directory files. - -- [ ] **Step 5: Commit runtime data handling** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Runtime ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs -git commit -m "feat: add bounded visualization snapshots" -``` - ---- - -### Task 3: Restricted loopback HTTP/SSE server and public facade - -**Files:** - -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs` - -**Interfaces:** - -- Produces: `PlanningVisualizationSession.Start(PlanningVisualizationStaticSnapshot)`, `Publish(PlanningVisualizationDynamicSnapshot)`, `Stop()`, `IsRunning`, and `SessionInfo.Uri`. -- This task produces `/api/bootstrap` and `/api/events`; Task 4 adds `/`, `/app.css`, and `/app.js`. Every available route requires `token=`. -- Statuses: bad token `403`, unknown route `404`, non-GET `405`, oversized/malformed request `400`, third live SSE client `503`. - -- [ ] **Step 1: Write failing real-socket checks** - -Create `ServerChecks`, call it after `RuntimeChecks`, and use `TcpClient` rather than mocks: - -```csharp -using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0 })) -{ - PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); - Verification.Equal("127.0.0.1", info.Uri.Host, "server binds loopback"); - Verification.Equal(403, SendStatus(info.Uri.Port, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"), - "missing token is forbidden"); - Verification.Equal(405, SendStatus(info.Uri.Port, - "POST /?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"), - "POST is rejected"); - Verification.Equal(200, SendStatus(info.Uri.Port, - "GET /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"), - "authorized bootstrap succeeds"); - session.Stop(); -} -Verification.True(CanBindReleasedPort(port), "stop releases port"); -``` - -Also send an unknown authorized route, a malformed request line, and a header larger than `16 KiB`; assert `404`, `400`, and `400`. Hold two authorized SSE connections open and assert a third receives `503`. Read one normal JSON response as bytes and assert its `Content-Length` equals the UTF-8 body length. - -Add a slow SSE client that stops reading, publish 10,000 tiny snapshots on a task, and assert the publish task completes within one second. The assertion is about `Publish`, not socket delivery. - -Before connecting any SSE client, publish one distinct cycle summary, wait two refresh periods for the dispatcher, and repeat for three summaries. Then connect and assert the first event contains all three summaries. This respects the capacity-one latest-frame contract while proving history collection is service-owned and does not depend on an open browser. - -- [ ] **Step 2: Run to verify RED** - -Run the visualization host. Expected: compilation fails because the server and facade do not exist. - -- [ ] **Step 3: Implement bounded request parsing and routing** - -Use `TcpListener(IPAddress.Loopback, validated.Port)`. For port `0`, read the assigned port from `LocalEndpoint`. The request reader must: - -```csharp -const int MaximumHeaderBytes = 16 * 1024; -static readonly TimeSpan RequestReadTimeout = TimeSpan.FromSeconds(2d); -``` - -Read until `\r\n\r\n`, reject a buffer that reaches the cap, split the first line into exactly method/target/version, allow only origin-form paths, and URL-decode only the `token` query value. Never accept a caller-provided filesystem path. - -The server owns one dispatcher task in addition to the accept loop. At `1 / RefreshRateHz`, the dispatcher reads `LatestVisualizationFrameStore.TryReadAfter`, appends a new non-null cycle summary to `BoundedCycleHistory` even when no browser is connected, and serializes only when at least one SSE client exists. It offers the resulting payload to each client's capacity-one outbound slot with `Interlocked.Exchange`. - -Each `SseClientConnection` owns its socket writer. A slow socket can block only its own writer; newer dispatcher payloads overwrite that client's unsent slot. Apply a one-second write timeout and disconnect the client on timeout. The writer emits: - -```text -event: frame -data: {camelCase JSON containing snapshot and bounded history} - -``` - -The SSE response uses `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-cache`, and `Connection: keep-alive`. Normal JSON responses use `application/json; charset=utf-8`; HTML/CSS/JavaScript use explicit UTF-8 content types and byte-accurate `Content-Length`. - -All socket exceptions are caught inside the client task. A server-fatal exception sets `FaultReason`, cancels the server, and never escapes through `Publish`. - -On normal `Stop`, offer one `event: end` payload with Chinese reason `会话已结束` to every client, allow at most `100 ms` for best-effort flush, then close sockets and the listener. Shutdown must never wait indefinitely for a browser. - -- [ ] **Step 4: Implement idempotent facade lifecycle** - -`PlanningVisualizationSession.Start` validates options and creates a 256-bit token using APIs available in `netstandard2.0`: - -```csharp -byte[] tokenBytes = new byte[32]; -using (RandomNumberGenerator random = RandomNumberGenerator.Create()) - random.GetBytes(tokenBytes); -string token = BitConverter.ToString(tokenBytes).Replace("-", string.Empty).ToLowerInvariant(); -``` - -It then starts the server and returns: - -```csharp -new PlanningVisualizationSessionInfo( - new Uri("http://127.0.0.1:" + port + "/?token=" + token), token); -``` - -`Stop` swaps the server field to null under a lifecycle lock, then cancels and disposes outside the lock. `Dispose` calls `Stop`. Calling `Publish` before start or after stop is a no-op; passing null still throws. - -- [ ] **Step 5: Run server checks GREEN** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: exit `0`, PASS line, loopback ports released, no unobserved task exception. - -- [ ] **Step 6: Commit the transport** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs -git commit -m "feat: serve planning snapshots on loopback" -``` - ---- - -### Task 4: Embedded Chinese scientific dashboard - -**Files:** - -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` - -**Interfaces:** - -- Produces: four tabs with stable IDs `overview`, `ls-st`, `kinematics`, `history-config`. -- Required chart IDs: `ls`, `st`, `curvature-s`, `curvature-t`, `velocity-t`, `acceleration-t`, `jerk-t`, and `yaw-rate-t`. -- The client gets `/api/bootstrap` once and consumes `/api/events` through `EventSource`. - -- [ ] **Step 1: Write failing embedded-resource and content checks** - -Create `WebAssetChecks`, call it after `ServerChecks`, read resources through `EmbeddedWebAssets`, and assert: - -```csharp -Verification.Contains(html, "路径总览", "overview Chinese title"); -Verification.Contains(html, "LS / ST", "LS/ST tab"); -Verification.Contains(html, "曲率与运动学", "kinematics tab"); -Verification.Contains(html, "周期历史", "history tab"); -Verification.Contains(html, "生效配置", "configuration panel"); -Verification.Contains(css, "--current-trajectory: #1769aa", "scientific current color"); -Verification.Contains(css, "stroke-width: 1.1", "thin scientific line"); -Verification.Contains(js, "末点后无时间区间", "jerk terminal explanation"); -Verification.Contains(html, "occupancy-grid", "single occupancy canvas exists"); -Verification.Contains(html, "world-overlay", "SVG trajectory overlay exists"); -Verification.Contains(js, "atob", "compact occupancy bitset is decoded in browser"); -Verification.True(!html.Contains("http://") && !html.Contains("https://"), "page has no CDN URL"); -``` - -- [ ] **Step 2: Run to verify RED** - -Run the visualization host. Expected: failure because resources are absent. - -- [ ] **Step 3: Implement page structure and scientific CSS** - -Add the three web files to the library as embedded resources: - -```xml - - - - - -``` - -The page must include: - -```html -

EM 轨迹规划观察台

等待数据
- -
-
-
- - -
-
- - - -
-``` - -The HTML references `/app.css?token=__SESSION_TOKEN__` and `/app.js?token=__SESSION_TOKEN__`. When serving only `index.html`, replace that exact placeholder with the lowercase hexadecimal session token; the token alphabet requires no HTML escaping. `app.js` reads the token from `window.location.search` and appends `encodeURIComponent(token)` to `/api/bootstrap` and `/api/events`. This keeps every route authorized without cookies or custom EventSource headers. - -CSS uses a white canvas, `#20252b` text, `#d9dde1` grid, `#1769aa` current line, `#8d959d` previous dashed line, `#d87918` handoff/gear marker, and `#b42318` only for failures/limits. SVG data lines are `1.1px`; axes are `0.8px`; grids are `0.55px`. - -- [ ] **Step 4: Implement SVG rendering and stale-state behavior** - -`app.js` must: - -- preserve equal X/Y scale in the overhead plot; -- decode the row-major occupancy bitset once and draw occupied cells on one Canvas below the SVG overlay, without one DOM node per grid cell; -- derive plot bounds from finite values and add a 5% pad; -- render series as SVG paths without thick strokes or point-per-sample DOM nodes; -- label axes exactly from snapshot `xAxisLabel/yAxisLabel`; -- render configuration as Chinese name, raw field, invariant value, unit; -- render the active segment and current horizon above faded global segments; -- show “末点后无时间区间” beside `jerk-t` rather than adding a final sample; -- show “数据已过期” when no frame arrives for `2 / refreshRateHz` seconds; -- show “会话已结束” only after the explicit terminal `event: end`; on an ordinary `EventSource` error show “连接中断,正在重连” while the independent stale timer may show “数据已过期”; -- install `window.onerror` and `unhandledrejection` handlers that show “页面绘图异常” and stop only that tab's redraw loop; -- never evaluate HTML from snapshot strings; use `textContent` for labels and values. - -- [ ] **Step 5: Run GREEN and build from a non-repository working directory** - -Run the host from repository root, then: - -```powershell -$visualizationHost = (Resolve-Path 'ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj').Path -Push-Location $env:TEMP -try { - dotnet run --project $visualizationHost -} finally { Pop-Location } -``` - -Expected: both invocations print PASS, proving assets come from the assembly rather than current directory. - -- [ ] **Step 6: Add a deterministic timed smoke mode** - -Add `SampleSnapshotFactory` that creates two direction segments, a gear marker, effective configuration groups and all required charts without referencing EMPlanner. Extend `Program` so no arguments runs checks, while `--smoke-seconds N` starts a session, publishes the sample at 10 Hz for exactly `N` seconds, prints the full URI once, then stops. - -```csharp -if (args.Length == 2 && args[0] == "--smoke-seconds" && - int.TryParse(args[1], out int seconds) && seconds > 0) - return RunSmoke(seconds); -``` - -Verify with: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 1 -``` - -Expected: exit `0`, one `http://127.0.0.1:/?token=` line, and clean shutdown after one second. - -- [ ] **Step 7: Commit the dashboard** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs -git commit -m "feat: add scientific planning dashboard" -``` - ---- - -### Task 5: Library documentation and final standalone verification - -**Files:** - -- Create: `ClumsyPilot/TrajectoryPlanningVisualization/README.md` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` - -**Interfaces:** - -- Documents the exact facade, route/security boundary, performance model, known `localhost` scope, chart conventions, and adapter example. - -- [ ] **Step 1: Add a failing documentation check** - -Assert the README contains `OBSERVE_ONLY`, `127.0.0.1`, `capacity 1`, `60`, `10 Hz`, `j[i]`, `末点后无时间区间`, and the exact `Start/Publish/Stop` example. - -- [ ] **Step 2: Run to verify RED** - -Expected: host fails because README is absent. - -- [ ] **Step 3: Write the README** - -Include this minimal use sequence: - -```csharp -using var visualization = new PlanningVisualizationSession( - new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 10d, HistoryCycleLimit = 60 }); -PlanningVisualizationSessionInfo info = visualization.Start(staticSnapshot); -visualization.Publish(dynamicSnapshot); -visualization.Stop(); -``` - -Explicitly state that callers build all domain-specific charts, the server is read-only, `Publish` drops old frames, and no browser event can flow back into planning. - -- [ ] **Step 4: Run complete standalone verification** - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -git diff --check -``` - -Expected: all commands exit `0`; host prints one PASS line; builds report zero errors; `git diff --check` reports no whitespace errors in task files. - -- [ ] **Step 5: Commit documentation** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/README.md ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs -git commit -m "docs: document planning visualization library" -``` - -Plan 1 is complete when this standalone verification is green. Do not begin EM-specific data conversion before this boundary is reviewed. diff --git a/docs/superpowers/plans/2026-08-06-em-full-direction-segment-staged-execution.md b/docs/superpowers/plans/2026-08-06-em-full-direction-segment-staged-execution.md deleted file mode 100644 index 100a014..0000000 --- a/docs/superpowers/plans/2026-08-06-em-full-direction-segment-staged-execution.md +++ /dev/null @@ -1,588 +0,0 @@ -# EM Full-Direction-Segment Staged Execution Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to execute only the current phase in a fresh window. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Execute the approved full-direction-segment planning and observation repair across nine bounded windows with fresh verification, independent handoffs, and copy-ready next-phase prompts. - -**Architecture:** The functional design and 11-task master implementation plan remain authoritative. This plan groups those tasks into nine phase gates; each window reads only its assigned tasks plus the previous handoff, commits implementation in the master-plan boundaries, then commits a factual handoff separately. The final response generates the next prompt only after the handoff commit exists, so every copied prompt carries real hashes. - -**Tech Stack:** Git on Windows/PowerShell, C#/.NET 8, existing EM/OSQP verification hosts, vanilla HTML/CSS/JavaScript/SVG/Canvas, Node.js 24 with test-only jsdom 29.1.1, Markdown handoffs. - -## Global Constraints - -- Repository root is `D:\Users\Desktop\项目\prakrobot\ParkingRobot` and expected branch is `trajplanner`. -- Functional design: `docs/superpowers/specs/2026-08-06-em-full-direction-segment-visualization-repair-design.md`. -- Master plan: `docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md`. -- Staged-execution design: `docs/superpowers/specs/2026-08-06-em-full-direction-segment-staged-execution-design.md`. -- Every implementation window reads `executing-plans`, `test-driven-development`, and `verification-before-completion`; read `systematic-debugging` only after a real failure or unexpected result. -- Do not re-run brainstorming, redesign approved behavior, expand features, or spawn subagents. -- MovementTest remains `OBSERVE_ONLY`; never add chassis, steering, braking, motor, or gear writes. -- Keep all unrelated dirty and staged work intact. Before editing a dirty target file, record its diff and preserve unrelated hunks. -- Use `apply_patch` for edits and explicit `git add -- ` commands. Never use `git add .` or `git add -A`. -- A phase may not implement work assigned to a subsequent phase. Minimal signature propagation is allowed only when the master plan lists that file in the current task. -- Each phase implementation commit follows the master plan. The handoff is a separate commit named `docs: record EM full-direction phase XX handoff`. -- A completed phase final response includes the real implementation and handoff hashes plus one copy-ready prompt for the next phase. -- A blocked phase creates a `阻塞` handoff and outputs a recovery prompt for the same phase, never a later-phase prompt. -- Phase 9 automated acceptance and supervised vehicle acceptance are separate checkpoints. Unsafe or unavailable vehicle work remains incomplete. - -## Common Phase Start - -Every phase prompt supplies the exact expected previous implementation/handoff hashes. At startup run: - -```powershell -git branch --show-current -git rev-parse HEAD -git log -5 --oneline -git status --short -git diff --cached --name-only -``` - -Then run `git merge-base --is-ancestor` for every hash named by the prompt. Read the four authority documents listed above, the current master-plan Task, and the previous handoff in full. If related code changed after the previous handoff, rerun the affected previous-phase GREEN commands before editing. - -## Common Phase Finish - -Before any success claim or commit: - -1. Run the phase RED and confirm the first failure is the intended missing behavior. -2. Run all phase GREEN/regression commands and read complete output. -3. Run `git diff --check` over the phase range and inspect `git diff --stat` plus `git diff --cached --name-only`. -4. Audit actuator calls whenever MovementTest or observer code is touched. -5. Commit only exact current-phase files. -6. Create the phase handoff with `apply_patch`, recording entry HEAD, implementation hashes, files, interfaces, RED/GREEN evidence, dirty-worktree preservation, warnings, omissions, and next-phase facts. -7. Commit only the handoff file. -8. Use the resulting handoff hash in the final answer’s next-phase prompt. - ---- - -### Task 1: Phase 01 — Planning scope, configuration, and full-segment selection - -**Master-plan scope:** Task 1 and Task 2 only. - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-01.md` - -**Interfaces:** - -- Produces `EmPlanningScope.RollingHorizon` and `FullDirectionSegment`. -- Produces request/metadata scope, desired speeds, adaptive-resource configuration, terminal tolerances, and explicit `NoProgress`, `TerminalPoseMismatch`, and `FullSegmentResourceLimitExceeded` statuses. -- Produces scope-aware `PlanningHorizonSelector.Select` that keeps rolling behavior intact and selects the true direction boundary for full mode. - -- [ ] **Step 1: Execute master Task 1 RED/GREEN/commit** - -Follow every checkbox in master Task 1. Required focused commands: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected GREEN: `PASS foundation` and `PASS em-planning-service`. - -Commit exactly as: - -```powershell -git commit -m "feat: define full-direction EM planning scope" -``` - -- [ ] **Step 2: Execute master Task 2 RED/GREEN/commit** - -Follow every checkbox in master Task 2. Required commands: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected GREEN: `PASS longitudinal-model` and `PASS em-planning-service`; full mode reaches the true Goal/GearSwitch boundary and rolling remains truncated by its configured window. - -Commit exactly as: - -```powershell -git commit -m "feat: select complete EM direction segments" -``` - -- [ ] **Step 3: Run phase regression and handoff** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -git diff --check -``` - -Expected: every `em-core-all` component PASS and no new whitespace errors. - -Create `phase-01.md`, then commit only it: - -```powershell -git add -- docs/superpowers/handoffs/em-full-direction-visualization/phase-01.md -git commit -m "docs: record EM full-direction phase 01 handoff" -``` - -**Exit gate:** Task 1–2 commits and handoff are reachable; no adaptive knot schedule or MovementTest/Web/Painter work exists. - ---- - -### Task 2: Phase 02 — Adaptive full-segment ST schedule - -**Master-plan scope:** Task 3 only. - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-02.md` - -**Interfaces:** - -- Consumes Phase 01 scope/configuration. -- Produces immutable `LongitudinalKnotSchedule` and `FullDirectionSegmentScheduleBuilder.TryBuild`. -- Makes `LongitudinalPlanningInput.KnotSchedule` the sole optimization-knot source while publication remains separately sampled. - -- [ ] **Step 1: Execute master Task 3 RED/GREEN** - -Run the exact model RED before implementation, then all Task 3 changes. Required GREEN: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -Expected: both PASS, including short/long derived duration, curvature/stop breakpoints, knot cap failure, and publication-density independence. - -- [ ] **Step 2: Commit and run regression** - -```powershell -git commit -m "feat: derive adaptive full-segment ST schedule" -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -git diff --check -``` - -Expected: commit contains only Task 3 paths; `em-core-all` PASS. - -- [ ] **Step 3: Write and commit Phase 02 handoff** - -```powershell -git add -- docs/superpowers/handoffs/em-full-direction-visualization/phase-02.md -git commit -m "docs: record EM full-direction phase 02 handoff" -``` - -**Exit gate:** derived `T_end` and adaptive knots are validated; no static-start objective, terminal pose, MovementTest, or visualization work is included. - ---- - -### Task 3: Phase 03 — Static start, no-progress rejection, and terminal publication gates - -**Master-plan scope:** Task 4 and Task 5 only. - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-03.md` - -**Interfaces:** - -- Consumes adaptive reference speed and scope. -- Produces nonzero motion from `v0=0`, `NoProgress`, terminal 3 cm/5° normalized-yaw validation, and publication sample cap. - -- [ ] **Step 1: Execute master Task 4 with real RED evidence** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -Expected RED: the clear 5 m static-start fixture remains all-zero or lacks measurable progress. - -After minimal implementation run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected: all PASS; the explicit all-zero candidate returns `NoProgress` and publishes no trajectory. - -Commit: `fix: accelerate EM trajectories from rest`. - -- [ ] **Step 2: Execute master Task 5 RED/GREEN** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -``` - -Expected RED: terminal world pose is not yet validated. - -After implementation run `trajectory` and `em-planning-service`; expect yaw wrap, 3 cm/5° boundary, sample cap, terminal anchor, and `N-1` jerk checks PASS. - -Commit: `feat: validate EM terminal world pose`. - -- [ ] **Step 3: Run phase regression and handoff** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -git diff --check -``` - -Create/commit `phase-03.md` with message `docs: record EM full-direction phase 03 handoff`. - -**Exit gate:** a full clear segment accelerates from rest and stops at a pose-valid terminal; invalid zero progress/pose/sample count cannot publish. - ---- - -### Task 4: Phase 04 — One-shot MovementTest direction segments - -**Master-plan scope:** Task 6 only. - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-04.md` - -**Interfaces:** - -- Produces `TrajectoryObservationSettings.PlanningScope`, full-mode defaults, and one plan attempt per active segment. -- Preserves stop hold plus three signed-speed samples before N+1 activation. - -- [ ] **Step 1: Run Task 6 RED and implement one-shot state** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected RED: current coordinator cadence starts more than one planning cycle in full scope. - -Implement only Task 6 and rerun the same command. Expected GREEN: one plan for N, observation continues, and one new plan starts only after confirmed N→N+1. - -- [ ] **Step 2: Audit read-only safety and commit** - -```powershell -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -git diff --check -``` - -Expected: no actuator call in observer runtime sources. - -Commit: `feat: observe one full EM direction segment`. - -- [ ] **Step 3: Write and commit Phase 04 handoff** - -Commit `phase-04.md` with `docs: record EM full-direction phase 04 handoff`. - -**Exit gate:** MovementTest is full-scope by explicit setting, plans once per direction segment, retains explicit waiting state, and remains read-only. - ---- - -### Task 5: Phase 05 — Observation snapshot and chart semantics - -**Master-plan scope:** Task 7 only. - -**Files:** - -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-05.md` - -**Interfaces:** - -- Produces `VisualizationChartAnnotation` and distinct coarse/Local G2/active/previous/current kinds. -- Produces real `s_end`, gear/final marker, vehicle/plan-start, correct LS/ST/kinematic axes, and real curvature limit. - -- [ ] **Step 1: Run both Task 7 RED hosts** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected RED: duplicate current horizon, missing annotations, or incorrect curvature-limit semantics. - -- [ ] **Step 2: Implement Task 7 and run GREEN** - -Run both commands again. Expected: both PASS; full mode contains exactly one current trajectory; jerk current series has `N-1` points; LS uses `ReferenceS`, ST uses `PathS`; curvature limit comes from vehicle geometry. - -Commit: `fix: publish distinct EM observation semantics`. - -- [ ] **Step 3: Regression, audit, and handoff** - -Run `trajectory-observation`, the visualization host, actuator `rg`, and `git diff --check`. Commit `phase-05.md` as `docs: record EM full-direction phase 05 handoff`. - -**Exit gate:** immutable snapshots carry all correct semantics before any Web or Painter rendering changes. - ---- - -### Task 6: Phase 06 — DOM/SVG Web rendering repair - -**Master-plan scope:** Task 8 only. - -**Files:** - -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-06.md` - -**Interfaces:** - -- Adds test-only Node/jsdom DOM verification. -- Repairs hidden tabs, empty overview, y ticks/units, stable domains, semantic layer order, vehicle/boundary markers, and thin paper style. - -- [ ] **Step 1: Install locked test dependency and run RED** - -Create the exact Task 8 `package.json`, run: - -```powershell -npm install --package-lock-only --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -``` - -Expected RED: hidden-section CSS, y ticks, semantic classes, or empty-state assertions fail. - -- [ ] **Step 2: Implement Task 8 and run GREEN** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: all DOM tests pass and .NET host prints `PASS trajectory-planning-visualization`. - -Commit: `fix: repair EM observation web charts`. - -- [ ] **Step 3: Verify production assets and handoff** - -Confirm no npm package is copied into plugin output, run `git diff --check`, and commit `phase-06.md` as `docs: record EM full-direction phase 06 handoff`. - -**Exit gate:** existing four-tab page renders correct data/axes/layers with no layout redesign; zoom is not yet implemented. - ---- - -### Task 7: Phase 07 — Per-chart viewport zoom - -**Master-plan scope:** Task 9 only. - -**Files:** - -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-07.md` - -**Interfaces:** - -- Produces per-chart viewport-only box zoom, wheel zoom, reset, and fullscreen. -- Never mutates snapshot arrays or sends planner configuration. - -- [ ] **Step 1: Run frozen-data interaction RED** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -``` - -Expected RED: zoom controls and viewport state are absent. - -- [ ] **Step 2: Implement Task 9 and run GREEN** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: wheel/box/reset/fullscreen tests pass; serialized input snapshot remains byte-identical. - -Commit: `feat: add observation chart viewport zoom`. - -- [ ] **Step 3: Handoff** - -Run `git diff --check`; commit `phase-07.md` as `docs: record EM full-direction phase 07 handoff`. - -**Exit gate:** every chart has independent local navigation and no Native Painter changes. - ---- - -### Task 8: Phase 08 — Native Painter correctness - -**Master-plan scope:** Task 10 only. - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- Handoff: `docs/superpowers/handoffs/em-full-direction-visualization/phase-08.md` - -**Interfaces:** - -- Consumes Phase 05 semantics. -- Produces equal-scale world geometry, correct LS/ST axes, thin vehicle outline/heading ray, and correct `s_end`/gear/final markers. - -- [ ] **Step 1: Run Painter geometry RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected RED: the current pose uses a large `endArrow: true` marker or old axis semantics. - -- [ ] **Step 2: Implement Task 10 and run GREEN** - -Run the same command. Expected: `PASS trajectory-observation`; tests prove the large triangle is absent and x/y use one scale. - -Commit: `fix: correct EM observation painter geometry`. - -- [ ] **Step 3: Safety audit and handoff** - -```powershell -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -git diff --check -``` - -Commit `phase-08.md` as `docs: record EM full-direction phase 08 handoff`. - -**Exit gate:** Native Painter is semantically correct but not redesigned; Web remains primary. - ---- - -### Task 9: Phase 09 — Documentation, automated acceptance, and vehicle continuation - -**Master-plan scope:** Task 11 only. - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/README.md` -- Create: `docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md` -- Create only after supervised execution: `docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md` - -**Interfaces:** - -- Produces operator documentation and fresh automated evidence. -- Produces either final supervised vehicle acceptance or an exact continuation/recovery prompt without false completion. - -- [ ] **Step 1: Update docs and commit** - -Follow master Task 11 Step 1 exactly. Commit only the three READMEs as: - -```powershell -git commit -m "docs: explain full-direction EM observation" -``` - -- [ ] **Step 2: Run the complete automated suite** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -git diff --check -``` - -Expected: Node tests pass; visualization/observation/em-all PASS; build exits 0; no observer actuator calls; no new whitespace errors. - -- [ ] **Step 3: Run deterministic local smoke** - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 60 -``` - -Verify the tokenized page and port cleanup exactly as master Task 11. Record screenshots/log paths. - -- [ ] **Step 4: Commit automated handoff** - -Write `phase-09.md`. If supervision/safety is unavailable, set status `自动验收完成但实车待验` and include every pending vehicle item. Commit only it: - -```powershell -git add -- docs/superpowers/handoffs/em-full-direction-visualization/phase-09.md -git commit -m "docs: record EM full-direction phase 09 handoff" -``` - -Output a full Phase 09 vehicle-continuation prompt using the real handoff hash. - -- [ ] **Step 5: Execute vehicle continuation only when supervised and safe** - -A new window verifies Phase 09 evidence reachability and reruns affected automation if related code changed. It then performs the eight-item vehicle checklist from master Task 11 without writing hardware commands. - -- [ ] **Step 6: Record final vehicle status** - -If all items pass, create `phase-09-vehicle.md` with status `完成` and commit: - -```powershell -git add -- docs/superpowers/handoffs/em-full-direction-visualization/phase-09-vehicle.md -git commit -m "docs: record EM visualization vehicle acceptance" -``` - -If a real regression appears, write status `阻塞`, record the first valid failure/reproduction/safety impact, commit only the same file with message `docs: record blocked EM visualization vehicle acceptance`, and output a Phase 09 vehicle recovery prompt. - -**Exit gate:** only fresh supervised vehicle evidence permits the statement that the overall work is complete. Final completion has no next-phase prompt. - ---- - -## Phase Prompt Construction Rule - -After each handoff commit, construct the next prompt from factual repository state. The prompt must include: - -- absolute repository path and expected branch; -- exact current HEAD, previous implementation hashes, and handoff hash; -- exact required documents and current sections/tasks; -- startup ancestry/status/dirty-worktree checks; -- exact allowed files and forbidden later-phase work; -- exact RED/GREEN/regression/audit commands; -- implementation and handoff commit messages; -- completion output and blocked recovery behavior. - -Never write an unresolved hash variable or depend on chat history. The prompt may instruct the next agent to read a file, but it must say why and identify the exact phase/task. diff --git a/docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md b/docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md deleted file mode 100644 index f1ad436..0000000 --- a/docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md +++ /dev/null @@ -1,1107 +0,0 @@ -# EM Full-Direction-Segment Planning and Visualization Repair Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add one-shot full-direction-segment LS/ST planning that starts from rest, reaches the configured cruise speed when feasible, stops at the true segment boundary with pose tolerances, and repairs the existing paper-style observation UI without adding actuator output. - -**Architecture:** Keep rolling planning intact behind an explicit `RollingHorizon` scope and add `FullDirectionSegment` as a separate request scope. Full-segment planning selects the remaining active direction segment, derives a bounded adaptive optimization schedule from the actual Local G2 `s_end` and jerk-limited speed envelope, then publishes a separately resampled trajectory. Observation snapshots remain immutable; Web and Native Painter consume the same corrected semantics, while a test-only jsdom host verifies actual DOM/SVG output. - -**Tech Stack:** C#/.NET 8, existing EM planner and OSQP abstraction, existing MovementTest observer, embedded HTML/CSS/vanilla JavaScript/SVG/Canvas, Node.js 24 test runtime with `jsdom` 29.1.1 as a development-only DOM dependency. - -## Global Constraints - -- MovementTest remains `OBSERVE_ONLY`; do not add chassis, steering, brake, motor, or gear write calls. -- “Global” means the complete remaining part of one coarse-path direction segment. A trajectory never crosses a forward/reverse boundary. -- `FullDirectionSegment` plans once when a segment becomes active and reuses the frozen result. Runtime rolling refinement and dynamic-obstacle replanning are outside this plan. -- If MovementTest starts in the middle of a segment, plan from the valid Local G2 projection of the startup pose to that segment’s true end boundary. -- Full mode derives `s_end` from actual Local G2 `PathS` and derives `T_end`; `DistanceHorizonMeters` and `TimeHorizonSeconds` do not truncate full mode. -- Forward desired/hard maximum speed is `1.0 m/s`; reverse desired/hard maximum speed is `0.5 m/s`. -- Terminal world-position error must be at most `0.03 m`; normalized yaw error must be at most `5°`. -- Published trajectory sampling is `0.1 s` for MovementTest and is independent of adaptive optimization knots. -- Jerk contains exactly `N-1` interval values for `N` trajectory points; there is no jerk sample after the terminal point. -- Resource exhaustion returns `FullSegmentResourceLimitExceeded`; it never silently truncates or falls back to rolling. -- Preserve the existing four-tab white paper/scientific Web layout, Chinese titles, mathematical symbols, and thin lines. -- Web zoom changes only the browser viewport. It never mutates snapshot data or sends planner settings. -- Native Painter receives coordinate, scale, marker, and semantic corrections only; do not redesign or beautify it. -- Before editing any already-dirty file, record `git diff -- ` and preserve all unrelated user hunks. Stage every task with explicit paths only. -- The approved design is `docs/superpowers/specs/2026-08-06-em-full-direction-segment-visualization-repair-design.md`. - ---- - -## File and Responsibility Map - -**New planner files** - -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs`: explicit rolling versus full-direction-segment request scope. -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs`: immutable optimization-knot schedule, separate from publication sampling. -- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs`: derives `T_end`, adaptive knots, and reference progress/speed from Local G2 `PathS` and the speed/stopping envelope. - -**New Web-test files** - -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json`: test-only Node package with exact jsdom dependency. -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json`: locked dependency graph generated by npm. -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs`: actual DOM/SVG regression tests. - -**Existing files with focused changes** - -- Configuration/contracts: `EmPlannerConfiguration.cs`, `SchedulingConfiguration.cs`, `LongitudinalConfiguration.cs`, `ValidationConfiguration.cs`, `EmPlanningRequest.cs`, `EmPlanningStatus.cs`, `EmTrajectoryMetadata.cs`. -- Scope and full planning: `PlanningHorizonSelector.cs`, `EmPlanningRequestValidator.cs`, `EmPlanningService.cs`. -- Longitudinal model: `PathSpeedLimitBuilder.cs`, `LongitudinalPlanningInput.cs`, `SequentialLongitudinalOptimizer.cs`, `LongitudinalObjectiveBuilder.cs`, `LongitudinalConstraintBuilder.cs`, `LongitudinalSolutionValidator.cs`, `LongitudinalPreviousTrajectorySeedBuilder.cs`. -- Publication: `TrajectorySampleSchedule.cs`, `EmTrajectoryAssembler.cs`, `EmTrajectoryValidator.cs`. -- MovementTest: `TrajectoryObservationContracts.cs`, `TrajectoryObservationPipeline.cs`, `MovementTest.TrajectoryObservationTest.cs`. -- Observation semantics: `TrajectoryObservationStaticSnapshotBuilder.cs`, `TrajectoryObservationDynamicSnapshotBuilder.cs`, `TrajectoryObservationKinematicChartBuilder.cs`, `TrajectoryObservationPresentation.cs`. -- Visualization contracts/assets: `VisualizationCharts.cs`, `VisualizationGeometry.cs`, `PlanningVisualizationSnapshots.cs`, `index.html`, `app.css`, `app.js`, `EmbeddedWebAssets.cs`. -- Tests: existing EM and visualization verification hosts plus the new test-only jsdom package. -- Operator docs: MovementTest and visualization READMEs, followed by a new acceptance handoff. - ---- - -### Task 1: Add explicit planning scope and validated configuration - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -**Interfaces:** - -- Produces: `EmPlanningScope.RollingHorizon` and `EmPlanningScope.FullDirectionSegment`. -- Produces: `EmPlanningRequest.PlanningScope` and `EmTrajectoryMetadata.PlanningScope`. -- Produces configuration properties used by Tasks 2-5: - `DesiredForwardSpeedMetersPerSecond`, `DesiredReverseSpeedMetersPerSecond`, - `MaximumOptimizationTimeStepSeconds`, `MaximumOptimizationSpatialStepMeters`, - `MaximumOptimizationKnotCount`, `MaximumPublishedSampleCount`, - `TerminalPositionToleranceMeters`, and `TerminalYawToleranceRadians`. -- Produces status values `NoProgress`, `TerminalPoseMismatch`, and `FullSegmentResourceLimitExceeded`. - -- [ ] **Step 1: Write failing contract/default/copy tests** - -Add checks that construct both request scopes, copy the configuration, and assert the exact defaults: - -```csharp -Verification.Equal(EmPlanningScope.FullDirectionSegment, fullRequest.PlanningScope, - "full request freezes its scope"); -Verification.NearlyEqual(1.0d, defaults.Longitudinal.DesiredForwardSpeedMetersPerSecond, - "forward desired speed"); -Verification.NearlyEqual(0.5d, defaults.Longitudinal.DesiredReverseSpeedMetersPerSecond, - "reverse desired speed"); -Verification.NearlyEqual(1.0d, defaults.Longitudinal.MaximumForwardSpeedMetersPerSecond, - "forward hard limit"); -Verification.NearlyEqual(0.5d, defaults.Longitudinal.MaximumReverseSpeedMetersPerSecond, - "reverse hard limit"); -Verification.NearlyEqual(0.20d, defaults.Scheduling.MaximumOptimizationTimeStepSeconds, - "adaptive maximum dt"); -Verification.NearlyEqual(0.10d, defaults.Scheduling.MaximumOptimizationSpatialStepMeters, - "adaptive maximum ds"); -Verification.Equal(401, defaults.Scheduling.MaximumOptimizationKnotCount, - "optimization knot cap"); -Verification.Equal(5001, defaults.Scheduling.MaximumPublishedSampleCount, - "publication sample cap"); -Verification.NearlyEqual(0.03d, defaults.Validation.TerminalPositionToleranceMeters, - "terminal position tolerance"); -Verification.NearlyEqual(5d * Math.PI / 180d, defaults.Validation.TerminalYawToleranceRadians, - "terminal yaw tolerance"); -``` - -Mutate the source after `Copy()` and verify the copy remains unchanged. Add invalid-value tests for zero/negative knot caps, desired speed above the hard limit, and terminal yaw tolerance above π. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected: compilation fails because the scope, statuses, and configuration members do not exist. - -- [ ] **Step 3: Add the contracts and defaults** - -Create: - -```csharp -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; - -public enum EmPlanningScope -{ - RollingHorizon, - FullDirectionSegment, -} -``` - -Add a required `EmPlanningScope planningScope` constructor parameter and immutable property to `EmPlanningRequest`. Add the same field to `EmTrajectoryMetadata`. Update every constructor call explicitly; do not add an implicit default that could silently change legacy rolling behavior. - -Add the configuration members listed above and copy them in their owning `Copy()` methods. Set the exact defaults from the test. Add the three new failure statuses to `EmPlanningStatus`. - -- [ ] **Step 4: Validate cross-field invariants** - -Extend `TryValidateConfiguration` with exact checks: - -```csharp -if (!Positive(longitudinal.DesiredForwardSpeedMetersPerSecond) || - longitudinal.DesiredForwardSpeedMetersPerSecond > longitudinal.MaximumForwardSpeedMetersPerSecond || - !Positive(longitudinal.DesiredReverseSpeedMetersPerSecond) || - longitudinal.DesiredReverseSpeedMetersPerSecond > longitudinal.MaximumReverseSpeedMetersPerSecond || - !Positive(scheduling.MaximumOptimizationTimeStepSeconds) || - !Positive(scheduling.MaximumOptimizationSpatialStepMeters) || - scheduling.MaximumOptimizationKnotCount < 3 || - scheduling.MaximumPublishedSampleCount < 2 || - !Positive(validation.TerminalPositionToleranceMeters) || - !Positive(validation.TerminalYawToleranceRadians) || - validation.TerminalYawToleranceRadians > Math.PI) -{ - return false; -} -``` - -Validate `request.PlanningScope` with `Enum.IsDefined`. Keep the rolling distance/time sufficiency validation only for `RollingHorizon`; full mode still validates positive legacy values for compatibility but does not use them as truncation inputs. - -- [ ] **Step 5: Run GREEN** - -Run the two commands from Step 2. Expected: `PASS foundation` and `PASS em-planning-service`. - -- [ ] **Step 6: Commit the contract boundary** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs -git commit -m "feat: define full-direction EM planning scope" -``` - ---- - -### Task 2: Select the complete remaining direction segment - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -**Interfaces:** - -- Consumes: `EmPlanningScope` from Task 1. -- Produces: `PlanningHorizonSelector.Select(..., EmPlanningScope planningScope, ...)`. -- Guarantees: full mode returns the real `Goal` or `GearSwitch` boundary with `ExactStopAtBoundary`; rolling mode preserves existing rolling/approach/exact selection. - -- [ ] **Step 1: Write failing scope-selection tests** - -Create a 10 m segment, project the vehicle at segment-local 3 m, set legacy distance horizon to 1 m and time horizon to 2 s, then assert: - -```csharp -EmPlanningStatus status = selector.Select(segment, 3d, 0d, 0d, - EmPlanningScope.FullDirectionSegment, configuration, out PlanningHorizonSelection full, out string failure); -Verification.Equal(EmPlanningStatus.Success, status, "full selection: " + failure); -Verification.NearlyEqual(segment.LengthMeters, full.WindowEndReferenceS, - "full selection reaches the true segment boundary"); -Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, full.LongitudinalMode, - "full selection always plans a stopped boundary"); -Verification.True(full.HasStopBoundary, "full selection retains a real stop boundary"); -``` - -Use the same fixture with `RollingHorizon` and assert the window ends at 4 m. Add a gear-pair fixture and assert segment 0 ends at `GearSwitch` without including segment 1 points. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: compile failure because `Select` does not accept planning scope. - -- [ ] **Step 3: Implement the explicit full branch** - -Change the selector signature and branch after the initial-state/stopping-distance precheck: - -```csharp -if (planningScope == EmPlanningScope.FullDirectionSegment) -{ - EmBoundaryType boundary = segment.EndBoundary.BoundaryType; - if (!IsStopBoundary(boundary)) - { - failureReason = "A full direction segment must end at Goal or GearSwitch."; - return EmPlanningStatus.InvalidReferencePath; - } - selection = new PlanningHorizonSelection(segment.LengthMeters, boundary, - ToTerminalType(boundary), EmLongitudinalMode.ExactStopAtBoundary, - segment.LengthMeters, true); - return EmPlanningStatus.Success; -} -``` - -Leave the existing rolling selection below this branch unchanged. Pass `request.PlanningScope` from `EmPlanningService` and include the scope in metadata and diagnostics. - -- [ ] **Step 4: Verify the service slices from the startup projection to the real boundary** - -Add an end-to-end service test where the initial pose projects into the middle of a direction segment. Assert the first published point is at the projected startup pose, the final point is the segment endpoint, and no point belongs to the next direction segment. - -- [ ] **Step 5: Run GREEN** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected: both commands print PASS. - -- [ ] **Step 6: Commit segment selection** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs -git commit -m "feat: select complete EM direction segments" -``` - ---- - -### Task 3: Derive adaptive full-segment optimization knots - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` - -**Interfaces:** - -- Produces immutable `LongitudinalKnotSchedule` with `KnotTimes`, `ReferencePathS`, `ReferenceSpeedMetersPerSecond`, `TotalDurationSeconds`, and `IsAdaptive`. -- Produces `FullDirectionSegmentScheduleBuilder.TryBuild(...)` returning `EmPlanningStatus.Success` or `FullSegmentResourceLimitExceeded`. -- `LongitudinalPlanningInput.PlanningScope` carries the request scope into the independent validator. -- `LongitudinalPlanningInput.KnotSchedule` becomes the only optimization-knot source. -- Publication still uses `Scheduling.OutputTimeStepSeconds`; no caller treats it as the optimization step in full mode. - -- [ ] **Step 1: Write failing duration, adaptivity, and resource-limit tests** - -Cover these deterministic fixtures: - -```csharp -// A short segment completes before a legacy 10 s horizon. -Verification.True(shortSchedule.TotalDurationSeconds < 10d, "short segment derives its own T_end"); -// A longer segment takes longer without changing DistanceHorizonMeters/TimeHorizonSeconds. -Verification.True(longSchedule.TotalDurationSeconds > shortSchedule.TotalDurationSeconds, - "duration grows from s_end and limits"); -// Curvature transitions and the stop boundary are represented in the adaptive schedule. -Verification.True(longSchedule.KnotTimes.Count <= configuration.Scheduling.MaximumOptimizationKnotCount, - "adaptive schedule respects knot cap"); -Verification.NearlyEqual(path.Points[path.Points.Count - 1].PathS, - longSchedule.ReferencePathS[longSchedule.ReferencePathS.Count - 1], "schedule reaches s_end"); -Verification.NearlyEqual(0d, - longSchedule.ReferenceSpeedMetersPerSecond[longSchedule.ReferenceSpeedMetersPerSecond.Count - 1], - "schedule stops at s_end"); -``` - -Set `MaximumOptimizationKnotCount = 4` on a path that needs more intervals and assert exact status `FullSegmentResourceLimitExceeded`, no schedule, and a diagnostic containing required versus configured knots. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -``` - -Expected: compile failure for the new schedule types. - -- [ ] **Step 3: Implement the immutable schedule contract** - -Use this public shape: - -```csharp -public sealed class LongitudinalKnotSchedule -{ - public LongitudinalKnotSchedule(IReadOnlyList knotTimes, - IReadOnlyList referencePathS, - IReadOnlyList referenceSpeedMetersPerSecond, bool isAdaptive); - - public IReadOnlyList KnotTimes { get; } - public IReadOnlyList ReferencePathS { get; } - public IReadOnlyList ReferenceSpeedMetersPerSecond { get; } - public double TotalDurationSeconds { get; } - public bool IsAdaptive { get; } - - public static LongitudinalKnotSchedule CreateRolling( - double timeHorizonSeconds, double timeStepSeconds); -} -``` - -Validate equal counts, exact zero first time/path, strictly increasing times, nondecreasing PathS, finite nonnegative speed, and exact zero terminal speed for adaptive schedules. - -- [ ] **Step 4: Extract a PathSpeedLimitBuilder overload that does not require knot times** - -Add: - -```csharp -public EmPlanningStatus Build(LateralPath path, TravelDirection direction, - double initialProgressSpeedMetersPerSecond, EmTerminalType terminalType, - EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit, - out string failureReason); -``` - -Keep the existing `Build(LongitudinalPlanningInput, ...)` as a delegating compatibility overload. This allows the service to build the physical PathS envelope before it creates the optimization schedule. - -- [ ] **Step 5: Implement full-segment schedule derivation** - -Use one forward/backward envelope pass over the existing `PathSpeedLimit.PathS` stations: - -1. Start with the hard maximum-speed array. -2. Forward-propagate a jerk-limited reachable speed from `v0/a0`, never exceeding desired speed or the hard PathS envelope. -3. Backward-propagate the jerk-limited stopping envelope to exact `(s_end, v=0, a=0)` using `JerkLimitedStoppingMath` for every remaining-distance check. -4. Intersect forward, backward, curvature, and desired-speed limits. -5. Integrate `dt = 2*ds/(v0+v1)` on nonzero-speed intervals; use the jerk-limited acceleration/deceleration phase duration when either endpoint speed is zero. -6. Add exact breakpoints for startup, speed-limit changes, brake onset, terminal stop, and the zero-speed hold. -7. Refine any interval exceeding either configured maximum `dt` or maximum `ds`. -8. If refinement needs more than `MaximumOptimizationKnotCount`, return `FullSegmentResourceLimitExceeded`. - -The builder signature is: - -```csharp -public EmPlanningStatus TryBuild(LateralPath path, PathSpeedLimit speedLimit, - double initialProgressSpeedMetersPerSecond, - double initialAccelerationMetersPerSecondSquared, - double desiredSpeedMetersPerSecond, EmPlannerConfiguration configuration, - out LongitudinalKnotSchedule schedule, out string failureReason); -``` - -Do not clamp a result to `s_end` after integration. A schedule is accepted only if its final reference state is exactly `s_end/0/0` within configured kinematic tolerance. - -- [ ] **Step 6: Make all ST components consume `input.KnotSchedule`** - -Add both `EmPlanningScope planningScope` and the schedule to `LongitudinalPlanningInput`, copy them into immutable `PlanningScope` and `KnotSchedule` properties, and reject an adaptive schedule supplied to rolling scope or a rolling schedule supplied to full scope. Replace every direct call to: - -```csharp -LongitudinalCandidate.CreateKnotTimes( - input.Configuration.Scheduling.TimeHorizonSeconds, - input.Configuration.Scheduling.OutputTimeStepSeconds) -``` - -with: - -```csharp -input.KnotSchedule.KnotTimes -``` - -This includes constraints, initial iterate creation, exact-stop stabilization, previous-trajectory resampling, and solution validation. The rolling service path creates `LongitudinalKnotSchedule.CreateRolling(...)`; the full path calls `FullDirectionSegmentScheduleBuilder`. - -- [ ] **Step 7: Run GREEN and prove publication sampling is independent** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -Expected: both PASS. Add one assertion that changing `OutputTimeStepSeconds` from 0.1 to 0.05 doubles publication density but leaves `KnotSchedule.KnotTimes.Count` unchanged. - -- [ ] **Step 8: Commit adaptive scheduling** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs -git commit -m "feat: derive adaptive full-segment ST schedule" -``` - ---- - -### Task 4: Fix static-start progress and reject false success - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningResult.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -**Interfaces:** - -- Consumes: desired speeds and adaptive reference profile from Tasks 1 and 3. -- Produces: a nonzero progress seed from `v0 = 0` when the segment is feasible. -- Produces: exact `NoProgress` failure with no candidate/trajectory publication. - -- [ ] **Step 1: Write the failing static-start regression** - -Use a clear 5 m forward segment, `v0 = 0`, `a0 = 0`, desired/max speed 1.0 m/s, and full scope. Assert: - -```csharp -Verification.True(result.Status == EmPlanningStatus.Success || - result.Status == EmPlanningStatus.SuccessWithFallback, "static start succeeds"); -Verification.True(result.Trajectory.Points.Any(point => point.PathS > 0.05d), - "static start makes measurable progress"); -Verification.True(result.Trajectory.Points.Any(point => point.SignedLongitudinalVelocity > 0.05d), - "static start accelerates"); -Verification.NearlyEqual(0d, result.Trajectory.Points[^1].SignedLongitudinalVelocity, - "full segment stops at the terminal boundary"); -``` - -Add a direct validator fixture containing all-zero `S/U/A/J` over a nonzero full segment. Expect `NoProgress`. Add allowed zero-progress fixtures for “already within terminal tolerance” and gear-switch stop hold. - -- [ ] **Step 2: Run RED against the original symptom** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -Expected: the static-start assertion fails because the current envelope seed targets the initial zero speed. - -- [ ] **Step 3: Drive the seed and objective toward the feasible reference speed** - -In `CreateEnvelopeSeed`, replace the current target: - -```csharp -double targetSpeed = Math.Min(input.InitialProgressSpeedMetersPerSecond, speedLimitAtS); -``` - -with: - -```csharp -double desiredSpeed = input.Direction == TravelDirection.Forward - ? configuration.DesiredForwardSpeedMetersPerSecond - : configuration.DesiredReverseSpeedMetersPerSecond; -double scheduleSpeed = input.KnotSchedule.ReferenceSpeedMetersPerSecond[index]; -double targetSpeed = Math.Min(desiredSpeed, Math.Min(scheduleSpeed, speedLimitAtS)); -``` - -In `LongitudinalObjectiveBuilder`, track the same reference rather than only the current linearization-point hard limit. Preserve acceleration, jerk, previous-trajectory, and exact-stop terms. - -- [ ] **Step 4: Add the independent no-progress gate** - -After physical/dynamics checks and before returning a canonical candidate, reject full-scope output when: - -```csharp -bool requiresProgress = input.PathUpperBoundS > - input.Configuration.Validation.TerminalPositionToleranceMeters; -double achievedProgress = candidate.S[candidate.S.Count - 1] - candidate.S[0]; -if (input.PlanningScope == EmPlanningScope.FullDirectionSegment && - requiresProgress && achievedProgress <= input.Configuration.Validation.SpatialToleranceMeters) -{ - failureReason = "NoProgress: a nonterminal full direction segment produced zero progress."; - failureStatus = EmPlanningStatus.NoProgress; - return false; -} -``` - -Extend the validator/result seam to return a status as well as a reason so `NoProgress` is not collapsed into generic `LongitudinalInfeasible`. - -- [ ] **Step 5: Run GREEN and the service gate** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected: all PASS; the explicit all-zero service fixture has `Trajectory == null` and diagnostic `NoProgress`. - -- [ ] **Step 6: Commit the static-start fix** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningResult.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs -git commit -m "fix: accelerate EM trajectories from rest" -``` - ---- - -### Task 5: Enforce terminal world pose and publication limits - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -**Interfaces:** - -- Produces: `EmTrajectoryValidator.Validate(..., Pose2D terminalPose, ...)`. -- Produces normalized yaw helper with result in `[-π, π]`. -- Produces publication sample-count gate using `MaximumPublishedSampleCount`. -- Maps pose mismatch to `EmPlanningStatus.TerminalPoseMismatch` and sample overflow to `FullSegmentResourceLimitExceeded`. - -- [ ] **Step 1: Write failing position, yaw-wrap, and sample-limit tests** - -Create terminal trajectories at 2.9 cm and 3.1 cm position error; create yaw cases `179°` versus `-179°`, `4.9°`, and `5.1°`. Assert only the outside cases fail. Add a trajectory whose output sampling would need one sample above `MaximumPublishedSampleCount` and assert no partial trajectory is assembled. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -``` - -Expected: compile failure because terminal pose is not part of validation. - -- [ ] **Step 3: Add normalized terminal-pose validation** - -Use the final Local G2 point as the expected world pose and validate the real terminal anchor: - -```csharp -private static double NormalizeAngle(double angle) -{ - while (angle > Math.PI) angle -= 2d * Math.PI; - while (angle < -Math.PI) angle += 2d * Math.PI; - return angle; -} - -double dx = terminal.X - terminalPose.X; -double dy = terminal.Y - terminalPose.Y; -double positionError = Math.Sqrt(dx * dx + dy * dy); -double yawError = Math.Abs(NormalizeAngle(terminal.Yaw - terminalPose.Heading)); -if (positionError > configuration.Validation.TerminalPositionToleranceMeters || - yawError > configuration.Validation.TerminalYawToleranceRadians) -{ - return Reject(EmTrajectoryValidationFailure.TerminalPoseMismatch, terminalIndex, - "TerminalPoseMismatch: position=" + Format(positionError) + - ";yaw=" + Format(yawError)); -} -``` - -Do this only for real `Goal` and `GearSwitchApproach` anchors, not rolling-safety window ends. - -- [ ] **Step 4: Enforce exact publication cardinality** - -Before allocating published samples, compute the required count including the exact terminal anchor and hold samples. If it exceeds `MaximumPublishedSampleCount`, return `FullSegmentResourceLimitExceeded`; do not remove the terminal anchor, increase `OutputTimeStepSeconds`, or truncate. - -Keep jerk interval semantics: the assembler uses interval `i` for point `i` only while `i + 1 < N`; the terminal point stores no successor-interval jerk and the chart builder publishes only `N-1` values. - -- [ ] **Step 5: Run GREEN** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected: both PASS, including yaw wrap and boundary-outside rejection. - -- [ ] **Step 6: Commit publication gates** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs -git commit -m "feat: validate EM terminal world pose" -``` - ---- - -### Task 6: Make MovementTest one-shot per direction segment - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` - -**Interfaces:** - -- Produces: `TrajectoryObservationSettings.PlanningScope`, defaulting to `FullDirectionSegment`. -- Produces: one plan attempt per active segment, reset only after a confirmed segment transition. -- Preserves: stop hold plus three correctly signed speed samples before `N -> N+1`. - -- [ ] **Step 1: Write failing settings and cadence tests** - -Assert the validated snapshot freezes `FullDirectionSegment`, Web defaults on, Native Painter defaults off, and `OutputTimeStepSeconds == 0.1`. Drive 20 observer ticks after one successful full plan and assert planning starts once, while observation and Web publication continue on every configured cadence. Then complete the existing stop-hold/three-sample handshake and assert exactly one new plan starts for segment N+1. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: failures because the current controller delegates to rolling coordinator cadence. - -- [ ] **Step 3: Add scope to settings and request creation** - -Add: - -```csharp -public EmPlanningScope PlanningScope { get; set; } = EmPlanningScope.FullDirectionSegment; -``` - -Copy and validate it. In `TrajectoryObservationController.StartCycle`, pass the scope into `EmPlanningRequest`. Apply the approved observer defaults in the MovementTest entry: - -```csharp -public bool UseFullDirectionSegmentPlanning = true; -public double OutputTimeStepSeconds = 0.10d; -public bool EnableWebVisualization = true; -public bool EnableNativePainterVisualization = false; -``` - -Keep `TimeHorizonSeconds` visible only as a rolling compatibility field and label it accordingly. Do not preserve the experimental 20 s/500 m values as the mechanism for full planning. - -- [ ] **Step 4: Gate one-shot planning in the controller** - -Track `plannedSegmentIndex` and `planAttemptedForActiveSegment` in the controller: - -```csharp -public bool ShouldStartCycle(DateTimeOffset now) -{ - if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment) - return !planAttemptedForActiveSegment; - return coordinator.ShouldStartCycle(now); -} -``` - -Set the attempt flag before launching the task so a slow solver cannot trigger duplicates. Reset it only after `TryAdvanceSegment` confirms the real transition or when a new MovementTest session constructs a new controller. A failed full plan remains failed and visible; no automatic rolling retry occurs. - -- [ ] **Step 5: Keep the existing gear-switch safety state machine unchanged** - -Run existing stop-hold and signed-speed tests plus the new one-shot assertions. The waiting notice must remain explicit and the active highlight must remain on segment N until all conditions pass. - -- [ ] **Step 6: Run GREEN and source audit** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -``` - -Expected: `PASS trajectory-observation`; `rg` returns no actuator call in observer runtime sources. - -- [ ] **Step 7: Commit one-shot observation** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs -git commit -m "feat: observe one full EM direction segment" -``` - ---- - -### Task 7: Publish unambiguous path, chart, and boundary semantics - -**Files:** - -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` - -**Interfaces:** - -- Produces: `VisualizationChartAnnotation` for vertical `s_end` lines and labeled points. -- Produces marker kinds `plan-start`, `gear-switch-end`, and `final-goal`. -- Produces distinct polyline kinds `coarse`, `local-g2`, `active-segment`, `previous`, and `current` with no duplicate `current-horizon` in full mode. -- Chart axes remain `ReferenceS (m)`, `PathS (m)`, `t (s)`, `l (m)`, `v (m/s)`, `a (m/s²)`, `j (m/s³)`, `κ (m⁻¹)`, and `ω (rad/s)`. - -- [ ] **Step 1: Write failing snapshot semantics tests** - -Assert that a full-mode snapshot contains exactly one current trajectory polyline, contains both coarse and Local G2 paths, has distinct `s_end` and vehicle/plan-start markers, and uses the correct terminal kind for gear switch versus final goal. - -For charts assert: - -```csharp -Verification.Equal("ReferenceS (m)", ls.XAxisLabel, "LS uses shared reference S"); -Verification.Equal("PathS (m)", st.YAxisLabel, "ST uses actual path arc length"); -Verification.Equal(trajectory.Points.Count - 1, jerk.Series[0].Points.Count, - "jerk has N-1 interval samples"); -Verification.True(st.Annotations.Any(a => a.Kind == "s-end"), "ST marks s_end"); -Verification.True(curvatureS.Series[0].Points.Any(p => Math.Abs(p.Y) > 1e-6), - "curvature-distance uses vehicle curvature data"); -``` - -Ensure the curvature hard-limit series uses `VehicleParameters.MaximumCurvaturePerMeter`, not `MaximumCurvatureRatePerMeterPerSecond`. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: failures for duplicate current horizon, missing annotations, and incorrect curvature limit. - -- [ ] **Step 3: Add chart annotations without breaking old consumers** - -Add: - -```csharp -public sealed class VisualizationChartAnnotation -{ - public VisualizationChartAnnotation(string id, string kind, string labelChinese, - double x, double? y = null); - public string Id { get; } - public string Kind { get; } - public string LabelChinese { get; } - public double X { get; } - public double? Y { get; } -} -``` - -Extend `VisualizationChart` with immutable `Annotations`. Keep the old constructor as a delegating overload with an empty annotation list for backward compatibility. - -- [ ] **Step 4: Correct snapshot builders** - -In full mode, remove `current-horizon`; in rolling mode publish it only if it is a genuinely different subset. Add current trajectory once above static paths. Add `s_end` annotations to world, LS, and ST. Label a direction boundary as `换向点 N / s_end` only for gear switches; use a distinct final-goal label and marker. - -Extend `TrajectoryObservationDynamicSnapshotBuilder.Build(...)` with an immutable `VehicleParameters vehicle` argument. Pass `bootstrap.Vehicle` from the MovementTest publication callback into the dynamic builder, then into the kinematic chart builder, so the curvature charts use the real vehicle curvature limit. Change yaw-rate axis text to `ω (rad/s)`. - -- [ ] **Step 5: Run GREEN** - -Run both commands from Step 2. Expected: both hosts PASS. - -- [ ] **Step 6: Commit observation semantics** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs -git commit -m "fix: publish distinct EM observation semantics" -``` - ---- - -### Task 8: Add real DOM/SVG tests and repair the existing Web page - -**Files:** - -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json` -- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` - -**Interfaces:** - -- Test-only dependency: exact `jsdom` 29.1.1; no npm package is copied into the production plugin. -- Test hook: when `window.__TRAJECTORY_VISUALIZATION_TEST__ === true`, `app.js` exposes immutable-input render hooks and does not start fetch/SSE. -- Produces actual DOM assertions for tab visibility, y ticks, units, paths, markers, empty state, and scientific line classes. - -- [ ] **Step 1: Add the test package and failing DOM tests** - -Create `package.json`: - -```json -{ - "name": "trajectory-planning-visualization-web-dom-tests", - "private": true, - "type": "module", - "scripts": { "test": "node --test dashboard.dom.test.mjs" }, - "devDependencies": { "jsdom": "29.1.1" }, - "engines": { "node": ">=24" } -} -``` - -Run `npm install --package-lock-only` in that directory to create the lock file. In `dashboard.dom.test.mjs`, load the real `index.html` and `app.js`, create `JSDOM` with `runScripts: "outside-only"` and `pretendToBeVisual: true`, stub Canvas `getContext`, set fixed element rectangles, set the test flag, and evaluate the real script. - -Test these behaviors with actual DOM queries: - -```javascript -assert.equal(document.querySelector("#overview").hidden, false); -click(document.querySelector('button[data-tab="ls-st"]')); -assert.equal(document.querySelector("#overview").hidden, true); -assert.equal(document.querySelector("#ls-st").hidden, false); -assert.equal(document.querySelector("#kinematics").hidden, true); -assert.ok(document.querySelectorAll("#st .axis-tick-y").length >= 4); -assert.equal(document.querySelector("#st .axis-label-y").textContent, "PathS (m)"); -assert.ok(document.querySelector("#world-overlay .world-current")); -assert.ok(document.querySelector("#world-overlay .marker-vehicle")); -``` - -Add an empty-snapshot test expecting a visible Chinese reason instead of an empty world canvas. - -- [ ] **Step 2: Run RED** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -``` - -Expected: failures because hidden sections are overridden by CSS, y tick labels are absent, and semantic classes/empty messages are missing. - -- [ ] **Step 3: Fix tab visibility and empty-state behavior** - -Add the high-specificity CSS rule before panel grids: - -```css -main > section[hidden] { display: none !important; } -#ls-st:not([hidden]), #kinematics:not([hidden]) { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); - gap: 18px; -} -``` - -In `renderWorld`, count valid static/dynamic path points. If none exist, add a `.world-empty-state` text element naming the missing source. Do not fabricate fallback points. - -- [ ] **Step 4: Render y ticks, units, and stable nonzero domains** - -Split tick generation into `niceDomain`, `formatTick`, and `renderAxes`. Create both x and y text nodes: - -```javascript -const yt = svg("text", { - x: left - 7, y, class: "axis-tick axis-tick-y", - "text-anchor": "end", "dominant-baseline": "middle" -}); -yt.textContent = formatTick(domain.y1 - tick * (domain.y1 - domain.y0) / 4); -``` - -Use `.axis-label-x` and `.axis-label-y` classes. For constant data, pad around the actual constant with `max(abs(value)*0.05, unitFloor)`; never force a nonzero constant onto a zero-centered 1-unit range. - -- [ ] **Step 5: Apply semantic path order and thin paper style** - -Dispatch line class by `kind`, not by static/dynamic origin. Draw in this exact order: coarse, Local G2, direction segments, previous, current, annotations/vehicle. Use: - -```css -.world-coarse { stroke: #9aa1a8; stroke-opacity: .48; stroke-dasharray: 5 4; stroke-width: .85; } -.world-local-g2 { stroke: #4f5963; stroke-width: .95; } -.world-segment-active { stroke: #5d91bd; stroke-opacity: .62; stroke-width: 1.0; } -.world-previous { stroke: #8d959d; stroke-dasharray: 5 4; stroke-width: .85; } -.world-current { stroke: #1769aa; stroke-width: 1.15; } -``` - -Render vehicle pose as a small oriented outline plus thin heading line using `frame.vehiclePose.heading`; render plan start, gear-switch `s_end`, and final goal with different marker classes. Do not use a large filled triangle. - -- [ ] **Step 6: Expose safe test hooks and keep production boot unchanged** - -At the end of `app.js`: - -```javascript -if (window.__TRAJECTORY_VISUALIZATION_TEST__ === true) { - window.__trajectoryVisualizationTestHooks = Object.freeze({ - setBootstrap(snapshot) { state.staticSnapshot = snapshot; }, - receiveFrame, - renderActiveTab, - installTabs - }); - installTabs(); -} else { - boot(); -} -``` - -Hooks accept snapshots but never expose mutable internal state. Production still performs tokenized fetch and SSE exactly once. - -- [ ] **Step 7: Run GREEN plus embedded-asset host** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: Node tests report all tests passed; .NET host prints `PASS trajectory-planning-visualization`. - -- [ ] **Step 8: Commit Web repair and DOM tests** - -```powershell -git add -- ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs -git commit -m "fix: repair EM observation web charts" -``` - ---- - -### Task 9: Add per-chart viewport zoom without changing data - -**Files:** - -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` -- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` - -**Interfaces:** - -- Produces per-chart `chartViewports[id]` containing only visible x/y domains. -- Produces actions `box zoom`, `wheel zoom`, `reset`, and `fullscreen`. -- Consumes immutable chart snapshot arrays and never writes to them. - -- [ ] **Step 1: Write failing interaction tests** - -Freeze the input chart and point arrays, dispatch wheel and pointer events, and assert the rendered axis tick labels and path coordinates change while the serialized input remains byte-for-byte identical. Click reset and assert the original tick domain returns. Click fullscreen and assert only the selected `.chart` receives the fullscreen class/API request. - -- [ ] **Step 2: Run RED** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -``` - -Expected: zoom controls and viewport state are missing. - -- [ ] **Step 3: Implement viewport-only controls** - -Add a compact `.chart-tools` toolbar in each chart host. Store domains as copied numbers: - -```javascript -state.chartViewports[id] = { x0, x1, y0, y1 }; -``` - -Wheel zoom uses the pointer’s data coordinate as its anchor. Box zoom ignores drags shorter than 4 CSS pixels. Reset deletes only `state.chartViewports[id]`. Fullscreen uses `requestFullscreen()` with a CSS fallback for the jsdom/test environment. SSE frame arrival re-renders using the active viewport if it is still finite; it does not alter the snapshot. - -- [ ] **Step 4: Run GREEN** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: all DOM tests and embedded-asset checks PASS. - -- [ ] **Step 5: Commit chart navigation** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs -git commit -m "feat: add observation chart viewport zoom" -``` - ---- - -### Task 10: Correct Native Painter geometry and semantics - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` - -**Interfaces:** - -- Consumes the same coarse/Local G2/current/previous/s_end semantics from Task 7. -- Produces small vehicle/pose outlines and thin heading rays with `endArrow: false`. -- LS x-axis is `ReferenceS (m)`; ST is `t (s)` versus `PathS (m)`; all world coordinates remain x/y equal-scale millimetres. - -- [ ] **Step 1: Write failing source/geometry checks** - -Add checks that `DrawPose` no longer calls `endArrow: true`, that pose outlines contain four rotated vehicle corners, and that world paths use one millimetres-per-metre factor for both x and y. Assert LS/ST labels and `s_end`/gear-switch labels match the Web semantics. - -- [ ] **Step 2: Run RED** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: failure on the current large arrow and old `path-S`/`T-S` labels. - -- [ ] **Step 3: Replace the black triangle with scaled pose geometry** - -Draw a four-corner outline based on the observed vehicle length/width and a thin heading ray no longer than half the vehicle length: - -```csharp -worldPainter.DrawLine(color, x, y, headingX, headingY, endArrow: false, width: 1); -``` - -Use thin line widths for coarse, Local G2, current, and previous paths. Mark current vehicle, plan start, gear-switch `s_end`, and final goal distinctly. Keep all existing Painter windows and lifecycle cleanup; do not add new Painter layers. - -- [ ] **Step 4: Correct Painter LS/ST axes** - -Use shared world `ReferenceS` for LS horizontal values and actual trajectory `PathS` for ST vertical values. Calculate plot scales from data bounds with the same x/y unit conversion. Show the explicit waiting notice during gear handoff. - -- [ ] **Step 5: Run GREEN** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: `PASS trajectory-observation`. - -- [ ] **Step 6: Commit Painter correctness** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs -git commit -m "fix: correct EM observation painter geometry" -``` - ---- - -### Task 11: Update operator documentation and run complete acceptance - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/README.md` -- Create after safe execution: `docs/superpowers/handoffs/em-observation-web/phase-08-vehicle.md` - -**Interfaces:** - -- Documents the full-versus-rolling switch, derived `s_end/T_end`, 1.0/0.5 m/s limits, terminal pose tolerances, chart units, zoom, and `OBSERVE_ONLY` safety boundary. -- Produces fresh automated evidence and either a passed or explicitly pending/blocked vehicle checklist. - -- [ ] **Step 1: Update the three READMEs** - -Document these exact operator facts: - -- `FullDirectionSegment` is the MovementTest default and performs one optimization per active direction segment. -- `DistanceHorizonMeters` and `TimeHorizonSeconds` apply only to `RollingHorizon` truncation. -- `s_end` comes from actual Local G2 `PathS`; `T_end` is derived from feasible acceleration/cruise/stopping behavior. -- Forward desired/max is 1.0 m/s; reverse desired/max is 0.5 m/s. -- Successful real boundaries require stop plus 3 cm position and 5° normalized yaw tolerances. -- Web is primary, Native Painter is optional correctness/audit output. -- Each chart’s axes/units, the `N-1` jerk rule, semantic path line styles, boundary markers, and zoom controls. -- Browser closure does not stop observation; MovementTest stop reclaims HTTP/SSE/port/Painter. -- No part of MovementTest writes chassis, steering, brake, motor, or gear commands. - -- [ ] **Step 2: Run all fresh automated verification** - -```powershell -npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -git diff --check -``` - -Expected: Node tests pass; both visualization and observation hosts print PASS; every `em-all` component prints PASS; build exits 0; actuator audit has no observer-runtime matches; `git diff --check` reports no new whitespace errors. Record pre-existing warnings separately. - -- [ ] **Step 3: Run deterministic local Web smoke** - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 60 -``` - -Open the printed tokenized URL. Verify the four tabs, nonblank world overview, nonhorizontal increasing ST for the moving fixture, y ticks/units, thin path comparison, `s_end`/gear/final markers, vehicle pose, chart zoom/reset/fullscreen, stale state, and port release. This smoke uses synthetic snapshots only. - -- [ ] **Step 4: Run the supervised vehicle checklist only when safe** - -In a supervised safe environment, keep MovementTest read-only and verify: - -1. Web enabled, Native Painter disabled by default, and UI/console/page show `OBSERVE_ONLY`. -2. The current full direction segment produces complete LS/ST; static start accelerates, reaches feasible cruise speed, begins braking from the envelope, and stops at the real boundary. -3. ST `PathS` increases; jerk has `N-1` intervals and no terminal successor or false `JerkLimitExceeded`. -4. World handoff displays `DeltaPosition`, shared-direction `DeltaReferenceS`, `DeltaV`, and `DeltaA`, never a direct comparison of unrelated local PathS. -5. At a true reversal, N remains highlighted until continuous stop hold and three correctly signed speed samples pass; waiting text is explicit. -6. Coarse, Local G2, current trajectory, vehicle pose, `s_end`, gear switch, and final goal are visually distinct. -7. Closing/slow-loading the browser does not stop observation. -8. Stopping MovementTest releases HTTP/SSE, port, and any Painter; source/log audit shows no hardware writes. - -If any item cannot be executed safely, record it as not run. Do not claim overall vehicle acceptance. - -- [ ] **Step 5: Write the vehicle handoff from fresh evidence** - -If every vehicle item passes, create `phase-08-vehicle.md` with status “完成”, environment prerequisites, execution time, per-item results, logs/screenshots, warnings, and conclusion. If a regression appears, create the same file with status “阻塞”, the first valid failure, exact reproduction, and safety impact; do not expand the fix in the vehicle session. - -- [ ] **Step 6: Commit docs and, only when available, vehicle evidence separately** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/TrajectoryPlanningVisualization/README.md -git commit -m "docs: explain full-direction EM observation" -``` - -After actual supervised acceptance only: - -```powershell -git add -- docs/superpowers/handoffs/em-observation-web/phase-08-vehicle.md -git commit -m "docs: record EM visualization vehicle acceptance" -``` - -Do not create or commit the vehicle handoff as “完成” from automated or synthetic evidence alone. diff --git a/docs/superpowers/plans/2026-08-06-em-observation-web-staged-execution.md b/docs/superpowers/plans/2026-08-06-em-observation-web-staged-execution.md deleted file mode 100644 index 4696f27..0000000 --- a/docs/superpowers/plans/2026-08-06-em-observation-web-staged-execution.md +++ /dev/null @@ -1,632 +0,0 @@ -# EM Observation Web Visualization Staged Execution Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Execute the approved EM observe-only web visualization work across eight fresh Codex windows with bounded context, reproducible verification, exact Git ownership, and durable handoffs. - -**Architecture:** The code-level work remains defined by the approved library and EM integration plans. This plan is the orchestration layer: each task is one isolated window, consumes the preceding handoff, executes a dependency-aligned slice of the original plans, records evidence, and emits the next task's complete prompt. - -**Tech Stack:** Codex staged sessions, PowerShell, Git, C# 10, `netstandard2.0`, `net10.0-windows` verification hosts, Markdown handoff records. - -## Global Constraints - -- Repository root is `D:\Users\Desktop\项目\prakrobot\ParkingRobot`. -- The approved design commit `b0b79e5` must remain an ancestor of every stage baseline. -- Execute stages strictly in order; do not advance after a blocked or unverified stage. -- Use one primary agent per window and do not spawn subagents. -- Preserve all unrelated dirty-worktree and index changes; never reset, clean, restore, or include them in a stage commit. -- Read only the current stage's named plan sections and source files; locate additional symbols with targeted `rg` before opening files. -- Follow TDD order from the original plan and use systematic debugging for unexpected failures. -- Keep `OBSERVE_ONLY`; never add or call actuator write APIs. -- A success claim requires fresh evidence collected under `verification-before-completion`. -- Each completed stage creates and commits exactly one handoff file under `docs/superpowers/handoffs/em-observation-web/`. -- A blocked stage emits a same-stage recovery prompt, not the next-stage prompt. -- The final response ends with the next prompt copied verbatim from this plan, except after fully completed Stage 8. - -## Exact Original-Task Ownership - -| Stage | Original plan ownership | -| --- | --- | -| 1 | 类库 Task 1–2 | -| 2 | 类库 Task 3 | -| 3 | 类库 Task 4–5 | -| 4 | 集成计划 Task 1–2 | -| 5 | 集成计划 Task 3 | -| 6 | 集成计划 Task 4–5 | -| 7 | 集成计划 Task 6–7 | -| 8 | 集成计划 Task 8 | - ---- -## Shared Session Protocol - -Every stage prompt below already contains the shared protocol. The agent must keep the original task-level commits specified by the code-level plans. After those commits and fresh verification, it creates its handoff file with actual facts under these headings: - -```markdown -# EM 观察网页可视化阶段 NN 交接 - -状态:完成 -阶段目标:本阶段实际完成的目标 -基线提交:启动阶段时记录的提交哈希 -实现提交:本阶段代码和文档提交哈希列表 -修改文件:本阶段实际修改和创建的文件列表 -新增或确认的接口:可供下一阶段依赖的接口和行为 -验证命令与结果:逐条记录命令、退出码和关键 PASS 输出 -未运行的验证及原因:没有未运行项时明确写“无” -已知警告或遗留问题:没有时明确写“无” -与原计划的偏差:没有时明确写“无” -下一阶段注意事项:下一阶段必须核查的事实 -``` - -Handoff commits use `docs: record EM visualization phase NN handoff`. The handoff records implementation commits; it does not need to contain its own commit hash. - -If a required command fails, the stage agent writes the same handoff fields with `状态:阻塞`, does not mark unchecked plan work complete, and returns a newly written recovery prompt that tells a fresh agent to resume the same stage from the recorded failure. It must include the exact failed command, exit code, first actionable error, current commits, relevant files, and remaining verification. - ---- - -### Task 1: Stage 1 — Visualization library foundations - -**Files:** - -- Read: `docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md` -- Read: `docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md` -- Read: `docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md` Task 1–2 -- Create/modify: every exact file listed by library Task 1–2 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-01.md` - -**Interfaces:** - -- Consumes: approved design commit `b0b79e5`, existing `ClumsyPilot.csproj`, Newtonsoft.Json 13.0.4. -- Produces: standalone visualization project, immutable contracts, `PlanningVisualizationOptions`, capacity-one latest frame, bounded cycle history, and stable JSON required by Stage 2. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 1/8:可视化类库基础。只完成本阶段,不要提前实现 HTTP/SSE、网页、EM Adapter 或 MovementTest 集成,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已经批准,不要重新进行 brainstorming。开始前完整阅读并遵守: -1. C:\Users\admin\.codex\skills\executing-plans\SKILL.md -2. C:\Users\admin\.codex\skills\test-driven-development\SKILL.md -3. C:\Users\admin\.codex\skills\verification-before-completion\SKILL.md -遇到任何非预期测试或构建失败时,再完整阅读 C:\Users\admin\.codex\skills\systematic-debugging\SKILL.md,然后先定位根因再修改。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 1–6、10–12 节 -- docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md 的标题、Global Constraints、File Structure、Task 1、Task 2 -- ClumsyPilot/ClumsyPilot.csproj - -启动检查: -- 执行 git branch --show-current、git rev-parse HEAD、git log -5 --oneline。 -- 执行 git merge-base --is-ancestor b0b79e5 HEAD;若退出码非 0,停止并报告基线不包含已批准设计。 -- 用 (git status --porcelain=v1 | Measure-Object).Count 统计全部脏项,但只展开本阶段文件路径的 git status --short 和 git diff。 -- 执行 git diff --cached --name-status。不得重置、取消暂存、覆盖或提交任何既有无关改动。 -- 运行 dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true,记录基线退出码;若失败,先判断是否为本阶段开始前已存在的问题并在交接中保留证据。 - -实施范围是原类库计划 Task 1–2 的所有复选步骤,保持原顺序和 TDD: -- 建立 ClumsyPilot/TrajectoryPlanningVisualization 独立 netstandard2.0 项目; -- 建立独立 net10.0-windows 验证宿主; -- 实现不可变契约、防御性复制、有限数值验证和紧凑占用 bitset; -- 主项目排除嵌套源码并通过 ProjectReference 引用类库; -- 实现 PlanningVisualizationOptions、LatestVisualizationFrameStore、BoundedCycleHistory、VisualizationJson; -- 严格执行原计划中的 RED、GREEN 和两个任务的独立提交。 - -只能修改原计划 Task 1–2 的 Files 清单。若当前阶段文件已有用户修改,先阅读 diff 并兼容;无法安全兼容时停止,不要覆盖。提交必须使用精确路径。若暂存区存在无关内容,使用仅包含当前阶段精确路径的提交方式,并在提交后用 git diff-tree --no-commit-id --name-status -r 实际提交哈希核对文件。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -2. dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj -3. dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -4. 对本阶段实际修改文件运行 git diff --check,并核对每个实现提交的文件树。 - -全部退出验证通过后,创建 docs/superpowers/handoffs/em-observation-web/phase-01.md。按照阶段执行设计第 8 节填写真实基线、实现提交、文件、接口、逐条命令/退出码、警告、偏差和下一阶段注意事项,不得把未运行写成通过。单独提交该文件,提交信息为 docs: record EM visualization phase 01 handoff,并核对该提交只含这一文件。 - -最终回答先用中文报告完成内容、实现/交接提交和新鲜验证证据。然后打开 docs/superpowers/plans/2026-08-06-em-observation-web-staged-execution.md,找到 Task 2 的提示词,并在最终回答最后一个 Markdown 代码块中逐字完整输出,供用户复制到新窗口。若本阶段阻塞,不得输出 Task 2;改为输出信息完整的阶段 1 恢复提示词。 -``` - -- [ ] **Step 2: Accept only a verified Stage 1 handoff** - -Expected: `phase-01.md` says `状态:完成`, names the contract/runtime commits, and records successful visualization-host and main-project builds. - ---- - -### Task 2: Stage 2 — Restricted Loopback HTTP/SSE - -**Files:** - -- Read: Stage 1 handoff and library Task 3 -- Create/modify: every exact file listed by library Task 3 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-02.md` - -**Interfaces:** - -- Consumes: Stage 1 immutable snapshots, options, frame store, history and JSON. -- Produces: authenticated loopback server, SSE connections and `PlanningVisualizationSession` facade required by Stage 3. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 2/8:受限 Loopback HTTP/SSE。只完成本阶段,不要实现网页资源或任何 EM/MovementTest 适配,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已经批准,不要重新进行 brainstorming。完整阅读并使用 executing-plans、test-driven-development、verification-before-completion;路径分别是: -- C:\Users\admin\.codex\skills\executing-plans\SKILL.md -- C:\Users\admin\.codex\skills\test-driven-development\SKILL.md -- C:\Users\admin\.codex\skills\verification-before-completion\SKILL.md -出现非预期失败时完整阅读并使用 C:\Users\admin\.codex\skills\systematic-debugging\SKILL.md。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 3–6、10–12 节 -- docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md 的 Global Constraints、File Structure、Task 3 -- docs/superpowers/handoffs/em-observation-web/phase-01.md -- 阶段 1 新增的 Contracts、Runtime 和 PlanningVisualizationOptions 相关文件;只读取 Task 3 直接依赖的类型 - -启动检查: -- 记录当前分支、HEAD、最近 8 条提交。 -- git merge-base --is-ancestor b0b79e5 HEAD 必须成功。 -- phase-01.md 必须存在且状态为完成;逐一用 git show --stat 核对其中实现提交,并运行独立可视化验证宿主建立入口基线。 -- 统计整个工作区脏项数量,只展开 Task 3 文件范围的状态/diff;检查暂存区并保护无关内容。 - -严格执行原类库计划 Task 3 的全部复选步骤和两个阶段:真实 socket RED 检查先行,再实现。必须落实: -- TcpListener 只绑定 IPAddress.Loopback; -- 只接受受限 HTTP/1.1 GET,16 KiB header 与 2 s 读取上限; -- 所有现有路由要求 256-bit 随机 token; -- 最多两个 SSE 客户端,第三个返回 503; -- Publish 只做验证和 Interlocked.Exchange,不序列化、不等待 socket; -- 单 dispatcher 在无浏览器时仍收集有界周期摘要; -- 每客户端容量一发送槽与 1 s 写超时; -- Stop 最多等待 100 ms 发送 event:end 并可靠释放端口; -- Start/Publish/Stop/Dispose 幂等且服务故障不逃逸到规划发布调用。 - -只能修改 Task 3 的 Files 清单。不得绑定 0.0.0.0、IPAddress.Any 或局域网地址,不得改用 HttpListener、ASP.NET Core、Kestrel 或外部包。按原计划完成 transport 实现提交,提交后核对文件树。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -2. dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj -3. 确认真实 socket 检查覆盖 403/404/405/400/503、慢客户端非阻塞、无客户端历史、正常停止和端口重绑。 -4. 对本阶段修改文件运行 git diff --check,并核对提交内容。 - -全部通过后创建 docs/superpowers/handoffs/em-observation-web/phase-02.md,按阶段设计第 8 节填写真实证据,单独提交为 docs: record EM visualization phase 02 handoff。最终回答先报告结果与提交,再从阶段执行手册 Task 3 复制完整提示词作为最后一个代码块。若阻塞,只输出阶段 2 恢复提示词,不得推进阶段 3。 -``` - -- [ ] **Step 2: Accept only a verified Stage 2 handoff** - -Expected: the handoff proves loopback-only binding, bounded request parsing, non-blocking publication and released ports with real-socket tests. - ---- - -### Task 3: Stage 3 — Chinese scientific dashboard and library closure - -**Files:** - -- Read: Stage 2 handoff and library Task 4–5 -- Create/modify: every exact file listed by library Task 4–5 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-03.md` - -**Interfaces:** - -- Consumes: Stage 2 session, bootstrap and SSE endpoints. -- Produces: embedded Chinese Canvas/SVG dashboard and documented, independently verified reusable library required by EM integration. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 3/8:中文科研风网页与独立类库收口。只完成本阶段,不要开始 EM Adapter 或 MovementTest 集成,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已经批准。完整阅读并使用 executing-plans、test-driven-development、verification-before-completion;发生非预期失败时再使用 systematic-debugging。对应 SKILL.md 均位于 C:\Users\admin\.codex\skills\ 下的同名目录。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 3–6、8、10–12 节 -- docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md 的 Global Constraints、File Structure、Task 4、Task 5 -- docs/superpowers/handoffs/em-observation-web/phase-02.md -- Stage 2 的 PlanningVisualizationSession、LoopbackVisualizationServer 与契约类型;不要阅读无关 EM 源码 - -启动时记录分支、HEAD、最近提交;验证 b0b79e5 为祖先;核对 phase-02.md 状态与提交;运行独立可视化验证宿主作为入口基线。统计全部脏项但只展开本阶段文件范围,保护无关工作区和暂存区内容。 - -严格执行原类库计划 Task 4–5 的全部复选步骤: -- 先写嵌入资源和中文内容 RED 检查; -- 实现程序集内 index.html/app.css/app.js,不使用 CDN、Node.js 或运行时文件查找; -- 鸟瞰图使用单个 Canvas 解码行优先 LSB-first bitset,SVG 只覆盖路径/车辆/标记; -- X/Y 等比例,科研白底和 1.1 px 数据线,中文说明、英文/数学轴与 SI 单位; -- 四个页签和八个固定 chart ID 完整; -- EventSource 普通断线显示重连/过期,只有 event:end 显示会话已结束;前端异常只停止当前页面重绘; -- 快照文本一律 textContent,不执行 HTML;每条路由保留 token; -- 从非仓库工作目录验证嵌入资源; -- 实现 --smoke-seconds N 的确定性合成冒烟模式; -- 写独立类库 README 和文档检查; -- 保留原计划 Task 4 与 Task 5 的独立提交。 - -只能修改 Task 4–5 的 Files 清单。不得把 EM、MDCS、Painter 或 MovementTest 类型引入独立类库或验证宿主。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -2. dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj -3. dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -4. 先 Resolve-Path 得到验证宿主项目绝对路径,再 Push-Location $env:TEMP,从非仓库目录运行该宿主并返回。 -5. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 1 -6. 对本阶段文件运行 git diff --check 并核对提交文件树。 - -全部通过后创建并单独提交 docs/superpowers/handoffs/em-observation-web/phase-03.md,提交信息为 docs: record EM visualization phase 03 handoff。最终回答报告证据后,从本手册 Task 4 逐字复制下一阶段完整提示词作为最后一个代码块。阻塞时只输出阶段 3 恢复提示词。 -``` - -- [ ] **Step 2: Accept only a verified Stage 3 handoff** - -Expected: the reusable library passes from repository and temporary working directories, has embedded assets, and contains no EM/MDCS/Painter dependency. - ---- - -### Task 4: Stage 4 — Observation settings and pure segment tracker - -**Files:** - -- Read: Stage 3 handoff and integration Task 1–2 -- Create/modify: every exact file listed by integration Task 1–2 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-04.md` - -**Interfaces:** - -- Consumes: completed reusable library, current MovementTest observer and EM direction segments. -- Produces: frozen settings and pure sequential segment tracker required by Stage 5. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 4/8:观察参数同步与纯换向段状态机。只完成集成计划 Task 1–2,不要修改活动段控制器、图表适配、网页托管或插件发布,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已经批准。完整阅读并使用 executing-plans、test-driven-development、verification-before-completion;非预期失败时使用 systematic-debugging。技能文件位于 C:\Users\admin\.codex\skills\对应目录。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 1–3、6–7、10–12 节 -- docs/superpowers/plans/2026-08-05-em-observation-web-integration.md 的 Prerequisite、Global Constraints、File Structure、Task 1、Task 2 -- docs/superpowers/handoffs/em-observation-web/phase-03.md -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs 中与 settings/bootstrap/segment 直接相关部分 -- DirectionSegmentView、VehicleMotionState、FrenetProjector、轨迹 metadata/terminal 契约 - -启动检查:记录分支/HEAD/最近提交,验证 b0b79e5 为祖先,核对 phase-03.md 完成及其提交。运行独立可视化验证宿主和 trajectory-observation 入口检查。统计全局脏项但只展开 Task 1–2 路径;特别保护 MovementTest 文件中用户已经设置的 5.0 s、100000、2.0 s,不得回退。 - -严格执行集成计划 Task 1–2 的所有 TDD 步骤和两个独立提交: -- 将 TrajectoryObservationSettings 默认值同步为 SolverTimeoutSeconds=5.0、MaximumOsqpIterations=100000、TimeHorizonSeconds=2.0; -- 新增并冻结网页/Painter 开关、端口、10 Hz、60 轮历史和换向确认参数; -- 网页与 Painter 默认都关闭,网页自动打开默认开启,端口 0 或 1024..65535; -- MovementTest 公共字段一对一映射到冻结 settings; -- 实现纯 TrajectoryObservationSegmentTracker,严格 N 到 N+1; -- 只有匹配段/方向的 GearSwitch 轨迹已到绝对末点、测量位姿同时接近两段连接点、真实速度连续停车保持、随后 3 个严格递增序列的目标符号速度样本才切段; -- 错误方向、零速样本、重复序列、超投影距离、非 GearSwitch、时间不连续必须重置确认; -- 单段结束进入 Completed,不越界;状态机不得写 coordinator、UI、网页或硬件。 - -只能修改 Task 1–2 的 Files 清单。保持 OBSERVE_ONLY,不调用或新增任何底盘/运动写接口。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -2. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -3. rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest,并确认运行观察源码没有调用这些接口;测试中的字符串审计本身需单独解释。 -4. 对阶段文件运行 git diff --check,核对实现提交文件树。 - -全部通过后创建并单独提交 docs/superpowers/handoffs/em-observation-web/phase-04.md,提交信息为 docs: record EM visualization phase 04 handoff。最终回答报告提交和证据,再从本手册 Task 5 逐字复制下一阶段提示词作为最后一个代码块。阻塞时输出阶段 4 恢复提示词而不是推进。 -``` - -- [ ] **Step 2: Accept only a verified Stage 4 handoff** - -Expected: settings retain the approved defaults, the tracker passes deterministic transition checks, and source audit confirms observe-only behavior. - ---- -### Task 5: Stage 5 — Active-segment rolling controller - -**Files:** - -- Read: Stage 4 handoff and integration Task 3 -- Modify: every exact file listed by integration Task 3 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-05.md` - -**Interfaces:** - -- Consumes: Stage 4 frozen settings and pure `TrajectoryObservationSegmentTracker`. -- Produces: active-segment controller/loop, per-segment coordinator replacement and visualization-only previous trajectory required by Stage 6. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 5/8:活动方向段滚动规划控制器。只完成集成计划 Task 3,不要实现静态/动态图表 Adapter、网页生命周期或插件发布,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已批准。完整阅读并使用 executing-plans、test-driven-development、verification-before-completion;发生非预期失败时完整阅读并使用 systematic-debugging。SKILL.md 位于 C:\Users\admin\.codex\skills\对应目录。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 3、7、9–12 节 -- docs/superpowers/plans/2026-08-05-em-observation-web-integration.md 的 Global Constraints、File Structure、Task 3 -- docs/superpowers/handoffs/em-observation-web/phase-04.md -- TrajectoryObservationPipeline.cs 全文 -- TrajectoryObservationSegmentTracker.cs 及其检查 -- EmPlanningCoordinator、TrajectoryExecutor、EmPlanningRequest、PlanningCycleResult 和 TrajectoryObservationLoop 直接相关契约 - -启动检查:记录分支、HEAD 和最近提交;验证 b0b79e5 为祖先;核对 phase-04.md 状态和实现提交。运行 trajectory-observation、coordinator、executor 三个入口基线。统计全局脏项但只展开 Task 3 路径,保护所有无关修改与暂存内容。 - -严格执行集成计划 Task 3 的 RED/GREEN 与提交: -- 移除硬编码 segmentIndex = 0,以 tracker 的 ActiveSegment 为唯一活动段来源; -- 保存 planningService,使 coordinator/executor 可在确认换向后按新段替换; -- 同段滚动周期继续使用 coordinator.PublishedTrajectory 作为 previous seed; -- 跨方向段只把旧轨迹保留为 PreviousTrajectoryForVisualization,新 coordinator 的第一次请求 PreviousTrajectory 必须为 null; -- 会话 cycle ID 在 coordinator 替换后继续单调增长; -- CreateEffectiveConfigurationSnapshot 返回 configuration.Copy,不泄露可变配置; -- 规划任务在飞行时不得切段或仅为切段取消; -- Tick 只在完成任务已消费且无任务在飞行时调用 TryAdvanceSegment; -- 等待换向时区分当前方向、期望方向和 directionConfirmed,不能伪造档位读数; -- TrajectoryObservationLoopTick 暴露 SegmentAdvanced 与不可变段状态; -- 全程 OBSERVE_ONLY,不新增执行器写调用。 - -只能修改 Task 3 的 Files 清单。若为测试需要访问内部状态,沿用现有验证宿主/内部可见模式,不扩大公开 EM API。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -2. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator -3. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor -4. rg -n "segmentIndex = 0|SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest,并解释任何命中,确认运行源码无硬编码活动段和执行器调用。 -5. 对阶段文件运行 git diff --check 并核对实现提交文件树。 - -全部通过后创建并单独提交 docs/superpowers/handoffs/em-observation-web/phase-05.md,提交信息为 docs: record EM visualization phase 05 handoff。最终回答报告新鲜证据,然后从本手册 Task 6 逐字复制完整提示词作为最后一个代码块。阻塞时只输出阶段 5 恢复提示词。 -``` - -- [ ] **Step 2: Accept only a verified Stage 5 handoff** - -Expected: requests follow the tracker-confirmed active segment, same-segment seeding remains intact, cross-direction seeding is null, and coordinator/executor regressions pass. - ---- - -### Task 6: Stage 6 — EM visualization adapters and handoff evidence - -**Files:** - -- Read: Stage 5 handoff and integration Task 4–5 -- Create/modify: every exact file listed by integration Task 4–5 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-06.md` - -**Interfaces:** - -- Consumes: Stage 3 generic visualization contracts and Stage 5 active-segment controller state. -- Produces: static/dynamic snapshot builders, all required charts and shared-segment handoff metrics required by Stage 7. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 6/8:EM 静态/动态数据可视化适配。只完成集成计划 Task 4–5,不要启动网页会话、修改 Painter 生命周期或插件发布,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已批准。完整阅读并使用 executing-plans、test-driven-development、verification-before-completion;非预期失败时使用 systematic-debugging。技能文件位于 C:\Users\admin\.codex\skills\对应目录。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 4–6、8–12 节 -- docs/superpowers/plans/2026-08-05-em-observation-web-integration.md 的 Global Constraints、File Structure、Task 4、Task 5 -- docs/superpowers/handoffs/em-observation-web/phase-05.md -- Stage 3 的 Visualization contracts 与 SampleSnapshotFactory -- TrajectoryObservationPipeline、现有 TrajectoryObservationCharts/Diagnostics/Presentation -- PlanningGridMap、DirectionSegmentView、FrenetProjector、EmTrajectory、EmTrajectoryPoint、TrajectorySampler 和配置 Copy 契约 - -启动检查:记录分支/HEAD/最近提交;验证批准设计为祖先;核对 phase-05.md 完成与提交;运行 trajectory-observation 和 em-core-all 入口基线。统计全局脏项但只展开 Task 4–5 文件范围,保护无关修改和暂存内容。 - -严格执行集成计划 Task 4–5 的全部 TDD 步骤和两个独立提交: -- 静态快照仅会话构建一次,包含世界边界、行优先 LSB-first bitset 占用图、粗路径、Local G2、全部方向段、换向点和冻结的真实生效配置; -- bitset 字节长度严格为 (Rows*Cols+7)/8,不得为每格创建 DTO、SVG 或 Painter 对象; -- 配置按计划规定顺序分组,展示 raw C# 字段名、不变量格式值、中文名和单位; -- 动态快照包含实车位姿、当前/上一轨迹、活动段与当前 horizon 高亮、状态和摘要,历史摘要不得包含完整点数组; -- 生成 ls、st、curvature-s、curvature-t、velocity-t、acceleration-t、jerk-t、yaw-rate-t; -- LS 使用完整活动方向段的 SourceStartArcLength + ReferenceS,ST 保持周期局部 PathS; -- jerk 只发布 N-1 个真实区间样本,使用区间起点,不制造末点之后的值; -- 显示实际加速度/jerk/曲率/带符号速度限制,但没有独立 yaw-rate 配置时不得虚构限制线; -- RollingContinuation 与 ExactStopAtBoundary 的中文语义和真实终端 v/a 必须区分; -- 用绝对 EffectiveAtUtc 对齐当前点 0 与上一轨迹插值点;比较世界 DeltaPosition、共享完整方向段 DeltaReferenceS、DeltaV、DeltaA,绝不相减两轮局部 PathS;投影失败时只将 DeltaReferenceS 标为不可用。 - -只能修改 Task 4–5 的 Files 清单。EmTrajectoryPoint 的 internal a/j 可在同一 ClumsyPilot 程序集中读取,不要为了网页修改公共 EM API。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -2. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all -3. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -4. 确认 rolling service 回归仍为 21 个 knot、滚动末速非零,jerk 图为 20 个区间样本。 -5. 对阶段文件运行 git diff --check 并核对两个实现提交的文件树。 - -全部通过后创建并单独提交 docs/superpowers/handoffs/em-observation-web/phase-06.md,提交信息为 docs: record EM visualization phase 06 handoff。最终回答报告证据,再从本手册 Task 7 逐字复制下一阶段提示词作为最后一个代码块。阻塞时只输出阶段 6 恢复提示词。 -``` - -- [ ] **Step 2: Accept only a verified Stage 6 handoff** - -Expected: all generic snapshots are built from frozen EM/map data, jerk has no terminal fiction, and handoff deltas use world/shared-segment coordinates. - ---- - -### Task 7: Stage 7 — MovementTest hosting, Painter isolation and packaging - -**Files:** - -- Read: Stage 6 handoff and integration Task 6–7 -- Create/modify: every exact file listed by integration Task 6–7 -- Create: `docs/superpowers/handoffs/em-observation-web/phase-07.md` - -**Interfaces:** - -- Consumes: Stage 3 reusable session and Stage 6 static/dynamic builders. -- Produces: optional web/Painter lifecycle, exception fuse, browser launch, published plugin DLL and operator documentation required by Stage 8. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 7/8:MovementTest 网页托管、Painter 隔离、插件发布与操作文档。只完成集成计划 Task 6–7,不要进行最终实车验收,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已批准。完整阅读并使用 executing-plans、test-driven-development、verification-before-completion;出现非预期失败时使用 systematic-debugging。技能文件位于 C:\Users\admin\.codex\skills\对应目录。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 2–6、8、10–12 节 -- docs/superpowers/plans/2026-08-05-em-observation-web-integration.md 的 Global Constraints、File Structure、Task 6、Task 7 -- docs/superpowers/handoffs/em-observation-web/phase-06.md -- MovementTest.TrajectoryObservationTest.cs、TrajectoryObservationPresentation.cs、TrajectoryObservationPipeline.cs -- Stage 6 三个 snapshot/handoff/chart builder -- PlanningVisualizationSession 公开 API -- Publish-ClumsyPilotPlugin.ps1 与 PluginPackagingChecks.cs -- MovementTest README 与 EMPlanner README -启动检查:记录分支、HEAD、最近提交;验证批准设计为祖先;核对 phase-06.md 状态与提交。运行两个验证宿主的当前入口检查。统计全局脏项但只展开 Task 6–7 文件范围;这些文件可能有用户修改,必须逐项兼容且不得覆盖。检查暂存区并保护无关内容。 - -严格执行集成计划 Task 6–7 的所有 TDD 步骤和两个独立提交: -- 建立内部 ITrajectoryObservationVisualizationSink seam 与 TrajectoryObservationVisualizationPublisher; -- publisher 在 10 Hz 门控点才调用 snapshotFactory,异常只记录一次、停止网页 sink 并永久熔断网页输出,规划 loop 继续; -- EnableWebVisualization=false 时不启动服务、不构建网页快照/历史; -- bootstrap 成功后按开关启动网页,记录完整 token URI;AutoOpen 失败只记录,不停止服务; -- 原生 Painter 默认关闭,移除静态 eager Presentation,仅在 EnableNativePainterVisualization=true 时会话内懒创建; -- Stop/替换会话幂等回收网页、端口与已存在 Painter; -- 实际 tick 使用 controller.ActiveSegment 及当前/上一轨迹发布,不在观察循环序列化或等待; -- 保持 OBSERVE_ONLY,网页关闭/慢客户端不改变规划周期; -- 插件发布从 ManagedDll 的同目录精确解析 TrajectoryPlanningVisualization.dll,放入 staging 后再原子交换;保留现有根目录保护、OSQP hash 与恢复机制; -- PluginPackagingChecks 验证精确包树和程序集名; -- 更新两个 README,完整说明开关、Token、科研风图、ReferenceS/PathS、N-1 jerk、滚动/停车、段高亮、换向确认和清理行为。 - -只能修改 Task 6–7 的 Files 清单。不得新增硬件写接口、局域网监听、自动循环重启网页服务或把独立网页依赖恢复到 Painter。 - -退出验证必须重新运行: -1. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -2. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package -3. 再运行一次 plugin-package,确认无遗留 staging/backup。 -4. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -5. dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -6. rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest,确认运行源码无执行器调用。 -7. 对阶段文件运行 git diff --check,核对两个实现提交和插件输出契约。 - -全部通过后创建并单独提交 docs/superpowers/handoffs/em-observation-web/phase-07.md,提交信息为 docs: record EM visualization phase 07 handoff。最终回答报告提交和证据,再从本手册 Task 8 逐字复制完整提示词作为最后一个代码块。阻塞时只输出阶段 7 恢复提示词。 -``` - -- [ ] **Step 2: Accept only a verified Stage 7 handoff** - -Expected: web and Painter are independently optional, visualization faults do not stop planning, and the plugin package contains the reusable visualization assembly. - ---- - -### Task 8: Stage 8 — Full automated, browser and vehicle acceptance - -**Files:** - -- Read: Stage 7 handoff and integration Task 8 -- Modify: no planned runtime source; acceptance-driven fixes return to the owning task's files and tests -- Create: `docs/superpowers/handoffs/em-observation-web/phase-08.md` -- Conditional create: `docs/superpowers/handoffs/em-observation-web/phase-08-vehicle.md` - -**Interfaces:** - -- Consumes: both verification hosts, main build, deterministic smoke mode, plugin output and deployed MovementTest. -- Produces: fresh end-to-end evidence and either full completion or an explicit vehicle-only continuation boundary. - -- [ ] **Step 1: Open a fresh window and paste this prompt exactly** - -```text -你正在执行 EM 观察网页可视化的阶段 8/8:全量自动化、网页冒烟与实车观察验收。本阶段以验证为主,不得顺手扩展功能,也不要派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -设计已批准。完整阅读并使用 executing-plans 与 verification-before-completion。只有发现回归时才使用 test-driven-development;出现非预期失败时完整阅读并使用 systematic-debugging。技能文件位于 C:\Users\admin\.codex\skills\对应目录。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 10–12 节和所有完成标准 -- docs/superpowers/plans/2026-08-05-em-observation-web-integration.md 的 Global Constraints 与 Task 8 -- docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md 的 Global Constraints 与 Task 5 最终验证 -- docs/superpowers/handoffs/em-observation-web/phase-07.md -- phase-01.md 至 phase-06.md 只读取状态、提交、验证和遗留问题字段,不重新加载全部实现推理 -- 两个 README 中的操作与图表解释 - -启动检查:记录分支、HEAD、最近 20 条提交;验证 b0b79e5 为祖先;核对 phase-01 至 phase-07 全部存在且状态为完成,并抽查各阶段实现提交可达。统计全局脏项但不展开无关 diff;检查暂存区。阶段 8 没有预定源码修改,不能把无关脏项当成本功能失败。 - -先执行完整自动化: -1. dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -2. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -3. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all -4. dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package -5. dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true -6. 对本功能自 phase-01 起的提交范围运行 whitespace 检查,并记录任何与本功能无关的既有警告,不能声称由本功能引入。 - -然后执行网页冒烟: -- 运行 dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 30。 -- 在 30 秒内打开输出的 token URI;如当前工具具有浏览器控制能力,可用浏览器检查,否则明确要求用户协助打开,不能伪造肉眼结果。 -- 核对中文标题/状态、英文或数学坐标、科研细线白底、四页签、八图表、Canvas 栅格、SVG 路径覆盖、活动段/horizon 高亮、生效配置、过期状态和进程退出后端口释放。 - -最后执行实车观察清单,但只能在有人监督且环境安全时进行: -1. 开启网页、保持 Painter 关闭;确认 OBSERVE_ONLY。 -2. 观察 rolling、approach、exact-stop;滚动模式不能标为强制停车。 -3. 确认 jerk 为 N-1 区间且无终点下一点/JerkLimitExceeded 假象。 -4. 确认当前/上一轮显示 DeltaPosition、DeltaReferenceS、DeltaV、DeltaA。 -5. 真实换向点必须经过停车保持和 3 个带符号速度样本后才 N 到 N+1 高亮。 -6. 关闭浏览器后规划周期继续;停止 MovementTest 后网页端口和 Painter 正确回收;全程没有执行器输出。 - -如果自动化或网页冒烟失败:不要创建“完成”交接。先定位归属阶段,补充聚焦失败测试,做最小修复,运行归属阶段验证后重新从完整自动化第 1 项开始。只提交回归测试和修复的精确文件,并记录偏差。 - -如果自动化和网页冒烟通过但当前没有受监督实车条件:创建 docs/superpowers/handoffs/em-observation-web/phase-08.md,状态必须写“自动验收完成但实车待验”,逐条记录自动证据和未运行的实车项;单独提交为 docs: record EM visualization phase 08 handoff。最终回答不得声称整体完成,最后一个代码块必须逐字复制本手册“Stage 8 Vehicle Continuation Prompt”。 - -只有自动化、网页冒烟和实车清单全部真实完成,phase-08.md 才能写“状态:完成”并单独提交。最终回答报告所有证据和提交,不再输出下一阶段提示词。 -``` - -- [ ] **Step 2: Accept the correct Stage 8 terminal state** - -Expected: either full verified completion, or `自动验收完成但实车待验` with the exact continuation prompt below. Automated-only evidence must never be labeled as full completion. - ---- - -## Stage 8 Vehicle Continuation Prompt - -Use this only when `phase-08.md` records successful automated/browser acceptance and explicitly leaves supervised vehicle observation pending. - -```text -你正在续验 EM 观察网页可视化阶段 8 的实车观察部分。自动化与网页冒烟已经完成;本窗口只核查交接证据并在受监督安全环境中完成尚未执行的实车清单,不要重复开发、扩展功能或派生子智能体。 - -仓库根目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot - -完整阅读并使用 C:\Users\admin\.codex\skills\verification-before-completion\SKILL.md。只有发现真实回归时才阅读和使用 test-driven-development 与 systematic-debugging;设计已批准,不要重新 brainstorming。 - -必读资料: -- docs/superpowers/specs/2026-08-06-em-observation-web-staged-execution-design.md 的第 8–10 节 -- docs/superpowers/specs/2026-08-05-em-observation-web-visualization-design.md 的第 10–12 节 -- docs/superpowers/plans/2026-08-05-em-observation-web-integration.md 的 Task 8 -- docs/superpowers/handoffs/em-observation-web/phase-08.md -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md - -启动时记录当前分支、HEAD、最近提交,验证 b0b79e5 为祖先,并确认 phase-08.md 的状态正是“自动验收完成但实车待验”。核对其中自动化提交和命令证据可达;如果代码在 phase-08 后发生相关变化,必须先重新运行受影响自动验证,不能直接沿用旧证据。保护所有无关脏工作区和暂存内容。 - -只有在有人监督且环境安全时执行: -1. 开启 EnableWebVisualization,关闭 EnableNativePainterVisualization;确认 UI、控制台和网页显示 OBSERVE_ONLY。 -2. 观察 rolling、approach、exact-stop;确认滚动末端停车硬约束显示未启用,精确停车锚点只在真实边界出现。 -3. 确认 jerk 为轨迹 N-1 个时间区间,没有末点之后的加加速度点或虚假 JerkLimitExceeded。 -4. 确认周期交接展示世界 DeltaPosition、共享方向段 DeltaReferenceS、DeltaV、DeltaA,没有直接比较局部 PathS。 -5. 在真实换向点确认连续停车保持和 3 个正确带符号速度样本后才从 N 高亮到 N+1,等待期间页面明确显示未满足条件。 -6. 关闭浏览器,确认规划观察周期继续且不受慢客户端/关闭影响。 -7. 停止 MovementTest,确认 HTTP/SSE、端口和可能存在的 Painter 全部回收。 -8. 通过日志和源审计确认全过程没有底盘、转向、制动、电机或档位写输出。 - -不得通过操作真实底盘来“帮助”观察任务满足条件;MovementTest 本身必须保持只读观察。任何一项无法安全执行都应记录为未完成,不能声称整体完成。 - -全部实车项通过后创建 docs/superpowers/handoffs/em-observation-web/phase-08-vehicle.md,记录车辆/环境前提、执行时间、逐项结果、日志或截图位置、未运行项、警告和最终结论。单独提交该文件,提交信息为 docs: record EM visualization vehicle acceptance。最终回答列出新鲜实车证据和交接提交,明确整体完成,不再输出后续提示词。 - -若发现回归,创建 phase-08-vehicle.md 并写状态“阻塞”,记录首个有效失败、复现步骤和安全影响;不要现场扩大修改。最终回答输出一份新的阶段 8 实车恢复提示词,携带真实失败事实和允许继续的精确范围。 -``` - ---- - -## Plan Completion Check - -- [ ] All eight stage prompts are copied without truncation when handed to new windows. -- [ ] Every original library Task 1–5 is owned exactly once by Stages 1–3. -- [ ] Every original integration Task 1–8 is owned exactly once by Stages 4–8. -- [ ] Every stage has a distinct handoff path and terminal verification. -- [ ] Blocked stages resume themselves and never advance. -- [ ] Stage 8 distinguishes full vehicle completion from automated-only acceptance. diff --git a/docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md b/docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md deleted file mode 100644 index 6d2a90e..0000000 --- a/docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md +++ /dev/null @@ -1,227 +0,0 @@ -# EM FullDirection Correctness Fixes Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use test-driven-development for every task. Execute tasks serially because they share the EM solver pipeline. - -**Goal:** Fix confirmed FullDirection projection, cancellation, curvature-constraint, timeout-budget, and trajectory-coordinate defects without replacing the existing LS/ST planner. - -**Architecture:** Keep `IEmPlanningService`, `EmPlanningRequest`, `LateralPlanner`, `LongitudinalPlanner`, and trajectory contracts compatible. Apply local fixes where one component owns the invariant; introduce only a shared internal lateral-curvature affine model and internal explicit-budget overloads where the same invariant necessarily crosses components. - -**Tech Stack:** C# 10, .NET Standard 2.0 production assembly, .NET 8 verification host, solver-neutral `IQpSolver` tests. - -## Global Constraints - -- `FullDirectionSegment` plans exactly one complete direction segment; `RollingHorizon` behavior is not redesigned in this plan. -- FullDirection ego admission is restricted to the segment-start prefix `[0, min(L, MaximumProjectionDistanceMeters)]`; it must not select a later U-shape/self-overlap branch. -- A start heading error with magnitude greater than or equal to `π/2` is rejected before `tan(headingError)` is evaluated. -- `Cancelled` always carries a null trajectory/path/candidate, even if a strict fallback candidate exists. -- `SolverTimeoutSeconds` is one combined LS+ST solve budget for a service call, not a fresh budget for each optimizer. -- Every LS QP has a finite linearized vehicle-curvature hard-bound row at every station; nonlinear validation remains authoritative. -- `SegmentLocalS` stores interpolated direction-segment reference S; `PathS` stores optimized lateral-path arc length. -- Do not edit or revert the user's existing `EmPlannerConfiguration.cs` change, PathSmoothing work, Map work, or `ClumsyPilot.csproj` changes. -- Do not add actuator calls, change public EM request/result signatures, or commit/stage files from the dirty shared worktree. - ---- - -### Task 1: Anchor FullDirection start projection and reject folded headings - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` - -**Interfaces:** -- Consumes: existing `FrenetProjector.TryProject` bounded-window overload. -- Produces: service-local FullDirection start-prefix admission; Rolling continues using `[0,L]`. - -- [ ] **Step 1: Write failing service tests.** Add a FullDirection U-shaped all-forward reference whose later arm is closer to the measured pose, and assert `ProjectionFailed` rather than accepting a later `ReferenceS`. Add a same-position start pose with yaw `π`, and assert `ProjectionFailed` with a null trajectory. - -```csharp -EmPlanningResult wrongBranch = service.Plan(fullURequest, CancellationToken.None); -Verification.Equal(EmPlanningStatus.ProjectionFailed, wrongBranch.Status, - "FullDirection cannot enter through a later U branch"); - -EmPlanningResult reversedHeading = service.Plan(oppositeHeadingRequest, CancellationToken.None); -Verification.Equal(EmPlanningStatus.ProjectionFailed, reversedHeading.Status, - "opposite start heading is rejected before slope conversion"); -``` - -- [ ] **Step 2: Run the focused test and verify RED.** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -``` - -Expected: the later U branch and/or opposite-heading assertion fails under the current global `[0,L]`, seed-zero projection. - -- [ ] **Step 3: Implement the local admission rule.** In `EmPlanningService.Plan`, choose the projection upper bound from scope and validate heading before constructing lateral input. - -```csharp -double startProjectionUpperS = request.PlanningScope == EmPlanningScope.FullDirectionSegment - ? Math.Min(segment.LengthMeters, configuration.Frenet.MaximumProjectionDistanceMeters) - : segment.LengthMeters; -if (!projector.TryProject(request.VehicleState.Pose, segment, 0d, startProjectionUpperS, - configuration.Frenet.MaximumProjectionDistanceMeters, 0d, out FrenetProjection startProjection) || - Math.Abs(startProjection.HeadingError) >= Math.PI / 2d) -{ - return Failure(EmPlanningStatus.ProjectionFailed, request, - "Vehicle pose is not an admissible start state for the selected direction segment."); -} -``` - -- [ ] **Step 4: Re-run `em-planning-service` and verify GREEN.** Existing Rolling projection behavior must remain green. - -### Task 2: Make cancellation terminal and non-publishable - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` - -**Interfaces:** -- Produces: `Cancelled` results with null candidate/path/trajectory at every layer. - -- [ ] **Step 1: Write failing optimizer tests.** Use a solver that returns one valid candidate and cancels the supplied source before the next iteration. Assert both optimizers return `Cancelled`, not `SuccessWithFallback`, and expose no candidate. - -```csharp -Verification.Equal(EmPlanningStatus.Cancelled, result.Status, - "cancellation is never converted to fallback success"); -Verification.True(result.Path == null, "cancelled lateral result has no path"); -``` - -- [ ] **Step 2: Verify RED with the focused groups.** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-integration -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration -``` - -Expected: at least one optimizer currently returns `SuccessWithFallback`. - -- [ ] **Step 3: Implement minimal cancellation precedence.** Special-case cancellation in each `FallbackOrFailure`, and check the token after LS, after ST, after assembly, and immediately before service success publication. - -```csharp -if (failureStatus == EmPlanningStatus.Cancelled) - return Failed(EmPlanningStatus.Cancelled, failureReason); -``` - -- [ ] **Step 4: Re-run both optimizer groups and `em-planning-service`; verify GREEN.** - -### Task 3: Add shared linearized curvature hard constraints - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` - -**Interfaces:** -- Produces: `LateralCurvatureLinearization.Create(input, layout, iterate)` returning immutable station affines with indices, gradient, and constant. -- Consumers: objective terms and hard constraints use the exact same affine coefficients. - -- [ ] **Step 1: Write a failing QP-shape test.** For a straight reference and zero iterate, set vehicle maximum curvature to `0.25 1/m`; assert every station has a row equivalent to `-0.25 <= DDL(i) <= 0.25`. Also assert the total constraint count increases by the station count. - -```csharp -Verification.Equal(expectedOldRows + layout.StationCount, problem.ConstraintCount, - "one curvature hard-bound row is emitted per station"); -Verification.True(HasBound(problem, - new Dictionary { { layout.DDL(station), 1d } }, -0.25d, 0.25d), - "straight-path curvature affine is hard bounded"); -``` - -- [ ] **Step 2: Run `lateral-model` and verify RED.** - -- [ ] **Step 3: Extract the existing affine calculation without changing its formula.** Move `CreateCurvatureAffines` and its value type from `LateralObjectiveBuilder` to the new internal file. Keep the nonlinear formula in `LateralGeometryEvaluator`/independent validator unchanged. - -- [ ] **Step 4: Add one curvature row per station in `LateralConstraintBuilder`.** Bounds are `[-maximumVehicleCurvature, +maximumVehicleCurvature]` after subtracting the affine constant. - -```csharp -AddRow(constraints, lower, upper, ref row, - -maximumCurvature - affine.Constant, - maximumCurvature - affine.Constant, - affine.Indices, affine.Gradient); -``` - -- [ ] **Step 5: Update test solver problem classification so the added lateral rows are not mistaken for ST rows, then run `lateral-model`, `lateral-integration`, and `em-planning-service` GREEN.** - -### Task 4: Relinearize rejected solved vectors and report iteration exhaustion honestly - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` - -**Interfaces:** -- Produces: rejected, parseable solver candidates may advance only the SQP iterate/warm start; they never replace `lastValidatedPath`. - -- [ ] **Step 1: Write failing tests.** Configure a low curvature limit so the first candidate is strict and the second parseable candidate fails nonlinear validation. Assert the third QP/warm start is based on the second candidate, while a later timeout still returns the first strict path. Change the outer-limit assertion from ordinary `Success` to `SuccessWithFallback` with a non-empty reason. - -- [ ] **Step 2: Run `lateral-integration` and verify RED.** - -- [ ] **Step 3: Move iterate/warm-start advancement to immediately after a parseable solved candidate, while updating `lastValidatedPath` only after independent validation.** Return `SuccessWithFallback` when the outer loop ends with a strict candidate but without satisfying convergence. - -- [ ] **Step 4: Run `lateral-integration` and `lateral-real-osqp` GREEN.** - -### Task 5: Share one LS/ST solver timeout budget - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanner.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanner.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` - -**Interfaces:** -- Keeps: existing public `Plan(input, token)` and `Optimize(input, token)` entry points. -- Adds: internal overloads accepting a finite positive `TimeSpan solveBudget`. - -- [ ] **Step 1: Write a failing service test.** Delay a lateral fake solve by at least 100 ms under a 2 s configured timeout, record all `QpSolverSettings.MaximumSolveDuration` values, and assert the first ST call receives less than 1.95 s rather than a fresh 2 s. - -- [ ] **Step 2: Run `em-planning-service` and verify RED.** - -- [ ] **Step 3: Add internal explicit-budget overloads.** Default public overloads continue deriving budget from configuration; service starts one monotonic `Stopwatch` immediately before LS and passes `configuredBudget - elapsed` to LS and then ST. Non-positive remaining time returns `SolverTimedOut` with no trajectory. - -- [ ] **Step 4: Add a final elapsed/cancellation check before publication and run `lateral-integration`, `longitudinal-integration`, and `em-planning-service` GREEN.** - -### Task 6: Preserve reference S separately from optimized PathS - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` - -**Interfaces:** -- Extends internal `InterpolatedLateralPathPoint` with `ReferenceS`. -- Keeps public `EmTrajectoryPoint` shape unchanged. - -- [ ] **Step 1: Write a failing curved/offset-path assembly test.** Construct a validated lateral path where reference-S and chord PathS differ; assert each output point's `SegmentLocalS` is the interpolated reference-S and `PathS` remains the ST progress value. - -- [ ] **Step 2: Run `trajectory` and verify RED.** Current assembly writes `sample.PathS` into both fields. - -- [ ] **Step 3: Interpolate `ReferenceS` using the same PathS bracket and pass `geometry.ReferenceS` as `SegmentLocalS`.** Update publication bounds so segment-local S is checked against direction-segment length/reference bound, while PathS is checked against the optimized path upper bound. - -- [ ] **Step 4: Run `trajectory` and `em-core-all` GREEN.** - -### Task 7: Core regression and diff hygiene - -**Files:** -- Verify only; no broad formatting. - -- [ ] **Step 1: Run the complete EM verification set.** - -```powershell -dotnet build ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj --no-restore -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj --no-build -- em-all -``` - -- [ ] **Step 2: Check only scoped diffs.** - -```powershell -git diff --check -- ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost -git status --short -- ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost -``` - -Expected: all groups pass; no whitespace errors; `EmPlannerConfiguration.cs` remains exactly the user's pre-existing modification. diff --git a/docs/superpowers/plans/2026-08-09-movementtest-web-path-visualization.md b/docs/superpowers/plans/2026-08-09-movementtest-web-path-visualization.md deleted file mode 100644 index 078c399..0000000 --- a/docs/superpowers/plans/2026-08-09-movementtest-web-path-visualization.md +++ /dev/null @@ -1,694 +0,0 @@ -# MovementTest Web Path Visualization Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Improve the MovementTest web path overview so coarse, smoothed, and EM paths remain distinguishable, while adding a metric coordinate system, a data-driven legend, fixed-size vehicle rendering, and explicit smoothed-path endpoints. - -**Architecture:** Keep the existing Canvas occupancy layer and SVG geometry layer. Extend the SVG renderer with world-to-screen metrics, metric grid/axis rendering, fixed-pixel marker transforms, and deterministic path layering; add one HTML legend overlay and one static `smooth-start` marker without changing the visualization JSON contract or planner behavior. - -**Tech Stack:** C# 10, .NET `netstandard2.0` visualization library, .NET 10 Windows verification hosts, embedded HTML/CSS/vanilla JavaScript, SVG, Canvas. - -## Global Constraints - -- Do not modify coarse-path, Local G2, EMPlanner, FullDirection, Rolling, or multi-segment state-machine behavior. -- Do not add actuator calls, trajectory tracking, or any execution closed loop; `OBSERVE_ONLY` remains unchanged. -- Do not introduce third-party JavaScript, CSS, map, chart, CDN, or NuGet dependencies. -- Do not replace or version the visualization JSON contract; only add `smooth-start` to the existing static marker collection. -- Keep X and Y at equal world scale and label coordinates in meters. -- Keep all path strokes and marker geometry visually stable across world extents. -- Do not add overview pan, zoom, fullscreen, or layer toggles. -- Preserve unrelated dirty-worktree changes; stage only the files named by the current task. - ---- - -## File Structure - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` - - Owns MovementTest-to-generic-visualization static geometry conversion; it will add the smoothed-path start marker. -- `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` - - Owns overview DOM structure; it will add the screen-space legend container. -- `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` - - Owns browser rendering; it will add coordinate ticks, world/screen metrics, fixed-size markers, ordered paths, and legend population. -- `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` - - Owns semantic visual styling; it will define coordinate, path, marker, and legend appearance. -- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` - - Verifies MovementTest snapshot semantics, including the new `smooth-start` marker. -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` - - Verifies embedded web assets expose coordinate, fixed-marker, layer, and legend capabilities. -- `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` - - Supplies deterministic overlapping coarse, smoothed, and EM geometry for the local browser smoke test. - -## Task 1: Export the Smoothed-Path Start Marker - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs:56-76` -- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs:82-110` - -**Interfaces:** -- Consumes: `TrajectoryObservationBootstrapResult.SmoothedPath.Path`, whose first element supplies `X` and `Y`. -- Produces: one existing-contract `VisualizationMarker` with `Id = "smooth-start"`, `Kind = "smooth-start"`, and `LabelChinese = "平滑路径起点"` when the smoothed path is non-empty. - -- [ ] **Step 1: Write the failing snapshot assertions** - -Add these assertions immediately after the existing `local-g2-path` assertion in `VerifiesStaticSnapshotExportsFrozenConfigurationAndGeometry`: - -```csharp -VisualizationMarker smoothStart = snapshot.StaticMarkers.Single(x => x.Kind == "smooth-start"); -Verification.Equal("平滑路径起点", smoothStart.LabelChinese, - "static snapshot labels the smoothed-path start"); -Verification.NearlyEqual(bootstrap.SmoothedPath.Path[0].X, smoothStart.Position.X, - "smoothed-path start marker X matches the first smoothed point"); -Verification.NearlyEqual(bootstrap.SmoothedPath.Path[0].Y, smoothStart.Position.Y, - "smoothed-path start marker Y matches the first smoothed point"); -``` - -- [ ] **Step 2: Run the focused test and verify failure** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: FAIL in `VerifiesStaticSnapshotExportsFrozenConfigurationAndGeometry` because no marker has `Kind == "smooth-start"`. - -- [ ] **Step 3: Add the minimal static marker** - -Insert this block at the start of `CreateMarkers`, after `var markers = new List();` and before iterating direction segments: - -```csharp -if (bootstrap.SmoothedPath.Path.Count > 0) -{ - var start = bootstrap.SmoothedPath.Path[0]; - markers.Add(new VisualizationMarker("smooth-start", "smooth-start", - "平滑路径起点", new VisualizationPoint(start.X, start.Y))); -} -``` - -- [ ] **Step 4: Run the focused test and verify success** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: `PASS trajectory-observation`. - -- [ ] **Step 5: Commit the marker contract change** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs -git commit -m "feat: mark smoothed path start in EM overview" -``` - -## Task 2: Add the Metric Coordinate System - -**Files:** -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js:40-94` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css:41-57` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs:21-43` - -**Interfaces:** -- Consumes: the existing `getBounds()` result and `#world-overlay.getBoundingClientRect()`. -- Produces: `niceWorldStep(span, targetTicks)`, `worldMetrics(bounds)`, and `renderWorldCoordinates(bounds, metrics)`; later tasks consume `metrics.unitsPerPixel` for fixed-size markers. - -- [ ] **Step 1: Write failing embedded-asset checks** - -Add the following assertions in `WebAssetChecks.Run` after the existing `.world-current` check: - -```csharp -Verification.True(css.Contains(".world-coordinate-grid"), - "overview exposes a metric grid semantic class"); -Verification.True(css.Contains(".world-axis-zero"), - "overview exposes a zero-axis semantic class"); -Verification.True(css.Contains(".world-axis-tick"), - "overview exposes coordinate tick semantics"); -Verification.True(css.Contains(".world-axis-label"), - "overview exposes coordinate axis-label semantics"); -Verification.True(js.Contains("function niceWorldStep("), - "overview selects readable metric tick steps"); -Verification.True(js.Contains("function worldMetrics("), - "overview derives stable world-to-screen metrics"); -Verification.True(js.Contains("function renderWorldCoordinates("), - "overview renders its own metric coordinate system"); -Verification.True(js.Contains("X (m)") && js.Contains("Y (m)"), - "overview labels both metric axes"); -``` - -- [ ] **Step 2: Run the web asset host and verify failure** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: FAIL with `overview exposes a metric grid semantic class`. - -- [ ] **Step 3: Add world/screen metrics and coordinate rendering** - -Insert the following functions after `drawOccupancy` and before `worldPath` in `app.js`: - -```javascript - function niceWorldStep(span, targetTicks = 7) { - if (!finite(span) || span <= 0) return 1; - const rough = span / Math.max(2, targetTicks); - const magnitude = Math.pow(10, Math.floor(Math.log10(rough))); - const normalized = rough / magnitude; - const factor = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10; - return factor * magnitude; - } - - function worldMetrics(bounds) { - const box = overlay.getBoundingClientRect(); - const pixelWidth = box.width > 0 ? box.width : 800; - const pixelHeight = box.height > 0 ? box.height : 520; - const spanX = Math.max(bounds.xMax - bounds.xMin, 1e-9); - const spanY = Math.max(bounds.yMax - bounds.yMin, 1e-9); - return { - pixelWidth, - pixelHeight, - spanX, - spanY, - unitsPerPixel: Math.max(spanX / pixelWidth, spanY / pixelHeight, 1e-9) - }; - } - - function renderWorldCoordinates(bounds, metrics) { - const group = svg("g", { class: "world-coordinate-system", "aria-hidden": "true" }); - const xStep = niceWorldStep(metrics.spanX, 8); - const yStep = niceWorldStep(metrics.spanY, 8); - const epsilon = Math.max(xStep, yStep) * 1e-9; - let count = 0; - for (let x = Math.ceil(bounds.xMin / xStep) * xStep; x <= bounds.xMax + epsilon && count < 100; x += xStep, count += 1) { - const normalized = Math.abs(x) < epsilon ? 0 : x; - group.append(svg("line", { x1: normalized, y1: -bounds.yMax, x2: normalized, y2: -bounds.yMin, class: "world-coordinate-grid" })); - const tick = svg("text", { x: normalized, y: -bounds.yMin - 7 * metrics.unitsPerPixel, class: "world-axis-tick", "font-size": 11 * metrics.unitsPerPixel, "text-anchor": "middle" }); - tick.textContent = formatTick(normalized); - group.append(tick); - } - count = 0; - for (let y = Math.ceil(bounds.yMin / yStep) * yStep; y <= bounds.yMax + epsilon && count < 100; y += yStep, count += 1) { - const normalized = Math.abs(y) < epsilon ? 0 : y; - group.append(svg("line", { x1: bounds.xMin, y1: -normalized, x2: bounds.xMax, y2: -normalized, class: "world-coordinate-grid" })); - const tick = svg("text", { x: bounds.xMin + 6 * metrics.unitsPerPixel, y: -normalized - 3 * metrics.unitsPerPixel, class: "world-axis-tick", "font-size": 11 * metrics.unitsPerPixel, "text-anchor": "start" }); - tick.textContent = formatTick(normalized); - group.append(tick); - } - if (bounds.xMin <= 0 && bounds.xMax >= 0) - group.append(svg("line", { x1: 0, y1: -bounds.yMax, x2: 0, y2: -bounds.yMin, class: "world-axis-zero" })); - if (bounds.yMin <= 0 && bounds.yMax >= 0) - group.append(svg("line", { x1: bounds.xMin, y1: 0, x2: bounds.xMax, y2: 0, class: "world-axis-zero" })); - const xLabel = svg("text", { x: bounds.xMax - 8 * metrics.unitsPerPixel, y: -bounds.yMin - 8 * metrics.unitsPerPixel, class: "world-axis-label", "font-size": 12 * metrics.unitsPerPixel, "text-anchor": "end" }); - xLabel.textContent = "X (m)"; - const yLabel = svg("text", { x: bounds.xMin + 8 * metrics.unitsPerPixel, y: -bounds.yMax + 16 * metrics.unitsPerPixel, class: "world-axis-label", "font-size": 12 * metrics.unitsPerPixel, "text-anchor": "start" }); - yLabel.textContent = "Y (m)"; - group.append(xLabel, yLabel); - overlay.append(group); - } -``` - -Replace the first statement sequence in `renderWorld` with: - -```javascript - const bounds = getBounds(); - const metrics = worldMetrics(bounds); - drawOccupancy(bounds); - overlay.replaceChildren(); - overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`); - overlay.setAttribute("preserveAspectRatio", "xMidYMid meet"); - renderWorldCoordinates(bounds, metrics); -``` - -Add the following CSS after `.world-line`: - -```css -.world-coordinate-grid { stroke: #dfe4e8; stroke-opacity: .62; stroke-width: .7; vector-effect: non-scaling-stroke; } -.world-axis-zero { stroke: #7c8791; stroke-opacity: .72; stroke-width: 1.1; vector-effect: non-scaling-stroke; } -.world-axis-tick { fill: #68737d; font-family: ui-monospace, Consolas, monospace; } -.world-axis-label { fill: #48535d; font-family: "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; font-weight: 600; } -``` - -- [ ] **Step 4: Run the web asset host and verify success** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: `PASS trajectory-planning-visualization`. - -- [ ] **Step 5: Commit the coordinate system** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs -git commit -m "feat: add metric axes to trajectory overview" -``` - -## Task 3: Fix Path Layering and Screen-Size Markers - -**Files:** -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js:52-93` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css:1-9,41-57` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs:21-43` - -**Interfaces:** -- Consumes: `worldMetrics(bounds).unitsPerPixel` from Task 2 and existing polyline/marker kinds. -- Produces: `lineKind(line)`, `markerTransform(position, unitsPerPixel, headingRadians)`, and `appendWorldMarker(marker, frame, metrics)`; the current EM trajectory is rendered after every static/reference layer. - -- [ ] **Step 1: Add failing checks for fixed-size rendering and semantic layers** - -Add these assertions after the coordinate checks in `WebAssetChecks.Run`: - -```csharp -Verification.True(css.Contains("--world-smoothed-path"), - "overview defines a dedicated smoothed-path color"); -Verification.True(css.Contains("--world-em-trajectory"), - "overview defines a dedicated EM-path color"); -Verification.True(css.Contains(".world-marker-shape"), - "overview marker shapes use a shared screen-space class"); -Verification.True(css.Contains(".marker-smooth-start"), - "overview styles the smoothed-path start marker"); -Verification.True(js.Contains("function markerTransform("), - "overview anchors fixed-size markers in world coordinates"); -Verification.True(js.Contains("function appendWorldMarker("), - "overview centralizes fixed-size marker rendering"); -Verification.True(!js.Contains("const corners = [[.38, .18]"), - "vehicle marker no longer uses a large world-size rectangle"); -Verification.True(!css.Contains(".marker-vehicle .vehicle-outline { fill: none; stroke: #20252b; stroke-width: .7; }"), - "vehicle outline no longer uses a world-scale heavy stroke"); -Verification.True(!css.Contains(".marker-vehicle .vehicle-heading { stroke: #20252b; stroke-width: .8; }"), - "vehicle marker no longer uses world-scale heavy strokes"); -``` - -- [ ] **Step 2: Run the web asset host and verify failure** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: FAIL with `overview defines a dedicated smoothed-path color`. - -- [ ] **Step 3: Define path and marker visual variables** - -Add these variables to `:root` without changing the chart color variables: - -```css - --world-coarse-path: #7e8790; - --world-smoothed-path: #168b83; - --world-active-segment: #3f78a8; - --world-em-trajectory: #df6b1f; -``` - -Replace the existing world path and marker CSS rules with: - -```css -.world-line { fill: none; vector-effect: non-scaling-stroke; } -.world-static { stroke: #8d959d; stroke-opacity: .38; stroke-width: 1.4; } -.world-future { stroke: #b6bec6; stroke-dasharray: 5 4; stroke-width: 1.2; } -.world-active { stroke: var(--world-active-segment); stroke-width: 1.5; } -.world-previous { stroke: #887493; stroke-opacity: .66; stroke-dasharray: 7 5; stroke-width: 2; } -.world-coarse { stroke: var(--world-coarse-path); stroke-opacity: .40; stroke-dasharray: 7 5; stroke-width: 2; } -.world-local-g2 { stroke: var(--world-smoothed-path); stroke-opacity: .65; stroke-width: 4; } -.world-segment-active { stroke: var(--world-active-segment); stroke-opacity: .50; stroke-width: 1.5; } -.world-direction-inactive { stroke: #aab2b9; stroke-opacity: .30; stroke-dasharray: 5 5; stroke-width: 1.1; } -.world-current { stroke: var(--world-em-trajectory); stroke-opacity: .96; stroke-width: 2.4; } -.world-marker-shape { vector-effect: non-scaling-stroke; } -.world-marker-label { fill: #20252b; font: 11px/1.2 "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; paint-order: stroke; stroke: rgba(255, 255, 255, .88); stroke-width: 3px; stroke-linejoin: round; } -.world-marker { fill: #d87918; stroke: #ffffff; stroke-width: 1.4; } -.marker-vehicle .vehicle-outline { fill: rgba(32, 37, 43, .16); stroke: #20252b; stroke-width: 1.5; } -.marker-vehicle .vehicle-heading { stroke: #20252b; stroke-width: 1.5; stroke-linecap: round; vector-effect: non-scaling-stroke; } -.marker-smooth-start { fill: #2f8f56; stroke: #ffffff; stroke-width: 1.5; } -.marker-plan-start { fill: #ffffff; stroke: #2f8f56; stroke-width: 2; } -.marker-gear-switch { fill: #d87918; stroke: #ffffff; stroke-width: 1.5; } -.marker-final-goal { fill: #1769aa; stroke: #ffffff; stroke-width: 1.5; } -.world-empty-state { fill: #66707a; font: 14px/1.2 "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; } -``` - -- [ ] **Step 4: Replace world marker and layer rendering** - -Add the following functions after `worldPath`: - -```javascript - function lineKind(line) { return String(line && line.kind || "").toLowerCase(); } - - function markerTransform(position, unitsPerPixel, headingRadians = 0) { - const headingDegrees = (finite(headingRadians) ? headingRadians : 0) * 180 / Math.PI; - return `translate(${position.x} ${-position.y}) rotate(${-headingDegrees}) scale(${unitsPerPixel})`; - } - - function appendWorldMarker(marker, frame, metrics) { - if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return; - const kind = String(marker.kind || "").toLowerCase(); - const pose = frame.vehiclePose; - const heading = kind === "vehicle" && pose && finite(pose.headingRadians) ? pose.headingRadians : 0; - const group = svg("g", { - class: `world-marker-group marker-${kind || "generic"}`, - transform: markerTransform(marker.position, metrics.unitsPerPixel, heading) - }); - if (kind === "vehicle") { - group.append(svg("path", { d: "M -9 -5 L 5 -5 L 9 0 L 5 5 L -9 5 Z", class: "world-marker-shape vehicle-outline" })); - group.append(svg("line", { x1: -4, y1: 0, x2: 7, y2: 0, class: "vehicle-heading" })); - } else if (kind === "smooth-start") { - group.append(svg("circle", { cx: 0, cy: 0, r: 5, class: "world-marker-shape marker-smooth-start" })); - } else if (kind === "plan-start") { - group.append(svg("circle", { cx: 0, cy: 0, r: 5, class: "world-marker-shape marker-plan-start" })); - } else if (kind === "gear-switch" || kind === "gear-switch-end") { - group.append(svg("path", { d: "M 0 -6 L 6 0 L 0 6 L -6 0 Z", class: "world-marker-shape marker-gear-switch" })); - } else if (kind === "final-goal") { - group.append(svg("path", { d: "M -6 -6 L 6 -6 L 6 6 L -6 6 Z", class: "world-marker-shape marker-final-goal" })); - } else { - group.append(svg("path", { d: "M 0 -6 L 6 0 L 0 6 L -6 0 Z", class: "world-marker-shape world-marker" })); - } - overlay.append(group); - const labelY = kind === "plan-start" ? 17 : -9; - const labelGroup = svg("g", { transform: `translate(${marker.position.x} ${-marker.position.y}) scale(${metrics.unitsPerPixel})` }); - const label = svg("text", { x: 10, y: labelY, class: "world-marker-label" }); - label.textContent = marker.labelChinese || ""; - labelGroup.append(label); - overlay.append(labelGroup); - } -``` - -Replace the complete `renderWorld` function with: - -```javascript - function renderWorld() { - const bounds = getBounds(); - const metrics = worldMetrics(bounds); - drawOccupancy(bounds); - overlay.replaceChildren(); - overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`); - overlay.setAttribute("preserveAspectRatio", "xMidYMid meet"); - renderWorldCoordinates(bounds, metrics); - const snapshot = state.staticSnapshot || {}; - const frame = state.frame || {}; - const addLine = (line, css) => { - const path = worldPath(line.points); - if (path) overlay.append(svg("path", { d: path, class: "world-line " + css })); - }; - const staticLines = safeArray(snapshot.staticPolylines); - staticLines.filter(line => lineKind(line) === "global" || lineKind(line) === "coarse") - .forEach(line => addLine(line, "world-coarse")); - staticLines.filter(line => lineKind(line).includes("local-g2") || lineKind(line).includes("g2")) - .forEach(line => addLine(line, "world-local-g2")); - staticLines.filter(line => lineKind(line) !== "global" && lineKind(line) !== "coarse" && !lineKind(line).includes("local-g2") && !lineKind(line).includes("g2")) - .forEach(line => addLine(line, "world-static")); - safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, - segment.segmentIndex === frame.activeSegmentIndex ? "world-segment-active" : "world-direction-inactive")); - const dynamicLines = safeArray(frame.dynamicPolylines); - dynamicLines.filter(line => lineKind(line) === "active-segment") - .forEach(line => addLine(line, "world-segment-active")); - dynamicLines.filter(line => lineKind(line) === "previous" || line.lineStyle === 1) - .forEach(line => addLine(line, "world-previous")); - dynamicLines.filter(line => lineKind(line) !== "active-segment" && lineKind(line) !== "previous" && line.lineStyle !== 1) - .forEach(line => addLine(line, "world-current")); - const allLines = staticLines.concat(safeArray(snapshot.directionSegments), dynamicLines); - const hasPath = allLines.some(line => safeArray(line && line.points) - .some(point => point && finite(point.x) && finite(point.y))); - safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)) - .forEach(marker => appendWorldMarker(marker, frame, metrics)); - if (!hasPath) { - const emptyGroup = svg("g", { transform: `translate(${(bounds.xMin + bounds.xMax) / 2} ${-((bounds.yMin + bounds.yMax) / 2)}) scale(${metrics.unitsPerPixel})` }); - const empty = svg("text", { x: 0, y: 0, class: "world-empty-state", "text-anchor": "middle" }); - empty.textContent = "未收到 Local G2 路径"; - emptyGroup.append(empty); - overlay.append(emptyGroup); - } - } -``` - -Add resize-aware redraw beside the existing window error listeners: - -```javascript - let overviewResizeFrame = 0; - window.addEventListener("resize", () => { - if (overviewResizeFrame || state.activeTab !== "overview" || !state.redrawEnabled.overview) return; - overviewResizeFrame = window.requestAnimationFrame(() => { - overviewResizeFrame = 0; - renderWorld(); - }); - }); -``` - -- [ ] **Step 5: Run the web asset host and verify success** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: `PASS trajectory-planning-visualization`. - -- [ ] **Step 6: Commit fixed markers and path layering** - -```powershell -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs -git commit -m "fix: clarify EM overview path layers" -``` - -## Task 4: Add a Data-Driven World Legend - -**Files:** -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html:21-26` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js:7-17,52-94` -- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css:22-24,41-70` -- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs:16-43` -- Test fixture: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs:11-33` - -**Interfaces:** -- Consumes: existing `LegendChinese`, `Kind`, `staticPolylines`, `directionSegments`, `dynamicPolylines`, `staticMarkers`, and `dynamicMarkers`. -- Produces: `renderWorldLegend(snapshot, frame)` and `#world-legend`; entries are deduplicated by semantic key and only emitted when corresponding data exists. - -- [ ] **Step 1: Write failing legend asset checks** - -Add these assertions in `WebAssetChecks.Run` after the `world-overlay` assertion: - -```csharp -Verification.True(html.Contains("id=\"world-legend\""), - "overview contains a screen-space legend host"); -Verification.True(html.Contains("aria-label=\"路径图例\""), - "overview legend has a Chinese accessible name"); -Verification.True(css.Contains(".world-legend"), - "overview legend has a screen-space visual container"); -Verification.True(css.Contains(".world-legend-swatch"), - "overview legend exposes semantic line and marker samples"); -Verification.True(js.Contains("function renderWorldLegend("), - "overview builds legend entries from current snapshot data"); -Verification.True(js.Contains("legendChinese"), - "overview reuses exported Chinese polyline legends"); -``` - -Add these assertions in `BuildsDeterministicSmokeSnapshots` after creating `staticSnapshot`: - -```csharp -Verification.True(staticSnapshot.StaticPolylines.Any(line => line.Kind == "coarse"), - "smoke snapshot exposes the coarse comparison path"); -Verification.True(staticSnapshot.StaticPolylines.Any(line => line.Kind == "local-g2"), - "smoke snapshot exposes the smoothed comparison path"); -Verification.True(staticSnapshot.StaticMarkers.Any(marker => marker.Kind == "smooth-start"), - "smoke snapshot exposes the smoothed-path start marker"); -``` - -- [ ] **Step 2: Run the web asset host and verify failure** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -``` - -Expected: FAIL with `smoke snapshot exposes the coarse comparison path`; the legend assertions also remain red until Steps 3–6 are complete. - -- [ ] **Step 3: Add the accessible legend host** - -Add this element inside `.world-stack`, after `#world-overlay`: - -```html - -``` - -- [ ] **Step 4: Expand the deterministic smoke snapshot** - -Replace the single static `global` polyline in `SampleSnapshotFactory.CreateStaticSnapshot` with: - -```csharp -new[] -{ - new VisualizationPolyline("coarse-path", "粗路径", "coarse", VisualizationLineStyle.Dashed, - Points((0d, 1d), (3d, 1.2d), (6d, 1.85d), (8d, 4.2d), (11d, 6d))), - new VisualizationPolyline("local-g2-path", "完整 Local G2 路径", "local-g2", VisualizationLineStyle.Solid, - Points((0d, 1d), (3d, 1d), (6d, 2d), (8d, 4d), (11d, 6d))) -}, -``` - -Add this marker as the first item in the static marker array: - -```csharp -new VisualizationMarker("smooth-start", "smooth-start", "平滑路径起点", new VisualizationPoint(0d, 1d)), -``` - -- [ ] **Step 5: Implement data-driven legend population** - -Add the legend reference after the existing overlay reference: - -```javascript - const worldLegend = document.getElementById("world-legend"); -``` - -Add this function after `appendWorldMarker`: - -```javascript - function renderWorldLegend(snapshot, frame) { - if (!worldLegend) return; - worldLegend.replaceChildren(); - const staticLines = safeArray(snapshot.staticPolylines); - const directionSegments = safeArray(snapshot.directionSegments); - const dynamicLines = safeArray(frame.dynamicPolylines); - const markers = safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)); - const entries = []; - const seen = new Set(); - const add = (key, label, swatchClass) => { - if (seen.has(key)) return; - seen.add(key); - entries.push({ label, swatchClass }); - }; - const coarse = staticLines.find(line => lineKind(line) === "global" || lineKind(line) === "coarse"); - const smooth = staticLines.find(line => lineKind(line).includes("local-g2") || lineKind(line).includes("g2")); - const active = dynamicLines.find(line => lineKind(line) === "active-segment"); - const previous = dynamicLines.find(line => lineKind(line) === "previous" || line.lineStyle === 1); - const current = dynamicLines.find(line => lineKind(line) !== "active-segment" && lineKind(line) !== "previous" && line.lineStyle !== 1); - if (coarse) add("coarse", coarse.legendChinese || "粗路径", "legend-line legend-coarse"); - if (smooth) add("smooth", smooth.legendChinese || "完整 Local G2 路径", "legend-line legend-smooth"); - if (active || directionSegments.length) - add("segment", active && active.legendChinese || "当前方向段", "legend-line legend-segment"); - if (previous) add("previous", previous.legendChinese || "上一条 EM 轨迹", "legend-line legend-previous"); - if (current) add("current", current.legendChinese || "当前 EM 轨迹", "legend-line legend-current"); - if (markers.some(marker => lineKind(marker) === "smooth-start")) - add("smooth-start", "平滑路径起点", "legend-marker legend-smooth-start"); - if (markers.some(marker => lineKind(marker) === "plan-start")) - add("plan-start", "EM 规划起点", "legend-marker legend-plan-start"); - if (markers.some(marker => lineKind(marker) === "vehicle")) - add("vehicle", "当前车辆", "legend-marker legend-vehicle"); - if (markers.some(marker => lineKind(marker) === "gear-switch" || lineKind(marker) === "gear-switch-end")) - add("gear", "换向点 / s_end", "legend-marker legend-gear"); - if (markers.some(marker => lineKind(marker) === "final-goal")) - add("goal", "平滑路径终点", "legend-marker legend-goal"); - entries.forEach(entry => { - const item = element("div", "world-legend-item"); - item.append(element("span", "world-legend-swatch " + entry.swatchClass), - element("span", "world-legend-label", entry.label)); - worldLegend.append(item); - }); - worldLegend.hidden = entries.length === 0; - } -``` - -Add this call in `renderWorld` after all markers are appended and before the empty-state branch: - -```javascript - renderWorldLegend(snapshot, frame); -``` - -- [ ] **Step 6: Style the legend with matching semantic samples** - -Add this CSS after the world marker rules: - -```css -.world-legend { position: absolute; top: 14px; right: 14px; z-index: 3; display: grid; gap: 6px; min-width: 154px; max-width: min(260px, calc(100% - 28px)); padding: 10px 12px; border: 1px solid rgba(112, 123, 133, .38); border-radius: 6px; background: rgba(255, 255, 255, .88); box-shadow: 0 2px 10px rgba(32, 37, 43, .08); color: #36404a; font-size: 12px; pointer-events: none; backdrop-filter: blur(2px); } -.world-legend[hidden] { display: none; } -.world-legend-item { display: grid; grid-template-columns: 30px minmax(0, 1fr); align-items: center; gap: 8px; min-height: 16px; } -.world-legend-swatch { display: block; justify-self: center; } -.legend-line { width: 28px; height: 0; border-top-style: solid; } -.legend-coarse { border-top: 2px dashed rgba(126, 135, 144, .62); } -.legend-smooth { border-top: 4px solid rgba(22, 139, 131, .65); } -.legend-segment { border-top: 2px solid rgba(63, 120, 168, .50); } -.legend-previous { border-top: 2px dashed rgba(136, 116, 147, .72); } -.legend-current { border-top: 3px solid rgba(223, 107, 31, .96); } -.legend-marker { width: 11px; height: 11px; border: 1.5px solid #ffffff; box-shadow: 0 0 0 1px rgba(32, 37, 43, .22); } -.legend-smooth-start { border-radius: 50%; background: #2f8f56; } -.legend-plan-start { border: 2px solid #2f8f56; border-radius: 50%; background: #ffffff; } -.legend-vehicle { width: 17px; height: 9px; border: 1.5px solid #20252b; border-radius: 2px; background: rgba(32, 37, 43, .16); } -.legend-gear { transform: rotate(45deg); background: #d87918; } -.legend-goal { background: #1769aa; } -.world-legend-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -``` - -Replace the existing mobile media query with the following complete rule: - -```css -@media (max-width: 720px) { - header { align-items: start; flex-direction: column; gap: 10px; } - nav, main, header { padding-left: 14px; padding-right: 14px; } - .world-stack { min-height: 360px; } - .world-legend { top: 8px; right: 8px; min-width: 138px; padding: 8px 9px; font-size: 11px; } - .config-entry { grid-template-columns: 1fr 1fr; } -} -``` - -- [ ] **Step 7: Run focused and integration verification** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true --no-restore -``` - -Expected output contains: - -```text -PASS trajectory-planning-visualization -PASS trajectory-observation -Build succeeded. -``` - -- [ ] **Step 8: Run the local visual smoke test** - -Run the sample server for 60 seconds: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 60 -``` - -Open the printed loopback URL and verify every item: - -- the vehicle is approximately 18–20 screen pixels long and does not cover the route; -- X and Y retain equal spatial scale; -- metric ticks, grid, `X (m)`, and `Y (m)` are readable; -- coarse gray dashes, wide translucent teal smooth path, and narrow orange EM path remain visible where they overlap; -- the legend matches only elements visible in the snapshot; -- smoothed start, final goal, current vehicle, and EM plan-start markers are visible; -- no browser console error or “页面绘图异常” status appears. - -- [ ] **Step 9: Check scoped diff hygiene and commit** - -Run: - -```powershell -git diff --check -- ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs -git status --short -- ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs -git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs -git commit -m "feat: add legend to EM path overview" -``` - -Expected: `git diff --check` emits no error, scoped status lists only the intended files before staging, and the commit contains only those five paths. - -## Final Verification - -After all task commits, run: - -```powershell -dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true --no-restore -git diff --check -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs -``` - -Expected: both verification hosts print their PASS lines, the main project reports `Build succeeded.`, and the scoped diff check produces no errors. diff --git a/docs/superpowers/plans/2026-08-09-trajplanner-output-demo.md b/docs/superpowers/plans/2026-08-09-trajplanner-output-demo.md deleted file mode 100644 index a102530..0000000 --- a/docs/superpowers/plans/2026-08-09-trajplanner-output-demo.md +++ /dev/null @@ -1,136 +0,0 @@ -# Trajplanner_output 真实轨迹输出 Demo Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 提供一个可配置、可运行、可导出且可供控制模块学习引用的真实 EM 轨迹序列 Demo。 - -**Architecture:** 独立 `net10.0-windows` 控制台项目以 `ProjectReference` 调用已有规划库,在固定演示场景中依次得到粗路径、平滑路径和真实 OSQP EM 轨迹。输出层只消费不可变 `EmTrajectory`,将其写入 CSV 并投影为控制模块 DTO,不包含硬件调用。 - -**Tech Stack:** .NET 10、C#、`ClumsyPilot.csproj`、OSQP Windows x64、CSV、Markdown。 - -## Global Constraints - -- Demo 必须使用 `EmPlanningService(new OsqpNativeSolver())`,不得用假求解器伪造成功轨迹。 -- Demo 配置全部集中于 `TrajectoryOutputDemoConfiguration.cs`;位置 m、航向 rad、速度 m/s、曲率 1/m、时间 s。 -- 只接受 `Success` 或 `SuccessWithFallback` 的非空 `EmTrajectory`。 -- OSQP 或任一规划阶段失败时非零退出,不输出部分/伪造 CSV。 -- 控制模块适配器只提供只读序列,不驱动、转向、制动或换向设备。 -- 新增注释使用 CoarsePath 风格中文 XML 文档注释。 - ---- - -### Task 1: 创建可运行 Demo 项目和集中配置 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj` -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Program.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoConfiguration.cs` -- Test: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj` - -**Interfaces:** -- Consumes: `ClumsyPilot.csproj` 的 `CoarsePath`、`PathSmoothing` 与 `EMPlanner` 公共 API。 -- Produces: 一份可复制、单文件可调的演示配置和标准退出码入口。 - -- [ ] **Step 1: 写入项目引用** - -创建 `net10.0-windows` 控制台项目,关闭隐式 using/启用 nullable,并引用 `../../ClumsyPilot.csproj`,同时设定 `ExcludeLegacyAutoAvoidance=true`。 - -- [ ] **Step 2: 写入默认演示配置** - -配置包含 `MapBoundsMm(0, 6000, 0, 4000)`、`50 mm` 栅格、显式空地图、起点 `(1,1,0)`、终点 `(3,1,0)`、车辆 `0.80 m × 0.60 m`、`0.05 m` 安全余量、`1/1.20 1/m` 曲率上限、前进方向与 `output/trajectory.csv`。 - -- [ ] **Step 3: 编写失败入口测试** - -Run: `dotnet run --project ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj -- --invalid-option` - -Expected: 非零退出并打印使用说明;尚未实现时命令因项目不存在而失败。 - -### Task 2: 实现真实规划链路与成功/失败边界 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoRunner.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Program.cs` -- Test: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj` - -**Interfaces:** -- Consumes: `CoarsePathPlanningService.Plan`、`PathSmoothingService.Smooth`、`EmPlanningService.Plan`。 -- Produces: 成功时 `EmTrajectory`;失败时含阶段、状态和原因的非零结果。 - -- [ ] **Step 1: 构造冻结的 CoarsePath 与平滑请求** - -使用配置创建 `CoarsePathPlanningJob` 和 `PathSmoothingRequest`,每一步仅在成功状态且输出非空时进入下一阶段;失败信息写入 Demo 结果。 - -- [ ] **Step 2: 构造真实 EM 请求** - -以平滑路径、同一地图、车辆、`VehicleMotionState`、默认 `EmPlannerConfiguration`、方向段索引和唯一输出 ID 创建 `EmPlanningRequest`,并调用 `new EmPlanningService(new OsqpNativeSolver()).Plan(...)`。 - -- [ ] **Step 3: 拒绝非完整输出** - -仅当 `result.Status` 为 `Success` 或 `SuccessWithFallback`、`result.Trajectory` 非空且点数大于零时返回成功;其他状态返回非零并输出 `FailureReason`。 - -- [ ] **Step 4: 运行真实链路** - -Run: `dotnet run --project ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj` - -Expected: OSQP 可用时输出轨迹 ID、点数和 CSV 路径;不可用时输出明确 OSQP/规划诊断且不产生成功 CSV。 - -### Task 3: 导出轨迹序列和控制模块只读适配器 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectorySequenceExporter.cs` -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/ControlModuleTrajectoryAdapter.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemoRunner.cs` -- Test: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj` - -**Interfaces:** -- Consumes: `EmTrajectory.Metadata` 和 `IReadOnlyList`。 -- Produces: UTF-8 CSV,以及控制模块可枚举的只读 `ControlTrajectoryPoint` 序列。 - -- [ ] **Step 1: 实现 CSV 字段和原子写入** - -首行固定为 `time_s,x_m,y_m,yaw_rad,signed_velocity_mps,yaw_rate_radps,curvature_per_m,direction,segment_index,path_s_m,boundary_type`。成功轨迹写入临时文件后原子替换目标 CSV,避免控制模块读到半文件。 - -- [ ] **Step 2: 实现控制 DTO** - -`ControlTrajectoryPoint` 提供时间、位置、航向、带符号速度、yaw rate、曲率、方向和边界类型;`ControlModuleTrajectoryAdapter.Create(EmTrajectory)` 返回只读列表和元数据,不产生任何硬件调用。 - -- [ ] **Step 3: 输出摘要** - -控制台打印轨迹 ID、生效时间、方向段、终端类型、点数、首末点和 CSV 绝对路径,不逐行刷屏。 - -- [ ] **Step 4: 验证文件内容** - -Run: `Import-Csv ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/output/trajectory.csv | Select-Object -First 1` - -Expected: 首个数据行具有全部 11 个字段,时间字段为非负数。 - -### Task 4: 编写 README 和最终验证 - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md` -- Modify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/*.cs` -- Test: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj` - -**Interfaces:** -- Consumes: 最终项目、配置、CSV 和控制 DTO。 -- Produces: 对外可复现的运行、调参和控制模块引用说明。 - -- [ ] **Step 1: 以 CoarsePath 结构编写 README** - -写入模块职责、文件结构、数据流、单位、运行命令、配置表、CSV 契约、控制模块 `ProjectReference` 示例、失败语义和“演示空地图不得用于真实作业”的限制。 - -- [ ] **Step 2: 完成 XML 注释** - -每个公开类型、配置字段、运行阶段、导出边界和控制 DTO 都说明职责、单位与失败/只读语义;不写逐行翻译式注释。 - -- [ ] **Step 3: 运行格式与构建验证** - -Run: `dotnet build ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/TrajectoryOutputDemo.csproj; git diff --check` - -Expected: 构建退出 0,格式检查退出 0;若现有 Visual Studio 锁定依赖 DLL,记录锁定文件和进程,不假称通过。 - -- [ ] **Step 4: 提交 Demo** - -Run: `git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_output docs/superpowers/plans/2026-08-09-trajplanner-output-demo.md; git commit -m "feat: add trajectory output demo"` - -Expected: 本机提交只包含 Demo、README 与实施计划。 diff --git a/docs/superpowers/plans/2026-08-10-em-closed-loop-movement-test.md b/docs/superpowers/plans/2026-08-10-em-closed-loop-movement-test.md deleted file mode 100644 index eea22e8..0000000 --- a/docs/superpowers/plans/2026-08-10-em-closed-loop-movement-test.md +++ /dev/null @@ -1,508 +0,0 @@ -# EM Closed-Loop Movement Test Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add an `EM闭环测试` MovementTest that plans one complete EM direction segment once, freezes it, adapts it to `Trajectory2D`, and tracks it with the imported Stanley/PID/GCP controller until that segment stops. - -**Architecture:** Keep the planning and controller models separate. Restore the reference controller's smallest portable runtime dependency set inside `ClumsyPilot`, add a pure `EmTrajectory`-to-`Trajectory2D` adapter, then make a session runner reuse the existing trajectory-observation bootstrap and one `TrajectoryObservationController.StartCycle` call before handing the frozen trajectory to `TrajectoryTrackingMovement`. - -**Tech Stack:** C# 10, .NET Standard 2.0 plugin, .NET 10 Windows verification host, Clumsy `MovementTest`/`DriveTask`, existing EM planner and OSQP runtime. - -## Global Constraints - -- The MovementTest registration name is exactly `EM闭环测试`. -- Plan exactly once in `EmPlanningScope.FullDirectionSegment`; do not replan or replace the trajectory while driving. -- Execute only the first direction segment. A gear-switch terminal stops and ends this test. -- Shared/control boundaries use SI units: m, m/s, rad, rad/s; body X is forward, body Y is left, counter-clockwise is positive. -- Preserve signed longitudinal velocity: forward positive and reverse negative. -- Do not change velocity/steering signs, CAN IDs, remote mappings, mechanical limits, or mode-switch policy. -- Do not overwrite unrelated dirty files. Stage only the exact files named by each task. -- Real-vehicle validation must remain low-speed, short-distance, in an open area, with hardware emergency stop available. - ---- - -## File Map - -### Existing user files retained and brought under version control - -- `ClumsyPilot/Control/**/*.cs` — Stanley/PID/GCP controller implementation already copied from the reference project. -- `ClumsyPilot/Trajectory/**/*.cs` — controller-side trajectory and projection implementation already copied from the reference project. - -### Portable runtime dependencies copied from the reference project - -- `ClumsyPilot/Shared/Mathematics/AngleMath.cs` — angle normalization and conversion. -- `ClumsyPilot/Shared/Mathematics/InterpolationMath.cs` — scalar interpolation. -- `ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs` — world/body transforms. -- `ClumsyPilot/Shared/Models/ChassisCommand.cs` — `Pose2D`, `Twist2D`, and chassis command value types. -- `ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs` — SI/GCP boundary over the existing `MultiWheelChassis`. -- `ClumsyPilot/StateEstimation/IVehicleStateProvider.cs` — controller state-source boundary. -- `ClumsyPilot/StateEstimation/VehicleState.cs` — immutable controller state. -- `ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs` — velocity filter primitive. -- `ClumsyPilot/StateEstimation/VelocityEstimator2D.cs` — Detour velocity estimation. -- `ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs` — live Detour state source. -- `ClumsyPilot/Movements/TrajectoryTrackingMovement.cs` — `DriveTask` movement wrapper around the geometric controller. - -### New EM closed-loop files - -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs` — pure model adapter. -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs` — UI input, session ownership, one-shot planning, controller handoff, and stop behavior. -- `ClumsyPilot/tests/EMPlannerVerificationHost/EmControlTrajectoryAdapterChecks.cs` — adapter behavior checks. -- `ClumsyPilot/tests/verify_em_closed_loop_movement.ps1` — MovementTest registration and one-shot lifecycle checks. -- `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` — add the `em-control-adapter` verification command. -- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` — operator procedure and test boundary. - ---- - -### Task 1: Restore the controller's portable runtime dependency chain - -**Files:** - -- Add and track: `ClumsyPilot/Control/**/*.cs` -- Add and track: `ClumsyPilot/Trajectory/**/*.cs` -- Create: `ClumsyPilot/Shared/Mathematics/AngleMath.cs` -- Create: `ClumsyPilot/Shared/Mathematics/InterpolationMath.cs` -- Create: `ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs` -- Create: `ClumsyPilot/Shared/Models/ChassisCommand.cs` -- Create: `ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs` -- Create: `ClumsyPilot/StateEstimation/IVehicleStateProvider.cs` -- Create: `ClumsyPilot/StateEstimation/VehicleState.cs` -- Create: `ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs` -- Create: `ClumsyPilot/StateEstimation/VelocityEstimator2D.cs` -- Create: `ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs` -- Create: `ClumsyPilot/Movements/TrajectoryTrackingMovement.cs` - -**Interfaces:** - -- Produces: `MyParking.Shared.Pose2D`, `Twist2D`, `AngleMath`, `MultiWheelChassisAdapter`. -- Produces: `MultiWheelC.StateEstimation.IVehicleStateProvider` and `DetourVehicleStateProvider`. -- Produces: `MultiWheelC.Trajectory.Trajectory2D` and `TrajectoryPoint`. -- Produces: `MultiWheelC.TrajectoryTrackingMovement : MovementDefinition` with public `Trajectory`, `StateProvider`, `CycleObserver`, and safety-limit fields. - -- [ ] **Step 1: Re-run the failing integration build** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -``` - -Expected: FAIL with missing `MyParking.Shared`, `MultiWheelC.StateEstimation`, `VehicleState`, and `MultiWheelChassisAdapter`. This proves the partial controller import is not silently excluded from the plugin build. - -- [ ] **Step 2: Add the exact portable reference sources** - -Read each source from: - -```text -D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\Shared\... -D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC\StateEstimation\... -D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC\Movements\TrajectoryTrackingMovement.cs -``` - -Add the files at the destination paths in this task without changing namespaces or logic. Do not import `FleetKinematics`, experiment recorders, composite movements, rotation movements, or wheel-feedback providers because `TrajectoryTrackingMovement` does not consume them. - -- [ ] **Step 3: Prove existing controller/trajectory files still match the reference implementation** - -Run: - -```powershell -$reference = 'D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC' -$local = (Resolve-Path '.\ClumsyPilot').Path -Get-ChildItem '.\ClumsyPilot\Control','.\ClumsyPilot\Trajectory' -File -Recurse | ForEach-Object { - $relative = $_.FullName.Substring($local.Length + 1) - $source = Join-Path $reference $relative - if (-not (Test-Path $source) -or (Get-FileHash $_.FullName).Hash -ne (Get-FileHash $source).Hash) { - throw "Reference mismatch: $relative" - } -} -``` - -Expected: exit 0 and no mismatch. - -- [ ] **Step 4: Run the build to verify the dependency chain compiles** - -Run: - -```powershell -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -``` - -Expected: PASS with zero errors. Record warnings verbatim; do not hide them. - -- [ ] **Step 5: Commit only the controller runtime integration** - -```powershell -git add -- ClumsyPilot/Control ClumsyPilot/Trajectory ClumsyPilot/Shared ClumsyPilot/StateEstimation ClumsyPilot/Movements/TrajectoryTrackingMovement.cs -git diff --cached --check -git commit -m "feat: integrate trajectory tracking controller runtime" -``` - ---- - -### Task 2: Adapt a frozen EM trajectory to `Trajectory2D` - -**Files:** - -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs` -- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/EmControlTrajectoryAdapterChecks.cs` -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` - -**Interfaces:** - -- Consumes: `EmTrajectory` with validated SI-unit `EmTrajectoryPoint` values. -- Produces: `public sealed class EmControlTrajectoryAdapter`. -- Produces: `public Trajectory2D Create(EmTrajectory trajectory)`. -- Duplicate-position threshold: `1e-6 m`, matching `Trajectory2D`'s minimum valid projection segment. - -- [ ] **Step 1: Register the adapter check before implementing the adapter** - -Extend `Program.cs` argument validation with `em-control-adapter`, and add: - -```csharp -if (args[0] == "em-control-adapter" || args[0] == "em-all") -{ - EmControlTrajectoryAdapterChecks.Run(); - Console.WriteLine("PASS em-control-adapter"); -} -``` - -Create `EmControlTrajectoryAdapterChecks.cs` with checks that build real `EmTrajectory` objects and assert: - -```csharp -internal static void Run() -{ - MapsFieldsAndRebuildsGeometricArcLength(); - KeepsReverseVelocityNegative(); - CollapsesTerminalHoldAndKeepsItsZeroSpeed(); - RejectsFewerThanTwoDistinctPositions(); -} -``` - -The forward mapping check must assert all of these exact relations: - -```csharp -Trajectory2D actual = new EmControlTrajectoryAdapter().Create(source); -Require(actual.Count == 3, "adapter point count"); -RequireClose(actual[0].ArcLengthMeters, 0d, "first arc"); -RequireClose(actual[1].ArcLengthMeters, 0.5d, "second arc"); -RequireClose(actual[2].ArcLengthMeters, 1.0d, "third arc"); -RequireClose(actual[1].PoseInWorld.XMeters, source.Points[1].X, "x"); -RequireClose(actual[1].PoseInWorld.YMeters, source.Points[1].Y, "y"); -RequireClose(actual[1].PoseInWorld.YawRadians, source.Points[1].Yaw, "yaw"); -RequireClose(actual[1].CurvaturePerMeter, source.Points[1].VehicleCurvature, "curvature"); -RequireClose(actual[1].ReferenceSpeedMetersPerSecond, - source.Points[1].SignedLongitudinalVelocity, "signed speed"); -``` - -For the terminal-hold case, provide two final EM samples at the same pose where the later sample has zero speed, then assert that the output contains one terminal position and `EndPoint.ReferenceSpeedMetersPerSecond == 0d`. - -- [ ] **Step 2: Run the new check and verify RED** - -Run: - -```powershell -dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- em-control-adapter -``` - -Expected: FAIL to compile because `EmControlTrajectoryAdapter` does not exist. - -- [ ] **Step 3: Implement the minimum adapter** - -Implement this public surface and algorithm: - -```csharp -public sealed class EmControlTrajectoryAdapter -{ - private const double MinimumSegmentLengthMeters = 1e-6; - - public Trajectory2D Create(EmTrajectory trajectory) - { - if (trajectory == null) - throw new ArgumentNullException(nameof(trajectory)); - - var points = new List(trajectory.Points.Count); - double arcLength = 0d; - for (int index = 0; index < trajectory.Points.Count; index++) - { - EmTrajectoryPoint source = trajectory.Points[index]; - var pose = new MyParking.Shared.Pose2D(source.X, source.Y, source.Yaw); - var converted = new TrajectoryPoint( - arcLength, pose, source.VehicleCurvature, source.SignedLongitudinalVelocity); - - if (points.Count == 0) - { - points.Add(converted); - continue; - } - - TrajectoryPoint previous = points[points.Count - 1]; - double dx = pose.XMeters - previous.PoseInWorld.XMeters; - double dy = pose.YMeters - previous.PoseInWorld.YMeters; - double distance = Math.Sqrt(dx * dx + dy * dy); - if (distance < MinimumSegmentLengthMeters) - { - points[points.Count - 1] = new TrajectoryPoint( - previous.ArcLengthMeters, pose, source.VehicleCurvature, - source.SignedLongitudinalVelocity); - continue; - } - - arcLength += distance; - points.Add(new TrajectoryPoint( - arcLength, pose, source.VehicleCurvature, - source.SignedLongitudinalVelocity)); - } - - if (points.Count < 2) - throw new ArgumentException( - "EM轨迹至少需要包含两个不同位置的有效控制点。", nameof(trajectory)); - - return new Trajectory2D(points); - } -} -``` - -Include `/// ` comments on the class and public method, explicitly stating the unit and signed-speed boundary. - -- [ ] **Step 4: Run adapter checks and the existing EM suite** - -Run: - -```powershell -dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- em-control-adapter -dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- trajectory-observation -``` - -Expected: both commands PASS. - -- [ ] **Step 5: Commit the adapter and checks** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmControlTrajectoryAdapterChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs -git diff --cached --check -git commit -m "feat: adapt EM trajectory for geometric controller" -``` - ---- - -### Task 3: Add the one-shot, single-direction `EM闭环测试` - -**Files:** - -- Create: `ClumsyPilot/tests/verify_em_closed_loop_movement.ps1` -- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs` - -**Interfaces:** - -- Consumes: `TrajectoryObservationSetupFactory.CreateBootstrapJob(...)`. -- Consumes: `TrajectoryObservationBootstrapper.Bootstrap(...)`. -- Consumes exactly one call to `TrajectoryObservationController.StartCycle(...)`. -- Consumes: `EmControlTrajectoryAdapter.Create(...)`. -- Produces: `[MovementTest(name = "EM闭环测试")] public sealed class EmClosedLoopMovementTest : MovementTest`. -- Produces: `Test()` for session start and `TestStop()` for idempotent cancellation/stop. - -- [ ] **Step 1: Write the structural/lifecycle verification first** - -Create `verify_em_closed_loop_movement.ps1` so it reads the new source file and fails unless all conditions hold: - -```powershell -$sourcePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\tarjplanner_movementtest\MovementTest.EmClosedLoopTest.cs' -if (-not (Test-Path -LiteralPath $sourcePath)) { throw 'EM closed-loop MovementTest source is missing.' } -$source = Get-Content -Raw -Encoding UTF8 -LiteralPath $sourcePath - -$required = @( - '\[MovementTest\(name = "EM闭环测试"\)\]', - 'EmPlanningScope\.FullDirectionSegment', - 'TrajectoryObservationBootstrapper', - '\.StartCycle\(', - 'EmControlTrajectoryAdapter', - 'TrajectoryTrackingMovement', - 'public override void TestStop\(\)', - '\.Cancel\(\)', - '\.Stop\(\)' -) -foreach ($pattern in $required) { - if ($source -notmatch $pattern) { throw "Missing required pattern: $pattern" } -} - -if ([regex]::Matches($source, '\.StartCycle\(').Count -ne 1) { - throw 'EM closed-loop test must contain exactly one planning-cycle call site.' -} -if ($source -match 'TrajectoryObservationLoop|ReplanPeriod|while\s*\(true\).*StartCycle') { - throw 'EM closed-loop test must not contain a replanning loop.' -} - -Write-Output 'EM closed-loop MovementTest checks passed.' -``` - -- [ ] **Step 2: Run the verification and confirm RED** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_em_closed_loop_movement.ps1 -``` - -Expected: FAIL with `EM closed-loop MovementTest source is missing.` - -- [ ] **Step 3: Implement the MovementTest input surface** - -Add `EmClosedLoopMovementTest` with the same goal, map, obstacle, vehicle, solver, and output-step defaults used by `TrajectoryObservationMovementTest`. Set control defaults explicitly: - -```csharp -public double MaximumCommandSpeedMetersPerSecond = 0.20d; -public double MaximumDistanceToTrajectoryMeters = 0.30d; -public double ExecutionTimeoutSeconds = 120d; -public float WheelAlignmentToleranceDegrees = 2f; -``` - -`Test()` must validate inputs, create a validated settings snapshot with: - -```csharp -PlanningScope = EmPlanningScope.FullDirectionSegment -``` - -and start one session through `EmClosedLoopMovementTestRunner.Start(...)`. `TestStop()` must call only: - -```csharp -EmClosedLoopMovementTestRunner.Stop(); -``` - -Use the existing observation test's culture-aware finite-number and bounded-obstacle input behavior. Keep those helpers private to the new test so the user's modified observation file is not edited. - -- [ ] **Step 4: Implement idempotent session ownership and one-shot planning** - -Use a static runner guarded by one lock. Store one `CancellationTokenSource`, one background `Task`, one active `DriveTask`, and a monotonically increasing session ID. - -The planning body must follow this exact sequence: - -```csharp -VehicleMotionState initialState = ReadPlanningState(); -CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( - initialState.Pose, goal, settings, obstacles, obstacleSnapshotVersion); -TrajectoryObservationBootstrapResult bootstrap = - new TrajectoryObservationBootstrapper().Bootstrap(job, token); -if (!bootstrap.Succeeded) - throw new InvalidOperationException(bootstrap.FailureReason); - -var controller = new TrajectoryObservationController( - bootstrap, settings, new EmPlanningService(new OsqpNativeSolver()), - "em-closed-loop-" + sessionId.ToString(CultureInfo.InvariantCulture)); -PlanningCycleResult cycle = await controller.StartCycle( - initialState.CapturedAtUtc, initialState, token).ConfigureAwait(false); -if (!cycle.Published || controller.PublishedTrajectory == null) - throw new InvalidOperationException( - string.IsNullOrWhiteSpace(cycle.Result.FailureReason) - ? cycle.Diagnostic - : cycle.Result.FailureReason); - -Trajectory2D trajectory = - new EmControlTrajectoryAdapter().Create(controller.PublishedTrajectory); -``` - -There must be no observation loop and no second `StartCycle` call. Log the EM point count, controller point count, geometric length, segment index, direction, and terminal type before control starts. - -Create `EmClosedLoopWheelSafety.AreWheelsForward(float toleranceDegrees)` in the same file using `MultiWheelChassisAdapter.AreParallelWheelsAligned(0d, toleranceRadians)`. If this check fails, throw before constructing `DriveTask`. - -Create and run the controller movement as: - -```csharp -var movement = new TrajectoryTrackingMovement -{ - Trajectory = trajectory, - MaximumCommandSpeedMetersPerSecond = maximumCommandSpeedMetersPerSecond, - MaximumDistanceToTrajectoryMeters = maximumDistanceToTrajectoryMeters, - ExecutionTimeoutSeconds = executionTimeoutSeconds -}; -var driveTask = new DriveTask(movement.Get()); -``` - -Publish `driveTask` under the session lock only if the session is still current and not cancelled. Otherwise stop it immediately. For a current session call `driveTask.Wait()` on the background worker, then report that the first direction segment ended. A `GearSwitch` terminal is reported as “stopped at gear-switch boundary; next segment was not started.” - -`Stop()` must atomically clear the active references, call `CancellationTokenSource.Cancel()`, and call `DriveTask.Stop()`. The completion continuation must clear only references that still belong to its own session. - -- [ ] **Step 5: Run structural verification and compile** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_em_closed_loop_movement.ps1 -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -``` - -Expected: structural check PASS; build PASS with zero errors. - -- [ ] **Step 6: Commit the MovementTest** - -```powershell -git add -- ClumsyPilot/tests/verify_em_closed_loop_movement.ps1 ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs -git diff --cached --check -git commit -m "feat: add one-shot EM closed-loop movement test" -``` - ---- - -### Task 4: Document operation and run the complete non-hardware verification - -**Files:** - -- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` - -**Interfaces:** - -- Documents: test selection, inputs, one-shot/single-direction boundary, `TestStop`, low-speed first-run procedure, and limits of automated validation. - -- [ ] **Step 1: Add the operator-facing section** - -Add a section named `EM闭环测试` containing these exact operational facts: - -```text -- 启动时只规划一次,并冻结首次成功的完整方向段轨迹。 -- 控制执行期间不重规划、不切换轨迹。 -- 到达目标或换向边界后停车;本测试不启动下一方向段。 -- 默认控制速度上限为 0.20 m/s。 -- 规划、适配或车轮方向检查失败时不会启动底盘控制。 -- TestStop 会同时取消规划并停止活动控制任务。 -- 首次实车测试使用空旷环境、短距离单方向目标,并确保硬件急停可用。 -``` - -- [ ] **Step 2: Run all targeted checks** - -Run: - -```powershell -dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- em-control-adapter -dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- trajectory-observation -powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_em_closed_loop_movement.ps1 -dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore -``` - -Expected: every command exits 0. Report warning counts from the build. - -- [ ] **Step 3: Verify scope and source hygiene** - -Run: - -```powershell -git diff --check -git status --short -git diff --no-ext-diff -- ClumsyPilot/Control ClumsyPilot/Trajectory ClumsyPilot/Shared ClumsyPilot/StateEstimation ClumsyPilot/Movements ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest ClumsyPilot/tests -``` - -Expected: no whitespace errors; only files named by this plan are staged or changed by this work. Existing unrelated modifications remain untouched. - -- [ ] **Step 4: Commit documentation** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md -git diff --cached --check -git commit -m "docs: explain EM closed-loop test operation" -``` - -- [ ] **Step 5: Hand off real-vehicle validation without claiming it passed** - -Report separately: - -```text -Automated status: adapter checks, observation regressions, structural lifecycle check, and plugin build. -Hardware status: not run by automated verification; requires a low-speed, short, single-direction field test with emergency stop available. -``` diff --git a/docs/superpowers/plans/2026-08-10-em-paths-monotonic-resampling-fix.md b/docs/superpowers/plans/2026-08-10-em-paths-monotonic-resampling-fix.md deleted file mode 100644 index 3149cea..0000000 --- a/docs/superpowers/plans/2026-08-10-em-paths-monotonic-resampling-fix.md +++ /dev/null @@ -1,166 +0,0 @@ -# EM PathS Monotonic Resampling Fix Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Normalize solver-sized `PathS` residuals during full-direction EM trajectory publication while rejecting material regressions. - -**Architecture:** `EmTrajectoryAssembler` passes `Validation.KinematicTolerance` into `TrajectorySampleSchedule`. Each output sample is normalized into `[previousPathS, terminalPathS]` only when the required correction is within that tolerance; larger violations retain fail-closed behavior with numeric diagnostics. - -**Tech Stack:** C#, .NET 10 verification host, netstandard2.0 plugin, `EMPlannerVerificationHost`. - -## Global Constraints - -- Do not modify `ClumsyPilot/Control`, `Shared`, `ClumsyPilot/StateEstimation`, `ClumsyPilot/Trajectory`, or controller Movement sources. -- Reuse `Validation.KinematicTolerance`; add no independent tolerance setting. -- Published `PathS` must be nondecreasing and no greater than terminal `PathS`. -- Violations greater than tolerance must still be rejected. - ---- - -### Task 1: Add PathS resampling regression coverage - -**Files:** -- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` - -**Interfaces:** -- Consumes: `EmTrajectoryAssembler.Assemble(...)` with `EmPlanningScope.FullDirectionSegment`. -- Produces: regression checks invoked by `TrajectoryChecks.Run()`. - -- [ ] **Step 1: Write the failing tolerance-sized regression test** - -Add `VerifiesFullScopeNormalizesToleranceSizedPathSResiduals()` to `Run()`. Construct four knots at `0.0, 0.1, 0.2, 0.3` seconds with `S = 0.0, 0.05, 0.05 - 0.5 * KinematicTolerance, 0.10`, use `RollingContinuation` plus `FullDirectionSegment`, assemble it, and assert every published `PathS` is at least its predecessor and no greater than `candidate.S[^1]`. - -Use this metadata helper signature so existing calls remain valid: - -```csharp -private static EmTrajectoryMetadata CreateMetadata(TravelDirection direction, EmTerminalType terminalType, - EmLongitudinalMode longitudinalMode = EmLongitudinalMode.ExactStopAtBoundary, - EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) -{ - return new EmTrajectoryMetadata("trajectory", DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch, 3L, - "reference", 4L, string.Empty, 2, direction, terminalType, longitudinalMode, planningScope); -} -``` - -- [ ] **Step 2: Write the material-regression protection test** - -Add `VerifiesFullScopeRejectsMaterialPathSRegressionWithDiagnostics()`. Use the same time knots with `S = 0.0, 0.05, 0.04, 0.10`; assert assembly throws `ArgumentException` and its message contains `sampleIndex=2`, `previousPathS=`, `candidatePathS=`, and `difference=`. - -- [ ] **Step 3: Run the focused test and verify RED** - -Run: - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -``` - -Expected: FAIL with `Trajectory PathS cannot decrease. (Parameter 'candidate')` in the tolerance-sized regression test. - -- [ ] **Step 4: Commit the red tests** - -```powershell -git add -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs -git commit -m "test: reproduce EM PathS resampling regression" -``` - -### Task 2: Implement bounded monotonic normalization - -**Files:** -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` -- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` - -**Interfaces:** -- Consumes: `EmPlannerConfiguration.Validation.KinematicTolerance`. -- Produces: `TrajectorySampleSchedule(..., bool resampleMotion, double pathSTolerance)`. - -- [ ] **Step 1: Pass the configured tolerance into the schedule** - -In `EmTrajectoryAssembler`, store and validate: - -```csharp -private readonly double pathSTolerance; - -pathSTolerance = configuration.Validation.KinematicTolerance; -if (!IsFinite(pathSTolerance) || pathSTolerance < 0d) - throw new ArgumentOutOfRangeException(nameof(configuration)); -``` - -Construct the schedule with: - -```csharp -var schedule = new TrajectorySampleSchedule(longitudinal.Candidate, outputTimeStepSeconds, - holdDurationSeconds, metadata.LongitudinalMode, isFullDirectionSegment, pathSTolerance); -``` - -- [ ] **Step 2: Normalize only tolerance-sized deviations** - -Add `using System.Globalization;`. Validate `pathSTolerance`, set `terminalPathS = candidate.S[candidate.S.Count - 1]`, and pass `samples.Count` into each `AddSample` call. Implement the core behavior: - -```csharp -double lowerDifference = previousPathS - sample.PathS; -double upperDifference = sample.PathS - terminalPathS; -if (lowerDifference > pathSTolerance || upperDifference > pathSTolerance) - throw PathSFailure(sampleIndex, previousPathS, sample.PathS, - Math.Max(lowerDifference, upperDifference), candidate); - -double normalizedPathS = Math.Min(terminalPathS, Math.Max(previousPathS, sample.PathS)); -if (normalizedPathS != sample.PathS) - sample = new TrajectorySample(sample.TimeFromStart, normalizedPathS, sample.ProgressSpeed, - sample.Acceleration, sample.Jerk, sample.IsHoldSample); -``` - -Retain the existing negative progress-speed check. Create diagnostics with invariant round-trip formatting: - -```csharp -private static ArgumentException PathSFailure(int sampleIndex, double previousPathS, - double candidatePathS, double difference, LongitudinalCandidate candidate) -{ - return new ArgumentException("Trajectory PathS violates monotonic publication bounds: sampleIndex=" + - sampleIndex + ";previousPathS=" + previousPathS.ToString("R", CultureInfo.InvariantCulture) + - ";candidatePathS=" + candidatePathS.ToString("R", CultureInfo.InvariantCulture) + - ";difference=" + difference.ToString("R", CultureInfo.InvariantCulture) + ".", nameof(candidate)); -} -``` - -- [ ] **Step 3: Run focused tests and verify GREEN** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory -``` - -Expected: `PASS trajectory`. - -- [ ] **Step 4: Commit the minimal production fix** - -```powershell -git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs -git commit -m "fix: normalize EM PathS resampling residuals" -``` - -### Task 3: Run integration regression and scoped verification - -**Files:** -- Verify only; no production edits expected. - -**Interfaces:** -- Consumes: the normalized `EmTrajectory` publication behavior. -- Produces: evidence that planning, observation, adapter, and plugin compilation remain valid. - -- [ ] **Step 1: Run EM integration checks** - -```powershell -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation -dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-control-adapter -``` - -Expected: all three commands print `PASS`. - -- [ ] **Step 2: Build and inspect only the scoped files** - -```powershell -dotnet build ClumsyPilot/ClumsyPilot.csproj -c Release --no-restore -git diff --check -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs -``` - -Expected: zero build errors; only the two known obsolete-API warnings may remain; diff check emits no errors. diff --git a/docs/superpowers/plans/2026-08-10-trajectory-planning-flow-demo.md b/docs/superpowers/plans/2026-08-10-trajectory-planning-flow-demo.md deleted file mode 100644 index 2ec0fb4..0000000 --- a/docs/superpowers/plans/2026-08-10-trajectory-planning-flow-demo.md +++ /dev/null @@ -1,179 +0,0 @@ -# Trajectory Planning Flow Demo Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build an offline single-page Chinese interactive demo that explains the map-to-control trajectory-planning evidence chain. - -**Architecture:** A standalone HTML file embeds coherent illustrative map, coarse-path, smoothing-path, EM-trajectory and control-command data. Inline CSS and JavaScript render a fixed-coordinate SVG map, stage-specific layers, time-based vehicle playback, data contracts and control-output semantics without network or project-runtime dependencies. A PowerShell verifier asserts required content before browser review. - -**Tech Stack:** HTML5, inline CSS, vanilla JavaScript, inline SVG, PowerShell 7 verification. - -## Global Constraints - -- Create exactly `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html`. -- Local-file operation only: no `http://`, `https://`, `fetch`, XHR or WebSocket. -- Use illustrative metric data; do not claim OSQP, localization, hardware or controller execution. -- Use existing `EmTrajectory` / `TrajectoryControlCommand` field names verbatim. -- Preserve and never stage unrelated user changes. - ---- - -### Task 1: Add a deterministic web-asset contract verifier - -**Files:** -- Create: `ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1` -- Test: `ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1` - -**Interfaces:** -- Consumes UTF-8 text from `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html`. -- Produces exit code `0` and `Trajectory planning flow demo checks passed.` only when all page contracts pass. - -- [ ] **Step 1: Write the failing test** - -```powershell -$ErrorActionPreference = 'Stop' -$page = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Trajplanner_output\trajectory-planning-flow-demo.html' -if (-not (Test-Path -LiteralPath $page)) { throw "Missing demo page: $page" } -$html = Get-Content -LiteralPath $page -Raw -Encoding UTF8 -$required = @('轨迹规划全链路 Demo', 'data-stage="map"', 'data-stage="coarse"', 'data-stage="smooth"', 'data-stage="em"', 'data-stage="control"', 'id="planning-map"', 'id="timeline"', 'Hybrid A* 粗路径', 'Local G2 平滑路径', 'EmTrajectory', 'TrajectoryControlCommand', 'mapSnapshotId', 'referencePathId', 'SignedLongitudinalVelocity', 'RequestDirectionChange', 'HoldBrake', 'IsTrajectoryComplete', 'function render()', 'function controlCommandForPoint(', 'addEventListener') -foreach ($token in $required) { if (-not $html.Contains($token)) { throw "Missing required demo contract: $token" } } -foreach ($forbidden in @('http://', 'https://', 'fetch(', 'XMLHttpRequest', 'WebSocket')) { if ($html.Contains($forbidden)) { throw "Demo must remain offline: $forbidden" } } -Write-Output 'Trajectory planning flow demo checks passed.' -``` - -- [ ] **Step 2: Run the test and verify initial failure** - -Run `pwsh -NoProfile -File ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1`. - -Expected: `Missing demo page`. - -- [ ] **Step 3: Commit the test** - -Run `git add -- ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1` followed by `git commit -m "test: verify trajectory planning flow demo"`. - -### Task 2: Implement the offline stage-and-playback page - -**Files:** -- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html` -- Test: `ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1` - -**Interfaces:** -- Consumes static `identity`, `map`, `coarsePath`, `smoothPath`, and `trajectory` constants. -- Produces `render()` which updates SVG layers, selected point, stage JSON, control command and boundary explanation. - -- [ ] **Step 1: Implement the responsive semantic shell** - -Use this exact control structure: - -```html - - - -

-

-```
-
-Inline CSS must make the map dominant, panels stack at narrow width, and identify every layer with text plus distinct marker/line pattern. Include visible units `m`, `rad`, `m/s`, `rad/s` and a label that data is illustrative. Do not import fonts, assets or libraries.
-
-- [ ] **Step 2: Implement internally consistent data and the exact control adapter**
-
-Create frozen `identity` fields: `mapSnapshotId`, `referencePathId`, `vehicleStateSequenceId`, `trajectoryId`, `effectiveAtUtc`. Use a 4 m by 3 m map with three obstacles; include a forward coarse/smooth segment, a shared gear-switch pose, and reverse parking into the Goal. Implement `trajectory` with six time samples at `0`, `1.0`, `2.0`, `2.2`, `3.2`, `4.3` seconds; samples `2.0` and `2.2` share the gear pose and Goal is the last sample. Each point contains `x`, `y`, `yaw`, `signedVelocity`, `acceleration`, `yawRate`, `direction`, `segmentIndex`, `boundaryType`.
-
-Implement this declared contract exactly:
-
-```javascript
-function controlCommandForPoint(point) {
-  const holding = point.boundaryType === "GearSwitchApproach" ||
-    point.boundaryType === "GearSwitchDeparture" || point.boundaryType === "Goal";
-  return {
-    SignedLongitudinalVelocity: holding ? 0 : point.signedVelocity,
-    YawRate: holding ? 0 : point.yawRate,
-    Direction: point.direction,
-    RequestDirectionChange: point.boundaryType === "GearSwitchApproach",
-    HoldBrake: holding,
-    IsTrajectoryComplete: point.boundaryType === "Goal"
-  };
-}
-```
-
-- [ ] **Step 3: Implement SVG render behavior**
-
-Implement `function render()` using one world-to-screen transform. Always draw map boundary, three obstacles, start/goal and current vehicle. For `map`, show only map information; for `coarse`, add discrete `Hybrid A* 粗路径` nodes and direction arrows; for `smooth`, add the `Local G2 平滑路径`, segment labels and curvature text; for `em` and `control`, add timed `EmTrajectory` points, selected-point highlight and heading arrow. Update `#data-sample` with stage-specific JSON. Update `#control-sample` with `controlCommandForPoint(selectedPoint)` only in control stage, otherwise state that control does not consume the stage yet.
-
-Every view must visibly state `Success` or `SuccessWithFallback` plus a complete trajectory is required for publication. Control view must state coarse/smoothed path is observability evidence, not a controller command.
-
-- [ ] **Step 4: Implement local interactions**
-
-Click handlers on `[data-stage]` update `aria-pressed`, visibility and `render()`. An `input` handler on `#timeline` selects the trajectory point and calls `render()`. Disable timeline in map/coarse/smooth; initialize stage `em`, selected point `0`, then call `render()` exactly once after event binding.
-
-- [ ] **Step 5: Run test and perform browser acceptance checks**
-
-Run `pwsh -NoProfile -File ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1`.
-
-Expected: `Trajectory planning flow demo checks passed.`
-
-Open the local page. Verify all five stage buttons change layers/data; EM/control timeline updates vehicle pose; gear sample returns zero speed/yaw rate plus `RequestDirectionChange: true` and `HoldBrake: true`; Goal returns zero speed/yaw rate plus `HoldBrake: true` and `IsTrajectoryComplete: true`; mobile reflow has no horizontal clipping.
-
-- [ ] **Step 6: Commit the page**
-
-Run `git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html` followed by `git commit -m "feat: add trajectory planning flow demo"`.
-
-### Task 3: Document the relationship to the executable output demo
-
-**Files:**
-- Modify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md`
-- Modify: `ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1`
-- Test: `ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1`
-
-**Interfaces:**
-- Consumes the local HTML page and `TrajectoryOutputDemoRunner` documentation.
-- Produces a README link that identifies the page as an illustrative observability/interface explainer, never a live planner.
-
-- [ ] **Step 1: Extend the test with a failing README assertion**
-
-Before final output, add:
-
-```powershell
-$readme = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Trajplanner_output\README.md') -Raw -Encoding UTF8
-if (-not $readme.Contains('trajectory-planning-flow-demo.html')) { throw 'README must link to the trajectory-planning-flow demo.' }
-```
-
-- [ ] **Step 2: Run the test and verify initial README failure**
-
-Run `pwsh -NoProfile -File ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1`.
-
-Expected: `README must link`.
-
-- [ ] **Step 3: Add the exact README content**
-
-Add after the existing data-flow diagram:
-
-```markdown
-## 全链路可视化(Offline Flow Demo)
-
-双击打开 [trajectory-planning-flow-demo.html](trajectory-planning-flow-demo.html),可在同一米制地图中按阶段查看地图、Hybrid A* 粗路径、Local G2 平滑路径、`EmTrajectory` 和控制层采样命令。该页面使用内置说明数据,帮助理解版本身份与数据边界;它不调用 OSQP、定位、硬件或真实控制器。
-
-控制器只消费已验证完整轨迹的采样结果或执行层导出的 `TrajectoryControlCommand`。粗路径、平滑路径、地图和规划诊断属于观测、回放和问题定位证据链,不是逐周期的底盘命令。
-```
-
-- [ ] **Step 4: Run final checks**
-
-Run `pwsh -NoProfile -File ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1` and `dotnet run --project ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/Tests/TrajectoryOutputDemo.Tests.csproj`.
-
-Expected: verifier passes and existing output-demo self-check succeeds.
-
-- [ ] **Step 5: Commit documentation and final verifier**
-
-Run `git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/README.md ClumsyPilot/tests/verify_trajectory_planning_flow_demo.ps1` followed by `git commit -m "docs: explain trajectory planning flow demo"`.
-
-## Plan self-review
-
-- Spec coverage: Tasks 1–2 cover offline behavior, five-stage navigation, fixed-coordinate map, planning artifacts, version identity, playback, gear-switch behavior and control-command contract. Task 3 makes the Demo discoverable and separates observability data from controller inputs.
-- Placeholder scan: no task relies on unspecified files, functions, test expectations or external services.
-- Type consistency: JavaScript and verifier use exactly `SignedLongitudinalVelocity`, `YawRate`, `Direction`, `RequestDirectionChange`, `HoldBrake`, `IsTrajectoryComplete`; README names the same boundary.
diff --git a/docs/superpowers/plans/2026-08-11-em-planner-controller-handoff-guide.md b/docs/superpowers/plans/2026-08-11-em-planner-controller-handoff-guide.md
deleted file mode 100644
index 04e25ba..0000000
--- a/docs/superpowers/plans/2026-08-11-em-planner-controller-handoff-guide.md
+++ /dev/null
@@ -1,499 +0,0 @@
-# EM Planner Controller Handoff Guide Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Add a detailed Chinese README and two fully annotated, copy-readable C# examples that explain the fixed offline planning pipeline and replace the controller test's artificial `Trajectory2D` with an EM-planned trajectory.
-
-**Architecture:** The README owns concepts, configuration locations, units, output contracts, failure semantics, and the numbered walkthrough. `EmPlannerFullPipelineDemo.cs` mirrors those numbers as one continuous offline data-flow example. `EmPlannerTrajectoryReplacementExample.cs` narrows the same contract to the exact `TestTrajectoryFactory` replacement boundary used by the external closed-loop tests.
-
-**Tech Stack:** Markdown, C# source examples, existing `MultiWheelC.TrajectoryPlanning` contracts, Hybrid A*, Local G2 smoothing, `EmPlanningService`, OSQP, `EmControlTrajectoryAdapter`, and `TrajectoryTrackingMovement`.
-
-## Global Constraints
-
-- Deliver copy-readable C# only; do not add a project file or claim the examples are standalone executables.
-- Use a fixed offline start, goal, vehicle, and obstacle scene; do not read live localization or send chassis commands.
-- Keep every planning step explicit and fully annotated in Chinese; do not use ellipses or placeholder APIs.
-- Match current repository type names, constructor signatures, enum values, properties, units, and status semantics.
-- Treat `EmPlanningService` as an in-process synchronous service returning `EmPlanningResult` from `Plan(request, cancellationToken)`.
-- Publish to the controller only when status is `Success` or `SuccessWithFallback` and the result contains a complete non-null trajectory.
-- Keep single-direction `Trajectory2D` consumption separate from multi-direction gear-switch orchestration.
-- Preserve the existing `trajectory-planning-flow-demo.html`; reference it as an optional visual companion.
-- Do not modify the external `停车机器人-合并测试` tree or unrelated dirty-worktree files.
-
----
-
-### Task 1: Write the main Chinese handoff guide
-
-**Files:**
-- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/README.md`
-- Reference: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`
-- Reference: `ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md`
-- Reference: `D:/Users/Desktop/项目/prakrobot/停车机器人-合并测试/parkr_shen/MultiWheelC/Movements/TrajectoryTrackingMovement.cs`
-- Reference: `D:/Users/Desktop/项目/prakrobot/停车机器人-合并测试/parkr_shen/MultiWheelC/Experiments/NewControllerTrackingTests.cs`
-
-**Interfaces:**
-- Consumes: the approved design in `docs/superpowers/specs/2026-08-11-em-planner-controller-handoff-guide-design.md` and current public planner/controller contracts.
-- Produces: numbered sections `步骤 1` through `步骤 15`, configuration tables, output-field tables, status handling, and exact links to both C# examples.
-
-- [ ] **Step 1: Record the failing documentation-presence check**
-
-Run:
-
-```powershell
-Test-Path 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide\README.md'
-```
-
-Expected before implementation: `False`.
-
-- [ ] **Step 2: Create the README with the integration boundary first**
-
-Start the document with this exact contract:
-
-```markdown
-# 轨迹规划服务调用与闭环控制器交接指南
-
-> 本指南展示固定离线场景的完整调用顺序。代码只用于复制阅读和接口交接,不会读取实车定位,也不会向底盘发送命令。
-
-| 边界 | 类型/方法 | 含义 |
-| --- | --- | --- |
-| EMplanner 正式输出 | `EmTrajectory` | 带时间、位姿、曲率、速度、加速度、jerk 与方向边界的不可变轨迹 |
-| 闭环控制器正式输入 | `Trajectory2D` | 几何控制器消费的世界位姿、弧长、曲率和有符号参考速度 |
-| 唯一适配入口 | `EmControlTrajectoryAdapter.Create(EmTrajectory)` | 将单方向 EM 轨迹转换为控制器轨迹 |
-
-`EmPlanningService` 是进程内同步服务。调用方创建冻结的 `EmPlanningRequest`,调用 `Plan(request, cancellationToken)`,并仅从本次返回的成功 `EmPlanningResult` 中索取 `Trajectory`。
-```
-
-- [ ] **Step 3: Add the complete numbered data flow**
-
-Add sections `步骤 1` through `步骤 15` in the exact order from the approved design. Each section must include four labeled paragraphs: `本步输入`、`调用`、`本步输出`、`失败处理`. Link every step to the matching numbered region in `EmPlannerFullPipelineDemo.cs`.
-
-The flow summary must be:
-
-```text
-固定场景
-  → PlanningGridMap
-  → PlanningRequest
-  → HybridAStarPlanner.Plan
-  → PathSmoothingRequest
-  → PathSmoothingService.Smooth
-  → 当前 DirectionSegmentView
-  → EmPlanningRequest
-  → EmPlanningService.Plan
-  → EmPlanningResult.Trajectory
-  → EmControlTrajectoryAdapter.Create
-  → Trajectory2D
-  → TrajectoryTrackingMovement
-```
-
-- [ ] **Step 4: Add configuration source and meaning tables**
-
-Create four tables: `粗路径配置`、`Local G2 平滑配置`、`EM 配置`、`闭环控制配置`. Every row must include `参数`、`源码位置`、`单位`、`固定 demo 值/当前默认值`、`含义`、`调大/调小影响`.
-
-At minimum cover:
-
-```text
-VehicleParameters.LengthMeters / WidthMeters / SafetyMarginMeters / MaximumCurvaturePerMeter
-HybridAStarConfiguration primitive length, heading bins, goal tolerances, search limits
-PathSmoothingConfiguration.OutputSpacingMeters / MaximumCollisionCheckStepMeters / MinimumClearanceReserveMeters
-LocalG2QuinticOptions window lengths, deviation, curvature jump, improvement and candidate limits
-EmPlannerConfiguration.Corridor / Frenet / Lateral / Longitudinal / Solver / Scheduling / Validation
-TrajectoryTrackingMovement Stanley gains, PID gains, speed limit, finish tolerances, deviation limit, timeout
-```
-
-Add a dedicated unit table that explicitly distinguishes `m` vs `mm`, `rad` vs `deg`, `1/m`, `m/s`, `m/s²`, `m/s³`, `rad/s`, world frame, body frame, and signed longitudinal speed.
-
-- [ ] **Step 5: Add complete result and field contracts**
-
-Document `EmPlanningResult`, `EmTrajectoryMetadata`, and every public `EmTrajectoryPoint` field that affects downstream interpretation. Include this warning verbatim:
-
-```markdown
-`VelocityX` 和 `VelocityY` 是世界坐标系中的预测速度分量,不是底盘纵向/横向命令;闭环测试不得把它们直接发送给车辆。
-```
-
-Explain that `EmControlTrajectoryAdapter` maps `X/Y/Yaw`, `VehicleCurvature`, and `SignedLongitudinalVelocity`, while rebuilding controller arc length from world positions.
-
-- [ ] **Step 6: Add service request, success, failure, and gear-switch guidance**
-
-Show the exact retrieval pattern:
-
-```csharp
-var planningService = new EmPlanningService(new OsqpNativeSolver());
-EmPlanningResult result = planningService.Plan(request, cancellationToken);
-
-bool succeeded = result.Status == EmPlanningStatus.Success ||
-    result.Status == EmPlanningStatus.SuccessWithFallback;
-if (!succeeded || result.Trajectory == null)
-{
-    throw new InvalidOperationException(
-        "EM 规划未返回可交给控制器的完整轨迹:" + result.FailureReason);
-}
-
-EmTrajectory emTrajectory = result.Trajectory;
-Trajectory2D controllerTrajectory =
-    new EmControlTrajectoryAdapter().Create(emTrajectory);
-```
-
-Add a status decision table covering invalid input, infeasible planning, cancellation, deadline/timeout, empty trajectory, direction mismatch, and adapter rejection. Explain that the fixed demo is single-direction; a gear-switch boundary requires a full stop and a separately confirmed next-direction plan rather than concatenation into one `Trajectory2D`.
-
-- [ ] **Step 7: Add the external-test replacement map and reading order**
-
-Identify the current artificial source as `TestTrajectoryFactory.CreateStraight4Meters` in `NewControllerTrackingTests.cs`, and the controller sink as `TrajectoryTrackingMovement.Trajectory`. State that only the trajectory-source block changes; the existing state provider and controller tuning remain owned by the test.
-
-End with:
-
-```markdown
-## 推荐阅读顺序
-
-1. 先完整阅读本 README。
-2. 顺序阅读 `EmPlannerFullPipelineDemo.cs` 的步骤 1–15。
-3. 最后复制 `EmPlannerTrajectoryReplacementExample.cs` 中与目标测试相符的替换方式。
-4. 如需流程图,再打开 `trajectory-planning-flow-demo.html`;HTML 不是接口规范。
-```
-
-- [ ] **Step 8: Run the README structural check**
-
-Run:
-
-```powershell
-$guide = 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide\README.md'
-Test-Path $guide
-rg -n "步骤 1|步骤 15|EmPlanningService|EmPlanningResult|EmTrajectoryPoint|Trajectory2D|TestTrajectoryFactory|VelocityX|换向|参数" $guide
-```
-
-Expected: `True`; every required concept has at least one match, and steps 1 and 15 are present.
-
-- [ ] **Step 9: Commit the README**
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/README.md
-git commit -m "docs: add EM planner controller handoff guide"
-```
-
-### Task 2: Add the fully annotated fixed offline pipeline example
-
-**Files:**
-- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerFullPipelineDemo.cs`
-- Reference: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs`
-- Reference: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs`
-
-**Interfaces:**
-- Consumes: real coarse-path, Local G2, EM, and control-adapter APIs; README steps 1–15.
-- Produces: `EmPlannerFullPipelineDemo.CreateFixedOfflineTrajectory(CancellationToken)` returning a `FixedOfflinePlanningOutput` that exposes both `EmTrajectory` and `Trajectory2D`.
-
-- [ ] **Step 1: Record the failing example-presence check**
-
-Run:
-
-```powershell
-Test-Path 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide\EmPlannerFullPipelineDemo.cs'
-```
-
-Expected before implementation: `False`.
-
-- [ ] **Step 2: Create the example shell and explicit output contract**
-
-Use this public shape:
-
-```csharp
-public sealed class FixedOfflinePlanningOutput
-{
-    public FixedOfflinePlanningOutput(
-        EmTrajectory emTrajectory,
-        Trajectory2D controllerTrajectory)
-    {
-        EmTrajectory = emTrajectory ??
-            throw new ArgumentNullException(nameof(emTrajectory));
-        ControllerTrajectory = controllerTrajectory ??
-            throw new ArgumentNullException(nameof(controllerTrajectory));
-    }
-
-    public EmTrajectory EmTrajectory { get; }
-    public Trajectory2D ControllerTrajectory { get; }
-}
-
-public static class EmPlannerFullPipelineDemo
-{
-    public static FixedOfflinePlanningOutput CreateFixedOfflineTrajectory(
-        CancellationToken cancellationToken);
-}
-```
-
-The declaration above defines the reader-facing signature. The final class must give that method the complete numbered body produced by steps 3–6; it must not remain declaration-only.
-
-- [ ] **Step 3: Implement steps 1–5 with a complete fixed scene and coarse plan**
-
-Use explicit SI-valued constants for start, goal, vehicle length/width, safety margin, curvature limit, map resolution, and at least one fixed obstacle. Construct the map with `MapBoundsMm`, `CircleObstacle` or `AxisAlignedRectangleObstacle`, `ManualObstacleSource`, `PlanningMapRequest`, and `new PlanningMapFactory().Create(mapRequest)`. Require `PlanningMapBuildResult.Succeeded`, `Map != null`, and `Map.PlanningReady`. Then construct `PlanningRequest` and call `new HybridAStarPlanner().Plan(request, cancellationToken)`.
-
-Reject non-success with diagnostics:
-
-```csharp
-if (coarseResult.Status != PlanningStatus.Success)
-{
-    throw new InvalidOperationException(
-        "Hybrid A* 粗路径失败:" +
-        coarseResult.Diagnostics.TerminationReason);
-}
-```
-
-- [ ] **Step 4: Implement steps 6–7 with Local G2 and a single active direction segment**
-
-Construct `PathSmoothingRequest` from the successful coarse path, segments, frozen map, vehicle, and explicit `PathSmoothingConfiguration`. Call `new PathSmoothingService().Smooth(smoothingRequest, cancellationToken)`. Accept only `Complete`, `PartialImprovement`, `NotNeeded`, or `Unchanged`, and reject empty path/segment results.
-
-Call `ReferencePathSegmenter.Create(smoothingResult)`, select segment index `0`, and assert that the fixed scene remains one direction. If the result contains more than one direction segment, throw an exception explaining that this fixed controller demo intentionally does not cross a gear-switch boundary.
-
-- [ ] **Step 5: Implement steps 8–11 with state, configuration, request, and service call**
-
-Use zero initial signed speed/acceleration and the fixed start pose for `VehicleMotionState`. Create `EmPlannerConfiguration` with explicit demo overrides, copy the repository's request-construction order, set `EmPlanningScope.FullDirectionSegment`, and invoke:
-
-```csharp
-var planningService = new EmPlanningService(new OsqpNativeSolver());
-EmPlanningResult emResult = planningService.Plan(
-    emRequest,
-    cancellationToken);
-```
-
-Every constructor argument must have an adjacent Chinese comment giving its source and unit where applicable.
-
-- [ ] **Step 6: Implement steps 12–15 with validation, field reading, adaptation, and return**
-
-Use this publication gate:
-
-```csharp
-bool emSucceeded = emResult.Status == EmPlanningStatus.Success ||
-    emResult.Status == EmPlanningStatus.SuccessWithFallback;
-if (!emSucceeded ||
-    emResult.Trajectory == null ||
-    emResult.Trajectory.Points.Count < 2)
-{
-    throw new InvalidOperationException(
-        "EM 规划没有返回完整可控轨迹:" + emResult.FailureReason);
-}
-```
-
-Iterate through the points and assign local variables for time, `X/Y/Yaw`, curvature, `PathS`, signed speed, world velocity components, acceleration, jerk, and yaw rate. The comments must explain that this is inspection, not command output.
-
-Finish with:
-
-```csharp
-EmTrajectory emTrajectory = emResult.Trajectory;
-Trajectory2D controllerTrajectory =
-    new EmControlTrajectoryAdapter().Create(emTrajectory);
-
-return new FixedOfflinePlanningOutput(
-    emTrajectory,
-    controllerTrajectory);
-```
-
-- [ ] **Step 7: Verify numbered coverage and banned placeholders**
-
-Run:
-
-```powershell
-$demo = 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide\EmPlannerFullPipelineDemo.cs'
-1..15 | ForEach-Object {
-    if (-not (Select-String -Path $demo -Pattern ("步骤 " + $_) -Quiet)) {
-        throw "缺少步骤 $_"
-    }
-}
-rg -n "TODO|TBD|[.][.][.]|NotImplementedException" $demo
-```
-
-Expected: all numbered checks complete without an exception; `rg` prints no matches and exits with code `1` because the banned patterns are absent.
-
-- [ ] **Step 8: Cross-check every API token against source**
-
-Run targeted searches for every constructed public type and method:
-
-```powershell
-rg -n "class (PlanningRequest|HybridAStarPlanner|PathSmoothingRequest|PathSmoothingService|EmPlanningRequest|EmPlanningService|EmControlTrajectoryAdapter)|Plan\(|Smooth\(|Create\(" ClumsyPilot/ParkrobTrajplanner -g '*.cs'
-```
-
-Expected: each type and call used by the example resolves to a current repository declaration; adjust the example to the source signature before continuing.
-
-- [ ] **Step 9: Commit the full pipeline example**
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerFullPipelineDemo.cs
-git commit -m "docs: add annotated EM planning pipeline example"
-```
-
-### Task 3: Add the closed-loop test replacement example
-
-**Files:**
-- Create: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerTrajectoryReplacementExample.cs`
-- Reference: `D:/Users/Desktop/项目/prakrobot/停车机器人-合并测试/parkr_shen/MultiWheelC/Experiments/NewControllerTrackingTests.cs`
-- Reference: `D:/Users/Desktop/项目/prakrobot/停车机器人-合并测试/parkr_shen/MultiWheelC/Movements/TrajectoryTrackingMovement.cs`
-- Reference: `D:/Users/Desktop/项目/prakrobot/停车机器人-合并测试/parkr_shen/MultiWheelC/Movements/MotionPlanExecutor.cs`
-
-**Interfaces:**
-- Consumes: `FixedOfflinePlanningOutput`, `EmTrajectory`, `EmControlTrajectoryAdapter`, `Trajectory2D`, and the external test's existing state-provider/controller setup.
-- Produces: exact before/after source blocks plus direct `TrajectoryTrackingMovement` and `TrackMotionPlanSegment` consumption examples.
-
-- [ ] **Step 1: Record the failing replacement-example presence check**
-
-Run:
-
-```powershell
-Test-Path 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide\EmPlannerTrajectoryReplacementExample.cs'
-```
-
-Expected before implementation: `False`.
-
-- [ ] **Step 2: Add a fully parameterized before block**
-
-Copy the current artificial trajectory call without abbreviating arguments:
-
-```csharp
-Trajectory2D trajectory =
-    TestTrajectoryFactory.CreateStraight4Meters(
-        trajectoryStartPose,
-        CruiseSpeedMetersPerSecond,
-        AccelerationMetersPerSecondSquared,
-        DecelerationMetersPerSecondSquared,
-        PointSpacingMeters);
-```
-
-Comment that this line generates controller test data and does not call the formal planner.
-
-- [ ] **Step 3: Add the after block and explain how the test obtains the result**
-
-Use:
-
-```csharp
-FixedOfflinePlanningOutput planningOutput =
-    EmPlannerFullPipelineDemo.CreateFixedOfflineTrajectory(
-        cancellationToken);
-
-EmTrajectory emTrajectory = planningOutput.EmTrajectory;
-Trajectory2D trajectory = planningOutput.ControllerTrajectory;
-```
-
-Explain that the full demo internally performs the synchronous `EmPlanningService.Plan` call and publication gate. Also show the equivalent direct retrieval block with `EmPlanningResult`, status check, `result.Trajectory`, and `EmControlTrajectoryAdapter.Create` so the reader understands how to extract the trajectory without using the demo wrapper.
-
-- [ ] **Step 4: Add both controller consumption paths without changing tuning ownership**
-
-Direct movement:
-
-```csharp
-var movement = new TrajectoryTrackingMovement
-{
-    Trajectory = trajectory,
-    StateProvider = stateProvider
-};
-```
-
-Composite movement:
-
-```csharp
-var segment = new TrackMotionPlanSegment(trajectory);
-```
-
-Comment that existing `ConfigureTrackingMovement`, finish tolerances, observers, state provider, and safety checks stay in the controller test; only the trajectory source changes.
-
-- [ ] **Step 5: Add rejection examples for failure and gear switch**
-
-Include explicit guards that reject null/failed EM results and state that a single `TrajectoryTrackingMovement` invocation consumes one direction segment. Show an exception for `EmTerminalType.GearSwitch` unless the surrounding test has implemented and safety-reviewed the stop-and-direction-confirmation sequence.
-
-- [ ] **Step 6: Verify the replacement map**
-
-Run:
-
-```powershell
-$example = 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide\EmPlannerTrajectoryReplacementExample.cs'
-rg -n "TestTrajectoryFactory.CreateStraight4Meters|EmPlanningService|EmPlanningResult|result.Trajectory|EmControlTrajectoryAdapter|TrajectoryTrackingMovement|TrackMotionPlanSegment|GearSwitch" $example
-rg -n "TestTrajectoryFactory.CreateStraight4Meters|Trajectory = trajectory" 'D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC' -g '*.cs'
-```
-
-Expected: the guide contains every acquisition and consumption boundary; the external tree still shows its original test source and sink because it was not modified.
-
-- [ ] **Step 7: Commit the replacement example**
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerTrajectoryReplacementExample.cs
-git commit -m "docs: show EM trajectory replacement in controller tests"
-```
-
-### Task 4: Verify the complete guide as one handoff artifact
-
-**Files:**
-- Verify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/README.md`
-- Verify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerFullPipelineDemo.cs`
-- Verify: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerTrajectoryReplacementExample.cs`
-- Preserve: `ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/trajectory-planning-flow-demo.html`
-
-**Interfaces:**
-- Consumes: all three guide deliverables and the real planner/controller sources.
-- Produces: fresh evidence that the guide is complete, cross-linked, placeholder-free, and limited to the intended files.
-
-- [ ] **Step 1: Verify all expected files and cross-links**
-
-Run:
-
-```powershell
-$root = 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide'
-@(
-    "$root\README.md",
-    "$root\EmPlannerFullPipelineDemo.cs",
-    "$root\EmPlannerTrajectoryReplacementExample.cs",
-    "$root\trajectory-planning-flow-demo.html"
-) | ForEach-Object {
-    if (-not (Test-Path $_)) { throw "缺少指南文件:$_" }
-}
-rg -n "EmPlannerFullPipelineDemo.cs|EmPlannerTrajectoryReplacementExample.cs|trajectory-planning-flow-demo.html" "$root\README.md"
-```
-
-Expected: all files exist and README links to both C# examples and the HTML companion.
-
-- [ ] **Step 2: Verify completeness and placeholder absence**
-
-Run:
-
-```powershell
-$root = 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide'
-rg -n "TODO|TBD|NotImplementedException|[.][.][.]" "$root\README.md" "$root\EmPlannerFullPipelineDemo.cs" "$root\EmPlannerTrajectoryReplacementExample.cs"
-```
-
-Expected: no matches; `rg` exits with code `1`.
-
-- [ ] **Step 3: Verify documentation-source correspondence**
-
-Run:
-
-```powershell
-$root = 'ClumsyPilot\ParkrobTrajplanner\Trajplanner_guide'
-rg -n "PlanningGridMap|HybridAStarPlanner|PathSmoothingService|EmPlanningRequest|EmPlanningService|EmTrajectoryPoint|EmControlTrajectoryAdapter" "$root\README.md" "$root\EmPlannerFullPipelineDemo.cs"
-rg -n "class (HybridAStarPlanner|PathSmoothingService|EmPlanningRequest|EmPlanningService|EmTrajectoryPoint|EmControlTrajectoryAdapter)" ClumsyPilot/ParkrobTrajplanner -g '*.cs'
-```
-
-Expected: every documented integration type is present in the guide and resolves to a real repository declaration.
-
-- [ ] **Step 4: Run repository-level static checks**
-
-Run:
-
-```powershell
-git diff --check
-dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
-```
-
-Expected: `git diff --check` exits `0`; build exits `0`. If the already-dirty worktree causes an unrelated build failure, record the exact failing project/file and separately report that the guide's static checks passed.
-
-- [ ] **Step 5: Audit scope and final diff**
-
-Run:
-
-```powershell
-git status --short
-git diff --no-ext-diff -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide docs/superpowers/plans/2026-08-11-em-planner-controller-handoff-guide.md
-```
-
-Expected: guide changes are limited to README and the two new C# examples; the existing HTML remains preserved. Unrelated pre-existing changes remain untouched.
-
-- [ ] **Step 6: Commit any final guide-only corrections**
-
-If verification required guide corrections, stage only the three guide deliverables and commit:
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/README.md ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerFullPipelineDemo.cs ClumsyPilot/ParkrobTrajplanner/Trajplanner_guide/EmPlannerTrajectoryReplacementExample.cs
-git commit -m "docs: finalize EM planner trajectory handoff guide"
-```
diff --git a/docs/superpowers/progress/em-planner-progress.md b/docs/superpowers/progress/em-planner-progress.md
deleted file mode 100644
index ce29f04..0000000
--- a/docs/superpowers/progress/em-planner-progress.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# EM Planner Execution Progress
-
-## Current State
-
-- Current stage: 10 - generic control adaptation, plugin packaging, and final rolling gate
-- Stage status: Completed
-- Current branch: `trajplanner`
-- Last checkpoint commit: `4058230` (last Stage 10 functional commit; this file is the final Stage 10 checkpoint)
-- Protocol design baseline: `554c84f`
-- Implementation-plan baseline: `8dd8ff0`
-
-## Historical Stage 8 Current State
-
-- Current stage: 8 — complete trajectory publication and pure one-shot planning service
-- Stage status: Completed
-- Current branch: `trajplanner`
-- Last checkpoint commit: `019b896` (last Stage 8 functional commit; this file is the Stage 8 checkpoint)
-- Protocol design baseline: `554c84f`
-- Implementation-plan baseline: `8dd8ff0`
-
-## Completed Tasks
-
-| Stage | Plan | Tasks | Commits | Verification |
-|---:|---|---|---|---|
-| 10 | `2026-08-03-em-planner-rolling-execution-implementation.md` | Task 4 - Generic Control Adapter | `aa51ae2` | RED: `executor` exited 1 with missing `TrajectoryControlAdapter` and `IVehicleStateProvider`. GREEN: `PASS executor` verifies exact forward/reverse signed-velocity and yaw-rate mapping, telemetry preservation, zero holds, one-shot direction requests, no lateral body command, and no in-place rotation. |
-| 10 | `2026-08-03-em-planner-rolling-execution-implementation.md` | Task 5 - Plugin Output and License Packaging | `3c84e36` | RED: `plugin-package` exited 1 because `Publish-ClumsyPilotPlugin.ps1` was absent. GREEN: `PASS plugin-package` verifies build-output metadata, managed `ClumsyPilot.dll`, x64/hash-checked `osqp.dll`, the exact three-license tree, and repeat publication with no stale staging directory. |
-| 10 | `2026-08-03-em-planner-rolling-execution-implementation.md` | Task 6 - Rolling End-to-End and Final Gate | `4058230` | RED: `em-all` exited 2 because its command route did not exist. GREEN: `PASS rolling-end-to-end` and `PASS rolling-execution-tail` cover forward/reverse publication, supersession, unsafe-tracking reset, gear dwell, goal stop, and failure continuation to a non-extrapolated zero-speed tail. |
-| 9 | `2026-08-03-em-planner-rolling-execution-implementation.md` | Task 1 - Cycle Identity, Scheduling Decision, and Stale-Result Suppression | `d75380c` | RED: the authorized inherited coordinator draft threw `ObjectDisposedException` when a completed current cycle was followed by another cycle. GREEN: `coordinator` printed `PASS coordinator`; the exact 100-run planned loop completed with exit 0 and 100 PASS lines. |
-| 9 | `2026-08-03-em-planner-rolling-execution-implementation.md` | Task 2 - Safe Previous-Trajectory Handoff | `55119de` | RED: `coordinator` exited 1 with missing `TrajectoryHandoffSelector` contracts. GREEN: `PASS coordinator` covers forward/reverse same-segment interpolation, age/tracking/terminal/segment/direction/beyond-trajectory/gear-boundary rejection, no cross-boundary interpolation, and coordinator consumption of its immutable published trajectory. |
-| 9 | `2026-08-03-em-planner-rolling-execution-implementation.md` | Task 3 - Gear-Switch State Machine and Trajectory Executor | `de402e6` | RED: `executor` exited 1 with missing gear-switch and execution-state types. GREEN: `PASS executor` covers forward-to-reverse and reverse-to-forward zero-speed dwell, one-shot request, confirmation, and Goal/RollingSafetyStop completion. |
-
-## Historical Completed Tasks
-
-| Stage | Plan | Tasks | Commits | Verification |
-|---:|---|---|---|---|
-| 1 | `2026-08-03-em-planner-foundation-implementation.md` | Task 1 — Verification Host and Immutable Contracts | `e12eb31` | RED: `foundation` failed with missing `VehicleMotionState`/`EmTrajectoryPoint`; GREEN: `foundation` printed `PASS foundation` with exit 0. |
-| 1 | `2026-08-03-em-planner-foundation-implementation.md` | Task 2 — Configuration, Diagnostics, and Request Validation | `b326431` | RED: `foundation` failed with missing configuration/validation types; GREEN: `foundation` printed `PASS foundation` twice with byte-identical stdout and exit 0. |
-| 1 | `2026-08-03-em-planner-foundation-implementation.md` | Task 3 — Direction Segmentation and Exact Boundary Anchors | `e492e16` | RED: `segmentation` failed with missing `ReferencePathSegmenter`; GREEN: `foundation` and `segmentation` each printed their PASS line with exit 0. |
-| 2 | `2026-08-03-em-planner-foundation-implementation.md` | Task 4 — Reverse-Safe Frenet Projection and Reconstruction | `06687e2` | RED: `frenet` failed with missing Frenet types including `FrenetProjector`; GREEN: `PASS frenet` with forward/reverse sign, yaw-wrap, bounded U-branch, singular-denominator, and reconstruction checks. |
-| 2 | `2026-08-03-em-planner-foundation-implementation.md` | Task 5 — Topology-Preserving Static Corridor | `2d252ff` | RED: `corridor` failed with missing static-corridor types; GREEN: `PASS corridor`, followed by `all-foundation` with all four PASS lines. |
-| 2 | `2026-08-03-em-planner-foundation-implementation.md` | Task 6 — Foundation Documentation and Gate | `aa62d6b` | RED: Foundation README was absent; GREEN: README contract scan passed. Placeholder/format scans passed, `all-foundation` passed twice with identical four-line stdout, and the normal-project baseline contained only legacy `auto_avoidance` errors. |
-| 3 | `2026-08-03-em-planner-osqp-backend-implementation.md` | Task 1 — Solver-Neutral Sparse QP Contracts | `104081e` | RED: `optimization` failed with missing `SparseTripletBuilder`; GREEN: `PASS optimization` covered deterministic CSC canonicalization, duplicate summation, zero dropping, upper-triangular Hessians, QP validation, and defensive copies. |
-| 3 | `2026-08-03-em-planner-osqp-backend-implementation.md` | Task 2 — Reproducible OSQP 1.0.0 Native Package | `2abb465` | RED: the planned package script was absent; GREEN: the fixed-tag build printed `OSQP v1.0.0 win-x64 package ready`. The package contains the pinned DLL, license files, version/build flags, and lowercase SHA-256 manifest; `dumpbin` verified x64 and the four required exports. |
-| 3 | `2026-08-03-em-planner-osqp-backend-implementation.md` | Task 3 — Absolute-Path Native Loader and ABI Structures | `39c1708` | RED: `osqp-loader` failed with missing `OsqpNativeLoader`; GREEN: `PASS osqp-loader` verified missing/corrupt DLL diagnostics, real `1.0.0` loading from a copied plugin directory, and one stable handle across 16 parallel first-use calls. |
-| 4 | `2026-08-03-em-planner-osqp-backend-implementation.md` | Task 4 — OSQP Solve Lifecycle and Status Mapping | `a957fda` | RED: `osqp-loader` failed with missing `OsqpNativeSolver`; GREEN: fixed bounded, equality, infeasible and one-tick time-limit QPs pass inside a copied clean-plugin process. Status values 1–11 map to the planner-neutral contract, with finite metrics and captured native status. Twenty exact `osqp` solve/cleanup cycles printed `PASS osqp-solve` and `PASS osqp-loader` on every iteration. |
-| 4 | `2026-08-03-em-planner-osqp-backend-implementation.md` | Task 5 — Backend Completion Gate | `61dfa79` | RED: `osqp` was rejected by the verification-host command parser; GREEN: it runs the full solver/loader group. README now records the pinned package, deployment layout, absolute-load rule, ownership and status mapping. `dumpbin /dependents` found only Windows/runtime DLLs; an externally located working directory still loaded a copied clean plugin bundle and solved the micro QPs. |
-| 5 | `2026-08-03-em-planner-lateral-ls-implementation.md` | Task 1 — Variable Layout and Exact Discrete Lateral Dynamics | `c225b17` | RED: `lateral-model` failed to build with missing `LateralVariableLayout`; GREEN: `PASS lateral-model` covered contiguous `4N-1` indices, range checks, unequal-S exact integration, station/corridor/start validation, defensive copies, and the lateral result publication contract. |
-| 5 | `2026-08-03-em-planner-lateral-ls-implementation.md` | Task 2 — Normalized Objective and Linear Hard Constraints | `f703d41` | RED: `lateral-model` failed to build with missing `LateralObjectiveBuilder`; GREEN: `PASS lateral-model` inspected normalized P/q coefficients with `1e-12` comparisons, exact integration equalities, finite hard bounds, goal/gear versus rolling terminal behavior, empty-intersection early failure, and the solver-neutral fake-QP boundary. |
-| 5 | `2026-08-03-em-planner-lateral-ls-implementation.md` | Task 3 — Nonlinear Geometry Evaluation and Independent Validation | `0c48a7d` | RED: `lateral-model` failed to build with missing `LateralGeometryEvaluator`; GREEN: `PASS lateral-model` covered straight and constant-curvature references in forward/reverse, full Frenet curvature, actual strictly increasing PathS, curvature/yaw-rate signs, and rejection of denominator, curvature, non-finite, and independently recomputed world-geometry violations. |
-| 6 | `2026-08-03-em-planner-lateral-ls-implementation.md` | Task 4 — Sequential Convex Outer Loop and Feasible-Candidate Fallback | `f19df53` | RED: `lateral-integration` failed to build with missing `SequentialConvexOptimizer` and `LateralPlanner`; GREEN: `PASS lateral-integration` scripted validation-before-fallback, invalid-vector rejection, `SolvedInaccurate` residual/geometry rejection, 0.05 m trust centering, complete-primal warm starts, five-call cap, cancellation, timeout and facade behavior. |
-| 6 | `2026-08-03-em-planner-lateral-ls-implementation.md` | Task 5 — Real-OSQP Lateral Scenarios and Gate | `4d83ed2` | RED: `lateral-all` reached the real forward scenario but rejected the empty initial warm start; the subsequent loader diagnosis confirmed the host output bundle intentionally lacks `osqp.dll`. GREEN: the solver-neutral full initial primal and a clean copied plugin-bundle probe produced `PASS lateral-model`, `PASS lateral-integration`, and `PASS lateral-real-osqp` for deterministic forward/reverse straight, gentle curve, seed-connected obstacle narrowing, gear-switch, and rolling scenarios. |
-
-| 7 | `2026-08-03-em-planner-longitudinal-st-implementation.md` | Task 1 — Speed Envelope over Actual PathS | `62ea9db`, correction `25742ab` | RED: `longitudinal-model` first failed with missing longitudinal input/speed-envelope types; the sparse stopping chord and then the discrete jerk-release tail checks also failed before refinement. GREEN: `PASS longitudinal-model` proves actual strictly increasing LS `PathS` consumption, finite curvature/curvature-rate/stopping limits, stopping-distance precheck, dense actual-PathS stations, and discrete deceleration/jerk tail stations. |
-| 7 | `2026-08-03-em-planner-longitudinal-st-implementation.md` | Task 2 — Time-Knot Layout, Dynamics, Objective, and Hard Constraints | `c01d0d5` | RED: `longitudinal-model` failed with missing ST candidate/layout/constraint types. GREEN: `PASS longitudinal-model` inspects normalized objective coefficients, solver-neutral `QuadraticProgram` assembly, exact constant-jerk dynamics, monotonic progress, finite physical bounds, exact start state, and hard terminal `s_N`/`u_N`. |
-| 7 | `2026-08-03-em-planner-longitudinal-st-implementation.md` | Task 3 — Longitudinal Outer Loop and Strict Validation | `510bf97` | RED: `longitudinal-integration` failed first with missing optimizer/validator/facade types; focused checks then exposed invalid fallback acceptance, sparse envelope probes, noncanonical endpoints, and loss of a strictly validated initial seed on immediate timeout. GREEN: `PASS longitudinal-integration` covers scripted statuses, strict residual/physical validation, deep-copy last-feasible fallback, complete-primal warm starts, the five-iteration cap, cancellation, and deterministic real OSQP forward/reverse/curvature/jerk-stop/short/zero-start scenarios in a clean copied plugin bundle. |
-| 8 | `2026-08-03-em-planner-longitudinal-st-implementation.md` | Task 4 — Complete Trajectory Assembly and Redundant-Field Consistency | `1d58864` | RED: `trajectory` exited 1 with CS0246 for missing `EmTrajectoryAssembler`. GREEN: `trajectory` exited 0 with `PASS trajectory`, checking forward/reverse signed speed, all redundant field identities, unwrapped yaw interpolation, exact terminal anchor, and the `0.20 s` / `0.05 s` hold tail. |
-| 8 | `2026-08-03-em-planner-longitudinal-st-implementation.md` | Task 5 — Independent World-Space Publication Validator | `7410654` | RED: `trajectory` exited 1 with missing `EmTrajectoryValidationFailure` / validator contracts. GREEN: `trajectory` exited 0 with `PASS trajectory`, rejecting deterministic mutations for non-finite values, timing/PathS/direction/field inconsistencies, every configured physical limit, terminal anchor/speed/yaw-rate failures, pose and `0.025 m` swept collisions, segment overflow, and reverse-specific speed limits. |
-| 8 | `2026-08-03-em-planner-longitudinal-st-implementation.md` | Task 6 — Pure One-Shot EmPlanningService | `019b896` | RED: `em-planning-service` exited 1 with CS0246 for missing `EmPlanningService`. GREEN: `em-core-all` exited 0 twice in the required four-line order; the service checks cover forward/reverse/gear/goal/rolling success, deterministic output, request IDs, invalid/stale/direction/projection/corridor/stopping/LS/ST/solver/timeout/cancellation/publication-validation failures, and debug-sink exception isolation. |
-
-## Current Verification
-
-- Commands: entry regressions `coordinator`, `executor`, `em-core-all`, `trajectory`, `lateral-all`, `optimization`, `osqp`, and `all-foundation`; Task 4 `executor`; Task 5 `plugin-package`; final `em-all` and `git diff --check`, all from the repository root.
-- Result: every entry regression exited 0. The final post-commit `em-all` exited 0 with foundation, OSQP, lateral, longitudinal, trajectory, facade, coordinator, executor, plugin packaging, rolling end-to-end, and rolling-tail PASS lines; `git diff --check` exited 0.
-- RED/GREEN: Task 4 RED `executor` exit 1 (missing generic adapter/provider), GREEN exit 0. Task 5 RED `plugin-package` exit 1 (missing script), GREEN exit 0. Task 6 RED `em-all` exit 2 (unregistered command), GREEN exit 0; the Task 5 output metadata required test-only clean-bundle copy fixtures to omit host-output `osqp.dll` before their existing explicit pinned-DLL copy.
-- Verified commits: `aa51ae2`, `3c84e36`, `4058230`.
-
-## Historical Current Verification
-
-- Commands:
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-integration`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-all`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-model`
-  - `1..20 | ForEach-Object { dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp; if ($LASTEXITCODE -ne 0) { throw "OSQP verification failed on iteration $_" } }`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- optimization`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp-loader`
-  - `dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- all-foundation`
-  - `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\dumpbin.exe /dependents ClumsyPilot/ThirdParty/OSQP/win-x64/osqp.dll`
-  - Clean copied plugin bundle: run `EMPlannerVerificationHost.exe osqp-probe` from a separate working directory.
-  - `git diff --check`
-- Result: The Stage 5 `lateral-model` gate exited 0 after all three Task commits. It proves the `4N-1` layout, exact unequal-station dynamics, immutable lateral inputs/results, normalized OSQP-convention P/q assembly, finite hard corridor/derivative/trust/denominator bounds, terminal distinction, full forward/reverse world reconstruction, actual PathS, and independent validation. `optimization`, `osqp`, and all four `all-foundation` groups exited 0. The dependency list remains `KERNEL32.dll`, `VCRUNTIME140.dll`, and API-set CRT DLLs only—no MKL, CUDA or external QDLDL. `git diff --check` had no whitespace diagnostics; immediately before this checkpoint update the staging area was empty and no Stage 5 scope files were uncommitted.
-- Result: Stage 6 exit commands all exited 0 at `4d83ed2`: `lateral-integration`; `lateral-all` (model, scripted SQP, and clean-plugin real OSQP groups); `optimization`; `osqp`; `all-foundation`; and `git diff --check`. The real OSQP gate runs every fixed scenario twice and compares status, point count, and all lateral-path numeric fields within `1e-10`; the obstacle scenario stays in the negative, seed-connected corridor interval. The LS implementation remains dependent only on `IQpSolver`/`QuadraticProgram`; no LS P/Invoke, UI, hardware object, or current-working-directory dependency was added.
-- Result: Stage 7 exit commands all exited 0 from the repository root at `510bf97`: `longitudinal-model`; `longitudinal-integration`; `lateral-integration`; `lateral-all`; `optimization`; `osqp`; `all-foundation`; and `git diff --check`. ST consumes only actual LS `PathS` and reaches OSQP only through `IQpSolver`/`QuadraticProgram`; no ST P/Invoke, UI, hardware object, or current-working-directory dependency was added. The jerk-stop case retains only the independently validated initial seed when OSQP reaches its 4000-iteration limit; the timed-out solver vector is not accepted.
-- Baseline note: invoking the pre-existing `lateral-all` test from the `ClumsyPilot` subdirectory fails its pinned-DLL lookup because that Stage 6 test constructs the source path from `Directory.GetCurrentDirectory()`. Running the documented command from the repository root exits 0. Stage 7 does not modify or bypass the completed LS test.
-- Normal-project baseline: the historical checkpoint recorded exit 1 from legacy `auto_avoidance/MultiWheelAutoAvoidance.cs` references to `NetTopologySuite` and `OpenCvSharp`. At the Stage 7 final read-only audit that source path is absent from both the working tree and the tracked `HEAD` tree, so `dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore` exits 0 with only two unrelated obsolete-API warnings. Stage 7 issued no delete/clean command and made no legacy-module change; preserve the observed workspace and do not recreate, delete, hide, or otherwise alter legacy functionality merely to change this baseline.
-- Verified commits: `104081e`, `2abb465`, `39c1708`, `a957fda`, `61dfa79`, `c225b17`, `f703d41`, `0c48a7d`, `f5c69c2`, `f19df53`, `4d83ed2`, `62ea9db`, `c01d0d5`, `25742ab`, and `510bf97`
-- Result: Stage 8 exit commands all exited 0 from the repository root at `019b896`: `em-core-all` twice (each printed `PASS longitudinal-model`, `PASS longitudinal-integration`, `PASS trajectory`, `PASS em-planning-service` in order); then `longitudinal-model`, `longitudinal-integration`, `trajectory`, `lateral-all`, `optimization`, `osqp`, `all-foundation`, and `git diff --check`. The trajectory gate independently verifies physical world-space publication and the service consumes only request snapshots through `IQpSolver` / `QuadraticProgram`; no Stage 8 production P/Invoke, UI, hardware, current-working-directory, dynamic-obstacle, rolling-execution, or controller dependency was added.
-- Stage 8 RED commands and exits: Task 4 `trajectory` exit 1 (missing `EmTrajectoryAssembler`); Task 5 `trajectory` exit 1 (missing validator contracts); Task 6 `em-planning-service` exit 1 (missing `EmPlanningService`). Task 4/5 GREEN command was `trajectory` exit 0 / `PASS trajectory`; Task 6 GREEN command was `em-core-all` exit 0 twice with the required four PASS lines.
-
-## Preserved Workspace State
-
-- The initially untracked Task 1 coordinator draft was explicitly authorized for Stage 9 ownership and committed as `d75380c`.
-- Every captured user-baseline status entry remains untouched and unstaged: the final `git status --short` set has the same 125 entries as Stage 10 entry, with no additions or losses.
-- The `lateral-all` fixture still resolves its explicitly copied pinned OSQP DLL from the repository-root current directory. Build-output `osqp.dll` is omitted only from the temporary clean-bundle host copy before that existing explicit copy; no LS/ST/OSQP/facade production behavior changed.
-- Existing baseline noise remains the two obsolete-API warnings and CRLF advisories on user-owned dirty files; `git diff --check` exits 0.
-
-## Historical Preserved Workspace State
-
-- The workspace continues to contain the same unrelated Map, CoarsePath, PathSmoothing, project-file, report, and documentation changes captured at Stage 6 entry. They belong to the user and remain untouched and unstaged.
-- Use explicit file paths for every `git add`; never stage the whole workspace.
-- The existing user hunk in `ClumsyPilot/ClumsyPilot.csproj` remains preserved. Do not clean, rewrite, or stage it.
-- The historical legacy `auto_avoidance/MultiWheelAutoAvoidance.cs` dependency failure is not reproducible in the Stage 7 final workspace because that untracked/unversioned source path is absent. Do not recreate, delete, bypass, or otherwise alter legacy functionality to manufacture a different baseline.
-- Remaining observed baseline noise: the first fresh Stage 8 core build printed the existing `Lidar2dDetect2LegTray.LegWidth` and `MultiWheelChassis.GetSteerWheels()` obsolete-API warnings; `git diff --check` emits CRLF conversion advisories for user-owned dirty files but exits 0. Neither is an EM Planner regression.
-
-## Decisions Needed
-
-- None. Stage 10 completed within scope; it is the final planned EM Planner stage.
-
-## Historical Decisions Needed
-
-- None. Stage 7 completed within scope.
-
-## Prior Stage 7 Entry Record
-
-- Stage: 7 — ST model and longitudinal optimization
-- Readiness: Ready for Stage 7 entry checks after this checkpoint is committed.
-- Plan: `docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md`
-- Tasks:
-  - Task 1 — Speed Envelope over Actual PathS
-  - Task 2 — Time-Knot Layout, Dynamics, Objective, and Hard Constraints
-  - Task 3 — Longitudinal Outer Loop and Strict Validation
-- Entry checks:
-  - Confirm branch `trajplanner`, `a957fda`, `61dfa79`, `c225b17`, `f703d41`, `0c48a7d`, `f5c69c2`, `f19df53`, and `4d83ed2` are ancestors of `HEAD`, and the Stage 6 progress checkpoint is present.
-  - Capture `git status --short` before editing and preserve every unrelated user change.
-  - Read the two EM Planner design documents, this progress file, and the longitudinal-ST implementation plan in full.
-  - Run `lateral-integration`, `lateral-all`, `optimization`, `osqp`, and `all-foundation` before Task 1.
-  - ST must consume actual, strictly increasing LS `PathS` and keep the OSQP boundary solver-neutral through `IQpSolver`.
-- Exit gate:
-  - Tasks 1–3 each have an independent commit and their prescribed RED/GREEN evidence.
-  - The actual-PathS speed envelope, stopping precheck, ST dynamics/QP, and strict longitudinal validation pass.
-  - The Stage 7 plan's full exit gate and `git diff --check` pass; the staging area is empty; a Stage 7 progress checkpoint is committed alone.
-
-## Prior Stage 7 Window Prompt (Historical)
-
-```text
-请继续 ParkingRobot 仓库的 EM Planner 多窗口实施。
-
-工作目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot
-
-本窗口只执行阶段 7:ST 模型与纵向优化。唯一授权范围是:
-`docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md` 的 Task 1、Task 2、Task 3。
-不要开始轨迹发布、单次规划服务、滚动执行、控制适配、动态障碍物或任何后续阶段。
-
-开始前必须完整读取:
-1. `docs/superpowers/specs/2026-08-03-em-planner-ls-st-design.md`
-2. `docs/superpowers/specs/2026-08-03-em-planner-windowed-execution-design.md`
-3. `docs/superpowers/progress/em-planner-progress.md`
-4. `docs/superpowers/plans/2026-08-03-em-planner-longitudinal-st-implementation.md`
-
-使用 `executing-plans`、`test-driven-development` 和 `verification-before-completion`;不要使用子代理。只在 `trajplanner` 上工作。Stage 4–6 提交必须可从 `HEAD` 追溯:`a957fda`、`61dfa79`、`c225b17`、`f703d41`、`0c48a7d`、`f5c69c2`、`f19df53` 和 `4d83ed2`;随后是本进度文件的独立 Stage 6 checkpoint。若这些提交、进度记录或入口验证任一不一致,停止实施并先报告事实,不要猜测或重做已提交工作。
-
-开始 Task 1 前:
-- 保存完整 `git status --short` 基线,确认暂存区为空;工作区已有大量与 EM 无关的 Map、CoarsePath、PathSmoothing、报告、文档、项目文件和未跟踪修改,必须完整保留。
-- 不得使用 `git add .`、`git add -A`、`git reset --hard`、`git checkout --` 或任何清理命令;每次只用显式路径暂存计划指定文件。
-- 运行以下隔离入口验证,并确认全部退出码为 0:
-  ```powershell
-  dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-integration
-  dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-all
-  dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- optimization
-  dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp
-  dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- all-foundation
-  ```
-- `ClumsyPilot.csproj` 中有既有用户 hunk;正常项目构建仍会因旧 `auto_avoidance/MultiWheelAutoAvoidance.cs` 缺少 `NetTopologySuite` 与 `OpenCvSharp` 失败。不要修改、删除、屏蔽或绕过该旧模块,也不要把它当成 EM 回归。
-
-实施约束:
-- 严格逐任务 TDD:先增加该任务的最小失败检查,观察其因缺少目标行为而失败,再写最小生产代码,并运行计划指定 GREEN 验证。
-- Task 1、2、3 各自形成独立功能提交;提交前用 `git diff --cached --check` 和 `git diff-tree` 核对文件集合。
-- ST 只能消费 LS 输出的实际、严格递增 `PathS`,并且只能通过既有 solver-neutral `IQpSolver`/`QuadraticProgram` 边界使用 OSQP;不能向 ST 引入 P/Invoke、当前工作目录依赖、UI 或硬件对象。
-- 不修改无关的 Map、CoarsePath、PathSmoothing、旧 `auto_avoidance`、已完成 LS 或 OSQP 后端;不实现轨迹发布、滚动执行、控制适配或动态障碍物。
-
-阶段结束条件:
-- 仅在 Task 1–3 均有独立提交、各自 RED/GREEN 证据和该计划全部出口验证通过后,运行 Stage 7 规定的 LS/ST/优化/OSQP/Foundation 回归及 `git diff --check`。
-- 检查暂存区为空、用户的起始 `git status --short` 修改仍被保留;然后只更新并单独提交 `docs/superpowers/progress/em-planner-progress.md`。
-- 最终报告必须给出实际提交、命令/退出码、未处理基线问题,并提供阶段 8(仅轨迹发布与单次规划服务 Task 4–6)的精确下一窗口提示词。
-```
-
-## Next Stage
-
-- None. Stage 10 is the final approved stage; no Stage 11 implementation work or prompt is generated.
-
-## Historical Stage 9 Next Stage
-
-- Stage: 9 — rolling coordination and gear-switch execution
-- Readiness: Ready after this Stage 8 progress checkpoint is committed.
-- Plan: `docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md`
-- Authorized tasks only:
-  - Task 1 — Cycle Identity, Scheduling Decision, and Stale-Result Suppression
-  - Task 2 — Safe Previous-Trajectory Handoff
-  - Task 3 — Gear-Switch State Machine and Trajectory Executor
-- Explicit exclusions: generic control adaptation, plugin packaging, dynamic obstacles, UI/hardware integration beyond the approved execution boundary, and Stage 10 work.
-
-## Historical Stage 10 Window Prompt
-
-```text
-请继续 ParkingRobot 仓库的 EM Planner 多窗口实施。
-
-工作目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot
-
-本窗口只执行阶段 10:通用控制适配、插件发布与最终滚动验收。唯一授权范围是:
-docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md 的 Task 4、Task 5、Task 6。
-不要开始动态障碍物、UI/硬件集成,或任何未在该计划中的后续工作。
-
-开始前必须完整读取:
-1. docs/superpowers/specs/2026-08-03-em-planner-ls-st-design.md
-2. docs/superpowers/specs/2026-08-03-em-planner-windowed-execution-design.md
-3. docs/superpowers/progress/em-planner-progress.md
-4. docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md
-
-使用 executing-plans、test-driven-development 和 verification-before-completion;不要使用子代理。只在 trajplanner 上工作。
-
-开始 Task 4 前:
-- 确认 d75380c、55119de、de402e6 以及紧随其后的 Stage 9 进度 checkpoint 均可从 HEAD 追溯。
-- 保存完整 git status --short 基线,确认暂存区为空,并保留所有既有用户修改。
-- 从仓库根目录运行并确认退出码为 0:coordinator、executor、em-core-all、trajectory、lateral-all、optimization、osqp、all-foundation。
-- 不使用 git add .、git add -A、清理命令或破坏性 Git 命令;每次仅显式暂存计划指定文件。
-- Stage 6 lateral-all 测试夹具依赖仓库根目录的当前工作目录定位固定 OSQP DLL;不要修改已完成 LS/ST/OSQP/发布门面来改变该基线。
-
-执行要求:
-- 严格逐任务 TDD;Task 4-6 各自独立提交,提交前运行 git diff --cached --check,并用 git diff-tree 核对范围。
-- Task 4 只能产生通用控制命令,不能引用 MultiVehicleScriptVx、MultiVehicleScriptVy、MultiVehicleScriptVth、UI 或硬件对象。
-- Task 5 只能按计划输出并验证 ClumsyPilot.dll、osqp.dll 与许可证目录;不得重写现有 csproj 用户 hunk。
-- Task 6 只添加计划要求的端到端证据和 README 所有权/部署说明;不实现动态障碍物、UI 或硬件集成。
-
-阶段结束时:
-- 仅在 Task 4-6 均有独立 RED/GREEN 证据和计划规定的最终出口验证后,运行 em-all 和 git diff --check。
-- 确认暂存区为空、起始用户修改仍保留;只更新并单独提交 docs/superpowers/progress/em-planner-progress.md。
-- 返回提交、RED/GREEN 证据、命令/退出码和未处理基线问题;Stage 10 是最终阶段,不生成 Stage 11 实施提示词。
-```
-
-## Historical Stage 9 Next-Window Prompt
-
-```text
-请继续 ParkingRobot 仓库的 EM Planner 多窗口实施。
-
-工作目录:D:\Users\Desktop\项目\prakrobot\ParkingRobot
-
-本窗口只执行阶段 9:滚动协调与换向执行。唯一授权范围是:
-docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md 的 Task 1、Task 2、Task 3。
-不要开始通用控制适配、插件最终发布、动态障碍物、UI/硬件集成或任何 Stage 10 工作。
-
-开始前必须完整读取:
-1. docs/superpowers/specs/2026-08-03-em-planner-ls-st-design.md
-2. docs/superpowers/specs/2026-08-03-em-planner-windowed-execution-design.md
-3. docs/superpowers/progress/em-planner-progress.md
-4. docs/superpowers/plans/2026-08-03-em-planner-rolling-execution-implementation.md
-
-使用 executing-plans、test-driven-development 和 verification-before-completion;不要使用子代理。只在 trajplanner 上工作。
-
-开始 Task 1 前:
-- 确认 1d58864、7410654、019b896 以及紧随其后的 Stage 8 进度 checkpoint 均可从 HEAD 追溯。
-- 保存完整 git status --short 基线,确认暂存区为空,并保留所有既有用户修改。
-- 从仓库根目录运行并确认退出码为 0:em-core-all、trajectory、lateral-all、optimization、osqp、all-foundation。
-- 不使用 git add .、git add -A、清理命令或破坏性 Git 命令;每次仅显式暂存计划指定文件。
-- Stage 6 lateral-all 测试夹具依赖仓库根目录的当前工作目录定位固定 OSQP DLL;不要修改已完成 LS/ST/OSQP/发布门面来改变该基线。
-
-执行要求:
-- 严格逐任务 TDD;Task 1–3 各自独立提交,提交前运行 git diff --cached --check,并用 git diff-tree 核对范围。
-- 保持单次 EmPlanningService 纯请求边界:协调器只能消费其不可变结果,不能把 UI、硬件或当前工作目录依赖回灌到核心。
-- 不修改无关 Map、CoarsePath、PathSmoothing、旧 auto_avoidance、已完成 Foundation/OSQP/LS/ST/Trajectory/Validation/Facade 文件;不实现动态障碍或控制适配。
-
-阶段结束时:
-- 仅在 Task 1–3 均有独立 RED/GREEN 证据和阶段出口验证后,运行计划规定的回归和 git diff --check。
-- 确认暂存区为空、起始用户修改仍保留;只更新并单独提交 docs/superpowers/progress/em-planner-progress.md。
-- 返回提交、RED/GREEN 证据、命令/退出码、未处理基线问题,并给出 Stage 10 的精确下一窗口提示词。
-```
diff --git a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-1.md b/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-1.md
deleted file mode 100644
index 3e6d9ce..0000000
--- a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-1.md
+++ /dev/null
@@ -1,235 +0,0 @@
-# 新窗口执行提示词:EM 纵向滚动规划阶段一
-
-请在一个新的 Codex 窗口中完整粘贴本文件内容,或明确要求 Codex 读取并执行本文件。
-
----
-
-你现在要在仓库 `D:\Users\Desktop\项目\prakrobot\ParkingRobot` 中执行 EM 纵向滚动轨迹规划修复的**阶段一:纵向基础与窗口语义**。
-
-这是一项代码实施任务,不是重新讨论方案。设计已经确认。本窗口只能完成阶段一,对应原十任务计划中的 Task 1 和 Task 2;完成阶段验收、交接文件和阶段二提示词后必须停止,不得继续实现 Task 3。
-
-## 必须使用的工作方法
-
-1. 首先使用 `executing-plans` skill 执行现有计划。
-2. 实现每个 bugfix/feature 时使用 `test-driven-development`:先写失败测试、运行确认失败原因正确,再做最小实现、重新运行确认通过。
-3. 遇到非预期编译错误、测试失败或行为不一致时,先使用 `systematic-debugging` 查明根因,不得盲目放宽阈值。
-4. 声称阶段完成前必须使用 `verification-before-completion`,重新运行全部阶段验收命令。
-5. 不需要重新 brainstorming;已经批准的设计规格是行为依据。
-
-## 必读文件
-
-开始实施前完整阅读:
-
-- `docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md`
-
-在十任务计划中,本窗口只能执行:
-
-- Task 1:完整 jerk 受限停车数学
-- Task 2:纵向模式、空间窗口和输入契约
-
-计划中的代码片段是实施基线,但在应用前必须与当前仓库接口核对;若代码已发生变化,应做保持设计语义的最小适配,并在交接文件中记录差异。
-
-## 开始前基线检查
-
-依次执行并记录结果:
-
-```powershell
-git branch --show-current
-git status --short
-git log -8 --oneline
-git show --stat --oneline e0ccc0c
-git show --stat --oneline e608299
-```
-
-预期当前分支为 `trajplanner`,并且至少能找到:
-
-- `e0ccc0c docs: plan EM longitudinal rolling implementation`
-- `e608299 docs: require stabilized EM stop terminals`
-
-仓库当前存在大量与本任务无关的用户修改。必须遵守:
-
-- 不运行 `git reset --hard`、`git checkout -- .`、`git clean` 或任何等价清理命令。
-- 不回退、不格式化、不移动与阶段一无关的文件。
-- 不使用 `git add .` 或 `git add -A`。
-- 提交前使用 `git diff --cached --name-only` 核对暂存区。
-- 如果阶段一目标文件本身已有未提交修改,先检查并保留用户内容,在其基础上最小集成;不能直接覆盖。
-
-## 不可改变的行为语义
-
-- `DistanceHorizonMeters` 只决定 LS 可以查看的空间长度。
-- `TimeHorizonSeconds` 只决定一次 ST 优化的未来时间范围。
-- 局部空间窗口末端不是停车点。
-- 只有真实 `Goal` 或 `GearSwitchApproach` 才是停车边界。
-- 纵向模式必须显式区分:
-  - `RollingContinuation`
-  - `ApproachStopBoundary`
-  - `ExactStopAtBoundary`
-- 精确停车最终要支持在 QP 内达到 `S=S_end、U=0、A=0`,并至少保留一个完整 `OutputTimeStepSeconds` 的静止稳定区间。
-- `ZeroSpeedHoldSeconds` 是 QP 时间范围之后的发布保持段,不能从 `TimeHorizonSeconds` 中扣除。
-- 不得通过放宽 jerk、加速度、求解残差或轨迹验证阈值规避问题。
-- 保持 `OBSERVE_ONLY`;不得增加任何底盘控制下发。
-
-## Task 1:完整 jerk 受限停车数学
-
-严格按照十任务计划 Task 1 的 TDD 步骤实施。
-
-### 文件范围
-
-- 新建:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs`
-- 修改:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs`
-- 测试:`ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs`
-
-### 必须产生的接口
-
-- `JerkLimitedStoppingMath.TryCalculate(double, double, double, double, out JerkLimitedStoppingProfile, out string)`
-- `JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(double, double, double, double, double)`
-- `JerkLimitedStoppingMath.CalculateMaximumStoppedDistance(double, double, double, double, double, double, double)`
-- `JerkLimitedStoppingProfile`,至少表达停车距离、持续时间、末速度和末加速度。
-
-停车数学必须使用与 ST QP 一致的恒 jerk 离散/分段动力学,能够覆盖初始正加速度、初始负加速度、三角形和梯形减速度轮廓,最终同时释放到零速度和零加速度。
-
-### 红灯和绿灯命令
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model
-```
-
-先证明新增测试因缺少新能力而失败;实现后必须得到 `PASS longitudinal-model`。
-
-### Task 1 提交
-
-只暂存 Task 1 文件,核对暂存区后提交:
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/JerkLimitedStoppingMath.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs
-git diff --cached --name-only
-git commit -m "feat: add complete jerk-limited stopping math"
-```
-
-## Task 2:纵向模式、空间窗口和输入契约
-
-Task 1 提交并通过测试后,严格按照十任务计划 Task 2 的 TDD 步骤实施。
-
-### 文件范围
-
-- 新建:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs`
-- 新建:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalTerminalSchedule.cs`
-- 修改:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs`
-- 修改:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs`
-- 修改:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs`
-- 修改:`ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`
-- 测试:`ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs`
-- 测试:`ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs`
-
-### 必须产生的接口和行为
-
-- `EmLongitudinalMode` 三种模式。
-- `LongitudinalTerminalSchedule.GetStabilizationStartIndex(IReadOnlyList, double)`。
-- `PlanningHorizonSelection` 显式保存:
-  - `WindowEndReferenceS`
-  - `WindowEndBoundaryType`
-  - `LongitudinalMode`
-  - `StopBoundaryReferenceS`
-  - `HasStopBoundary`
-- `LongitudinalPlanningInput` 分离路径上界、真实停车边界和纵向模式。
-- 选择顺序必须是:
-  1. 识别当前方向段的真实边界;
-  2. 用 Task 1 的完整 jerk 模型检查最小停车距离;
-  3. 只用 `DistanceHorizonMeters` 截取 LS 空间窗口;
-  4. 真实边界不在窗口内时选择 `RollingContinuation`;
-  5. 边界已在窗口内但本次时间范围无法形成稳定停车时选择 `ApproachStopBoundary`;
-  6. 能在时间范围内停车并留出内部稳定区间时选择 `ExactStopAtBoundary`。
-- `EmPlanningRequestValidator` 必须检查前视停车余量、时间步、时间范围和内部稳定区间组合,不允许无效配置进入求解器。
-
-### 红灯和绿灯命令
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation
-```
-
-先证明模式/属性/行为测试失败;实现并更新所有直接构造调用后,必须得到:
-
-```text
-PASS foundation
-PASS longitudinal-model
-```
-
-### Task 2 提交
-
-只暂存 Task 2 文件,核对暂存区后提交:
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmLongitudinalMode.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalTerminalSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs
-git diff --cached --name-only
-git commit -m "feat: separate rolling horizons from stop boundaries"
-```
-
-若为修复编译必须改动直接调用者,使用明确路径单独加入,并在提交前解释原因;仍然禁止把其他脏文件带入提交。
-
-## 阶段一最终验收
-
-Task 1 和 Task 2 完成后,从当前代码状态重新运行,不得引用之前的测试输出:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model
-git diff --check
-git log -5 --oneline
-git status --short
-```
-
-验收时同时人工确认:
-
-- 普通 5 m 局部窗口不会因为 `TimeHorizonSeconds=2s` 而被解释为必须在两秒内走完 5 m。
-- Rolling 局部窗口末端不再具有“停车边界”身份。
-- Goal/GearSwitch 边界进入窗口但暂时无法稳定停车时是 `ApproachStopBoundary`,不是失败,也不是强制零速。
-- 精确停车模式为至少一个 QP 内部静止时间步保留了明确的调度信息。
-
-## 阶段结束交接——必须执行
-
-验收通过后使用 `writing-plans`,根据**实际提交和实际接口**完成以下两份文件:
-
-1. 阶段一交接:
-
-   `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md`
-
-2. 阶段二精细提示词:
-
-   `docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-2.md`
-
-阶段一交接必须包含:
-
-- 开始基线提交和结束提交;
-- Task 1、Task 2 各自的提交号;
-- 实际修改/新增文件;
-- 最终接口签名和与原计划不同之处;
-- 全部验证命令、退出码和关键 PASS 输出;
-- 遗留问题、未验证内容和目标机限制;
-- 阶段二可依赖的明确事实。
-
-阶段二提示词必须:
-
-- 引用阶段一真实提交号和交接文件;
-- 只覆盖原计划 Task 3~5;
-- 明确速度包络、QP 条件终端约束、解验证、初始解和外层迭代的文件范围;
-- 要求 TDD,并列出 `longitudinal-model` 与 `longitudinal-integration` 验收命令;
-- 保留 Task 3、Task 4、Task 5 的独立提交边界;
-- 要求阶段二结束后生成阶段二交接和阶段三提示词;
-- 重复工作树保护、全局行为语义和 `OBSERVE_ONLY` 限制。
-
-只暂存这两份交接文档,并提交:
-
-```powershell
-git add -- docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-2.md
-git diff --cached --name-only
-git commit -m "docs: hand off EM rolling planning phase one"
-```
-
-提交后输出一个简短阶段报告,包含两个功能提交、交接提交和两项验收结果,然后**立即停止**。不要开始 Task 3,也不要在本窗口实施阶段二。
-
----
-
-以上内容是本窗口的完整授权范围。
diff --git a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-2.md b/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-2.md
deleted file mode 100644
index 1b14df3..0000000
--- a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-2.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# New Window Prompt: EM Longitudinal Rolling Planning — Phase 2
-
-Work in `D:\Users\Desktop\项目\prakrobot\ParkingRobot`. This is an implementation task, not a design discussion. Read these files completely before editing:
-
-- `docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md`
-- `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-1.md`
-
-Phase 1 ended at `21b20d0` and consists of `e56220f` and `21b20d0`. Preserve their actual interfaces: `JerkLimitedStoppingMath`, `JerkLimitedStoppingProfile`, `EmLongitudinalMode`, `LongitudinalTerminalSchedule`, `PlanningHorizonSelection.WindowEndReferenceS`, `WindowEndBoundaryType`, `LongitudinalMode`, `StopBoundaryReferenceS`, `HasStopBoundary`, and the mode-explicit `LongitudinalPlanningInput` constructor/properties. `TerminalPathS` is only a temporary compatibility alias; use `PathUpperBoundS` for path bounds and `StopBoundaryPathS` for real stops.
-
-Execute only original-plan Tasks 3, 4, and 5. Do not begin Task 6 or later. Use `executing-plans`, test-driven development for every behavior change, systematic debugging for every unexpected test/build failure, and verification-before-completion before each commit and before phase handoff. No new brainstorming is needed.
-
-Start by recording:
-
-```powershell
-git branch --show-current
-git status --short
-git log -8 --oneline
-git show --stat --oneline e56220f
-git show --stat --oneline 21b20d0
-```
-
-The branch must be `trajplanner`; the worktree has substantial unrelated user changes. Never use reset, checkout, clean, `git add .`, or `git add -A`. Inspect any target file that is already dirty and preserve its user content. Explicitly stage only the current task files and run `git diff --cached --name-only` before every commit.
-
-## Task 3 — mode-conditioned PathS speed envelope
-
-Files: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimit.cs`, `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs`, and `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs`.
-
-Write and observe a failing `longitudinal-model` test first. Rolling must keep a nonzero speed allowance at `PathUpperBoundS` and report no stop boundary. For Approach and Exact modes, compute each stopping-speed limit by inverting `JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(remaining, maximumAcceleration, maximumDeceleration, maximumJerk, directionMaximum)` against `StopBoundaryPathS`; only the real stop-boundary sample is exactly zero. Replace the old square-root and discrete-tail approximations. Add `PathSpeedLimit.HasStopBoundary` and retain any necessary compatibility alias without introducing warnings. Then run:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model
-```
-
-Commit only these Task 3 files:
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimit.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs
-git diff --cached --name-only
-git commit -m "feat: keep rolling speed envelopes open"
-```
-
-## Task 4 — conditional QP terminals and independent validation
-
-Files: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs`, `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs`, and `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs`.
-
-TDD requirements: Rolling and Approach have no terminal S/U/A equalities. Exact mode must use `LongitudinalTerminalSchedule.GetStabilizationStartIndex` and enforce `S=StopBoundaryPathS`, `U=0`, `A=0` at every knot in the internal stabilization tail. The exact mode row count is `8*K-2+3*M`; non-exact modes remain `8*K-2`. The validator always checks finite values, time layout, monotone PathS, bounds, jerk, and exact constant-jerk dynamics. For Approach and Exact, independently recompute every knot's complete jerk-limited stopping profile and reject if `S + stoppingDistance > StopBoundaryPathS + tolerance`; exact mode must also canonicalize its full stabilization tail. Run `longitudinal-model` red then green and commit only Task 4 files:
-
-```powershell
-git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs
-git diff --cached --name-only
-git commit -m "feat: apply ST stop constraints only at real boundaries"
-```
-
-## Task 5 — mode-specific seeds and outer iteration
-
-Files: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs`, `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs`, and `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs`.
-
-Write failing `longitudinal-integration` tests first. A rolling two-second ST solve with a five-metre LS window must end before `PathUpperBoundS` at nonzero speed. Exact mode must have `S=StopBoundaryPathS`, `U=0`, `A=0` at every stabilization-tail knot. Dispatch seed creation by mode: rolling forward integration without a forced endpoint; approach as a cruise plus complete jerk-limited stop prefix limited to this ST horizon; exact stop reaches rest at the schedule anchor then fills the internal tail. In outer iterations, only Exact mode pins tail progress. Replace upper-bound uses with `PathUpperBoundS`; only real-stop endpoint uses may use `StopBoundaryPathS`. Keep terminal-acceleration soft objective only for non-exact modes. Verify both commands and commit only Task 5 files:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration
-git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs
-git diff --cached --name-only
-git commit -m "feat: seed rolling and exact-stop ST profiles"
-```
-
-Global semantics are non-negotiable: `DistanceHorizonMeters` is LS look-ahead only; `TimeHorizonSeconds` is one ST horizon only; only Goal/GearSwitchApproach can request an exact zero-speed terminal; `ZeroSpeedHoldSeconds` is outside the QP horizon; do not loosen jerk/acceleration/solver/validation tolerances; and retain `OBSERVE_ONLY` with no chassis command.
-
-At phase-2 end, rerun fresh `longitudinal-model` and `longitudinal-integration`, run `git diff --check`, inspect `git log -5 --oneline` and `git status --short`, then use `writing-plans` to create the phase-2 handoff and a phase-3 prompt. Commit only those two handoff documents and stop; do not implement phase 3.
diff --git a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-3.md b/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-3.md
deleted file mode 100644
index ae0247f..0000000
--- a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-3.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# New Window Prompt: EM Longitudinal Rolling Planning — Phase 3
-
-Work in `D:\Users\Desktop\项目\prakrobot\ParkingRobot`. This is an implementation task, not a design discussion. Read these files completely before editing:
-
-- `docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md`
-- `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-2.md`
-
-Phase 2 code ends at `59d13e5`; Task 3 and Task 4 also have local fixups `dbc7b7c` and `26bd822` because the dirty shared worktree prevented autosquash. Preserve all actual interfaces listed in the handoff, especially `EmLongitudinalMode`, `LongitudinalTerminalSchedule`, mode-explicit `LongitudinalPlanningInput`, `PathUpperBoundS`, `StopBoundaryPathS`, and `PathSpeedLimit.HasStopBoundary`.
-
-Execute only original-plan Tasks 6 and 7. Do not begin Task 8 or later. Use `executing-plans`, TDD for every behavior change, `systematic-debugging` for any unexpected build/test failure, and `verification-before-completion` before every commit and before phase handoff. No brainstorming is needed.
-
-Start by recording:
-
-```powershell
-git branch --show-current
-git status --short
-git log -10 --oneline
-git show --stat --oneline 59d13e5
-```
-
-The branch is `trajplanner` and the worktree has substantial unrelated user changes. Never reset, checkout, clean, broadly stage, stash user files, use `git add .`, or use `git add -A`. Stage only the listed task files and run `git diff --cached --name-only` before every commit.
-
-## Task 6 — mode-aware trajectory publication and validation
-
-Files:
-
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs`
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs`
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs`
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs`
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/ExecutorChecks.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/CoordinatorChecks.cs`
-
-Write and observe failing trajectory tests first. Add `EmLongitudinalMode` to trajectory metadata and pass the mode from `EmPlanningService`. For Rolling and Approach, sample exactly the ST candidate knots: no synthetic zero-speed hold, no terminal boundary anchor, and a nonzero terminal state remains valid when ordinary limits are met.
-
-For Exact mode, verify the complete internal static tail from `LongitudinalTerminalSchedule.GetStabilizationStartIndex`: every tail knot must hold the same PathS with `U=0`, `A=0`, and zero tail jerk. Mark only the first tail knot as the real Goal/GearSwitch boundary anchor; then append `ZeroSpeedHoldSeconds` only after the QP horizon, without duplicate timestamps. Add `TerminalAccelerationNotZero` to publication failures and make `EmTrajectoryValidator` always retain ordinary finite, kinematic, jerk, and collision checks while applying terminal-anchor/tail checks only to Exact mode. Do not weaken any threshold or add chassis behavior.
-
-Required checks before the Task 6 commit:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
-```
-
-Commit only the Task 6 files with:
-
-```powershell
-git commit -m "feat: publish rolling trajectories without stop tails"
-```
-
-## Task 7 — previous-trajectory longitudinal soft reference
-
-Files:
-
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs` (new)
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs`
-
-Write and observe failing tests first. Create `LongitudinalPreviousTrajectorySeed` with `PathS`, `ProgressSpeedMetersPerSecond`, and `Empty`. Its builder must resample the previous `EmTrajectory` at `newEffectiveAtUtc + knotTime`, linearly interpolate world position and signed speed, project monotonically onto the current `LateralPath`, and publish absolute progress speed. Any incompatible direction, segment, missing trajectory, or out-of-range sample must return `Empty` without throwing into planning.
-
-In `EmPlanningService`, construct current ST knot times after lateral planning, build this seed from the previous trajectory/current path/effective time/segment/direction, and pass the two reference arrays into the existing mode-explicit `LongitudinalPlanningInput`. Confirm the objective receives nonzero previous-S and previous-U soft-reference terms only for a valid seed; these remain soft terms, never terminal constraints. Keep LS slicing on `WindowEndReferenceS`.
-
-Required checks before the Task 7 commit:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service
-```
-
-Commit only the Task 7 files with:
-
-```powershell
-git commit -m "feat: reuse prior trajectory in longitudinal planning"
-```
-
-Global semantics are non-negotiable: `DistanceHorizonMeters` is LS look-ahead only; `TimeHorizonSeconds` is one ST horizon only; only Goal/GearSwitchApproach may require Exact zero speed; `ZeroSpeedHoldSeconds` is outside the QP horizon; do not loosen jerk, acceleration, solver, or validation thresholds; and retain `OBSERVE_ONLY` with no chassis command.
-
-At phase end, rerun `trajectory`, `em-planning-service`, `coordinator`, and `executor`; run `git diff --check`, `git log -5 --oneline`, and `git status --short`. Use `writing-plans` to create the phase-3 handoff and phase-4 prompt, commit only those two documents, and stop without implementing Task 8.
diff --git a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-4.md b/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-4.md
deleted file mode 100644
index 6ef40c2..0000000
--- a/docs/superpowers/prompts/2026-08-05-em-longitudinal-rolling-phase-4.md
+++ /dev/null
@@ -1,153 +0,0 @@
-# New Window Prompt: EM Longitudinal Rolling Planning — Phase 4
-
-Work in `D:\Users\Desktop\项目\prakrobot\ParkingRobot`. This is an implementation task, not a design discussion. Before editing, read these files completely:
-
-- `docs/superpowers/specs/2026-08-05-em-longitudinal-rolling-planning-design.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-planning.md`
-- `docs/superpowers/plans/2026-08-05-em-longitudinal-rolling-four-phase-roadmap.md`
-- `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-phase-3.md`
-
-The phase-3 code end is `6cfbaf6` (`feat: reuse prior trajectory in longitudinal planning`); Task 6 is `4159ae0`. Earlier Task 3/4 fixups `dbc7b7c` and `26bd822` intentionally remain separate because the shared worktree was already dirty and Git refused autosquash. Keep the actual interfaces from the phase-3 handoff, particularly `EmLongitudinalMode`, `LongitudinalTerminalSchedule`, explicit-mode `LongitudinalPlanningInput`, `PathUpperBoundS`, `StopBoundaryPathS`, `PathSpeedLimit.HasStopBoundary`, `EmTrajectoryMetadata.LongitudinalMode`, and `LongitudinalPreviousTrajectorySeed`.
-
-Execute only original-plan Tasks 8, 9, and 10. Use `executing-plans`, TDD for every behavior change, `systematic-debugging` for every unexpected build/test failure, and `verification-before-completion` before each commit and the final handoff. Do not re-run brainstorming. Do not create chassis behavior: `OBSERVE_ONLY` remains mandatory.
-
-Start by recording:
-
-```powershell
-git branch --show-current
-git status --short
-git log -10 --oneline
-git show --stat --oneline 6cfbaf6
-```
-
-The branch is `trajplanner` and the worktree contains substantial unrelated user changes. Never reset, checkout, clean, broadly stage, stash user files, use `git add .`, or use `git add -A`. Before every commit stage only that task's listed files, then run `git diff --cached --name-only` and `git diff --cached --check`.
-
-## Task 8 — service mode flow, Goal/GearSwitch, and multi-cycle regression
-
-Files:
-
-- `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs`
-
-Write and observe the failing tests first. With a sufficiently long unobstructed straight path, call `EmPlanningService.Plan` across three physical states and assert the published metadata flows in this exact order:
-
-```csharp
-Verification.Equal(EmLongitudinalMode.RollingContinuation,
-    rolling.Trajectory.Metadata.LongitudinalMode, "cycle 1 rolls");
-Verification.True(rolling.Trajectory.Points[rolling.Trajectory.Points.Count - 1]
-    .SignedLongitudinalVelocity != 0d, "cycle 1 has nonzero terminal speed");
-
-Verification.Equal(EmLongitudinalMode.ApproachStopBoundary,
-    approach.Trajectory.Metadata.LongitudinalMode, "cycle 2 approaches");
-
-Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary,
-    exact.Trajectory.Metadata.LongitudinalMode, "cycle 3 stops");
-Verification.NearlyEqual(0d, exact.Trajectory.Points[exactAnchor]
-    .SignedLongitudinalVelocity, "goal speed is zero");
-Verification.True(exact.Trajectory.Points.Count > exactAnchor + 1,
-    "goal anchor is followed by a QP stabilization point");
-Verification.NearlyEqual(exact.Trajectory.Points[exactAnchor].PathS,
-    exact.Trajectory.Points[exactAnchor + 1].PathS,
-    "goal stabilization keeps the stop position");
-Verification.NearlyEqual(0d, exact.Trajectory.Points[exactAnchor + 1]
-    .SignedLongitudinalVelocity, "goal stabilization speed is zero");
-```
-
-Repeat the exact-stop assertion using `CreateGearPairReferencePath()` and verify that the published trajectory contains no point from the following direction segment. Add an independent semantic regression with:
-
-```csharp
-configuration.Scheduling.DistanceHorizonMeters = 5d;
-configuration.Scheduling.TimeHorizonSeconds = 2d;
-configuration.Scheduling.OutputTimeStepSeconds = 0.1d;
-configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.2d;
-```
-
-It must publish 21 points, end before `PathS=5m`, have nonzero terminal speed, and pass publication validation. This proves distance is the LS window and time is the single ST horizon.
-
-Run the new test first:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all
-```
-
-Before fixture work, at least one assertion may fail because an old fake solver assumes every trajectory stops. If a production failure is exposed, repair it only in the original owning Task 1–7 scope and rerun its targeted tests; Task 8 itself is fixture/regression work and must not weaken production validation. Update scripted fixtures by mode:
-
-- rolling and approach return nonzero terminal velocity and no hold tail;
-- exact returns the real boundary anchor and its QP-internal `S/U/A` static tail, with only a later optional external hold.
-
-The phase-3 service fixture already uses a realistic longer direct path and captures the longitudinal QP for prior-trajectory soft-reference coverage. Preserve that coverage while adding the new mode-flow assertions.
-
-Required checks and commit:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all
-git add -- ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmFixtureFactory.cs
-git diff --cached --name-only
-git diff --cached --check
-git commit -m "test: cover rolling-to-stop EM planning flow"
-```
-
-Expected: `PASS longitudinal-model`, `PASS longitudinal-integration`, `PASS trajectory`, and `PASS em-planning-service` as part of `PASS em-core-all`.
-
-## Task 9 — observation diagnostics and documentation
-
-Files:
-
-- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs`
-- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
-- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`
-- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md`
-
-Write failing observation tests first. The one-time configuration diagnostic must include:
-
-```text
-maximumJerkLimitedStopDistance=
-maximumJerkLimitedStopDuration=
-requiredDistanceHorizon=
-```
-
-Every successful trajectory summary must include:
-
-```text
-longitudinalMode=RollingContinuation
-terminalSpeed=
-terminalAcceleration=
-```
-
-Failure diagnostics must include at least `longitudinalMode=`, `remainingToBoundary=`, `minimumStoppingDistance=`, `minimumStoppingDuration=`, and `maximumStoppedReachableDistance=`. Use the worst forward/reverse speed to print the maximum jerk-limited stopping capability once per session, not inside a per-frame UI redraw block. Preserve `OBSERVE_ONLY` and do not issue a chassis command.
-
-Update `EMPlanner/README.md` to state exactly that `DistanceHorizonMeters` controls the LS reference window, `TimeHorizonSeconds` controls ST output duration, only Goal/GearSwitchApproach may require an exact zero-speed terminal, and rolling/approach may publish nonzero terminal speed. Update the MovementTest README to state that rolling trajectory observation remains `OBSERVE_ONLY` and never commands the chassis.
-
-Required checks and commit:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
-git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md
-git diff --cached --name-only
-git diff --cached --check
-git commit -m "docs: explain rolling longitudinal planning diagnostics"
-```
-
-Expected: `PASS trajectory-observation`.
-
-## Task 10 — complete regression and target-machine OSQP verification
-
-Task 10 is verification-only: do not modify files and do not create an empty commit. Run:
-
-```powershell
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
-dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true
-dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-real-osqp-probe
-```
-
-Then perform the manual `TrajectoryObservationMovementTest` observation-only probe with the current manual parameters. Confirm one startup configuration print, rolling with nonzero terminal speed and no hold when far from Goal, approach near Goal, exact stop with `U=0,A=0`, no point-21 `JerkLimitExceeded`, and recurring `OBSERVE_ONLY: no chassis command is sent.`
-
-The real-OSQP probe must run on a machine with the required native dependency. If this window cannot load it, record the result precisely as pending target-machine verification; do not bypass it by modifying longitudinal code, solver residuals, or any validation threshold. Finish with `git diff --check`, `git status --short`, and `git log --oneline -12`, preserving all unrelated dirty work.
-
-After Task 10, use `writing-plans` to create `docs/superpowers/handoffs/2026-08-05-em-longitudinal-rolling-final.md`, record all command results and target-machine status, stage and commit only that final handoff document, and stop.
-
-Global semantics are non-negotiable: `DistanceHorizonMeters` is only an LS look-ahead window; `TimeHorizonSeconds` is only one ST horizon; only actual Goal/GearSwitch boundaries permit exact zero speed; `ZeroSpeedHoldSeconds` is outside QP time; previous-trajectory arrays remain objective soft references; never loosen jerk, acceleration, solver, or validation thresholds; and never add chassis commands.
diff --git a/docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md b/docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md
deleted file mode 100644
index f8b018c..0000000
--- a/docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md
+++ /dev/null
@@ -1,5664 +0,0 @@
-# Local G2 Minimum Curvature Excursion Feasibility Report
-
-## Result
-
-- Final state: `QUALITY_RED`
-- Underlying state without performance gate: `QUALITY_RED`
-- Authorization boundary: no evaluator change
-- Selected stable key: `not evaluated`
-
-## Isolation And Identity
-
-- Shared baseline commit: `da9157b17a7e4206ed4cdebd1985ee2577fae480`
-- Frozen core/strict commit: `3006aa0fd8dac8e4665ff3b89b7cda139e88e940`
-- Measurement commit: `c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825`
-- Strict root: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\strict-root-3006aa0`
-- Measurement root: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\measurement-root-3006aa0`
-- Measurement commit parent: `3006aa0fd8dac8e4665ff3b89b7cda139e88e940`
-- Measurement commit changed path: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs`
-- Fixture SHA-256: `3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563`
-- Shared evaluator SHA-256: `929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3`
-- Strict evaluator SHA-256: `929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3`
-- Measurement evaluator SHA-256: `ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e`
-- Probe-source manifest SHA-256: `9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea`
-- Frozen core JSON: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\frozen-core.json` (SHA-256 `08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa`)
-- Frozen canonical source manifest: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\frozen-core-probe-source-manifest.txt` (raw-byte SHA-256 `9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea`)
-- Strict-vs-shared-baseline complete diff: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\strict-vs-shared-baseline.diff` (SHA-256 `188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982`)
-- Measurement evaluator diff: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\measurement-evaluator.diff` (SHA-256 `d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd`)
-- Measurement commit history proof: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\measurement-commit-history-proof.json` (SHA-256 `b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0`)
-
-## Frozen Probe Source Manifest
-
-The following canonical path/SHA-256 entries bind every reported probe source.
-```text
-.gitattributes	705fd4d6451a31d36b3df7de96f83f30ac976c9b4a6d1e51671d8e2f33e2d0da
-ClumsyPilot/ClumsyPilot.csproj	b0c0485b2baca817ac96fde51c4c2b61a22bd0c2275d7ae6abb142214d822988
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs	9b0a0618af3858f3089086fceb3463040fde3242d01df212c58d71f5a91440ce
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs	f65b1207e54c344b825907e8181f28cf3732a153d1564510f49c851b90f78569
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs	cc5d3fbe6bfdc3655e9f21c177b2e18cc82871ceb5cc1ee47ab90e5aad468611
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs	d9e3b4c2891eb5a8d00778b3182eb3edecc1f3b9e57fd9a287209ab85d1d91ed
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs	999f643a058673a4b6520416f1d6422effdfd44a6b8351faee3898c0f14f3e47
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs	eaab3e2d1baf73a56e30419fef47e0220fb8e3acfa9a2cc309d6a628c59bc449
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs	32b76987f4227be10f9576cdac57b6750a370fb45593605cc7b9847497efc919
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs	aa6684a7745061b2be35e2bd8c6778a0c52a41715f06ba3ab88a2f08e68ce98f
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs	d0083b459785ee7a646810d1b39e47be8bfffd111fdbfb90c7e810ec5a061405
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs	bac0a1da812cbefad50e2f944c2fad24a0bd4eb043f7b44a95325c795411c034
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs	ed6333bbc77140ade49f7d60117167592f369c3f32dd08e6b129ac6208342519
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs	8f14263eea1a9f4426ee838d4caddb88c68e99224f32004222d81e3551626a26
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs	037e38ae32dc4fdacc95e397752bb1320b9c61d047150d6f2998a3a5d8ed51de
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs	d6ab8299c5604004d9e0e09c1426f0d88ca0a8f091a7f3b7fc39372f95048bd9
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs	f4f184094b4e9a483b93af457767d72860314780bc600fb536d3e3e7b27f5364
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs	74ef0ce76c75b4b6af9941c4e79be908e9ebe0424132a6ddf1d3d5f535ec5e83
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs	44d6902f7db6bb0c5797db573ce70fe1b1c5ce950a4a26db18948315b894365b
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs	7c8663ca0430e40d7f5c2c75669c18912cbecdd70cbb4b3ab538efb6021fd03b
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs	c084bee2ae90e95d25e0d19bc3b6276a8ad4135b32b50d961911cf1bc56860ad
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs	a140f4ace9830e3d110b76746b82a913f2335bd360d7791b4e79edea8adf61d9
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs	881741ccf08b0f8b7aa060e9123d461be949cf4ae6421ae04d5767b92341704b
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs	98730f489741ffd0cb99c0a8672ca25dc7c7797a84043189820a78fa9c210db5
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs	89f7fbb9c529d318c1dd98e7820f5d7f9d39a908e8a934c8a7201401115e26a9
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs	7af0dd0bf4f22ccb5e516fdba17a2c91feb9405afd5326a1a4d1ff122a3d0507
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs	8bf28d9d7ee4d14692a2720eb71483f6f32433127ac4ab0eb4e2cf6d1c794d8d
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs	43e0428b0fb5ab7dfedf1013308062faa6d82c54dea9043e92d756e9d2529028
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs	25ef83f499c9663599f6cb77d95888d23b43fe1af93521a75f6262eb9d6cf465
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs	86e994ebee46e3250907b7958910de14672c12563a626bc811707ab2ec19e2dd
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs	090e4ea91bd5ed59f4d8233b1c1bb67108f954686ac0601f077ca34cf24410d2
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs	89c7f461e992d73714ef42e27a08188934dd9a6a416a72bf928d0b3f3b668113
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs	fabaf74ef20f09c26da1de650ca5efbc9324886b0ce9358493c812f210a400fc
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs	d20291071775dfb8326a960c7d01a1d97054ca182d7382a5ead0137a2eee2df4
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs	d98c78adc7b01dbc6cdc985e891c23da6d0d84e62328b4f30104b08a482c1943
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs	fa32bf227e537519fd525d95ca3e937873e3959273cd0f81197f4d59f740463c
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs	37c0cc594ef516d2e933db07e994e0a93ee0126a7a39894fca4216f093d70b97
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs	27c0d7b06f4e3b319aac33a2105a5a23afd793adef89328db15ea16deacd245a
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs	5b008f0dff69b2f334aadb0950538e19654a192217cd1914175b8f75cb08650e
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs	01f8cbc788182bb123325cfbf7e6548fa599e6cf3d8a909e43e1351dd9ad8607
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs	87fec1e39dd1c07ed0fb15589aef3eb43112a1e7b59aac003784fd88df66a575
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs	4085eebfc4da76e4f776c335262ab51882a5a5ad7e71ee2d18525c9364577279
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs	302c227360c06d979cb1014261f34db410112b5be66861e12abe932e34ef926d
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs	21c9866703cf1e4f3f738918aac69c6689ac1247ddcf304b79c0c62dc767b9df
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs	51c8999b2ba20313e7051d35a7b95a8a067c91e725b0a650f7bc7c3dcbc57a0c
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs	b0a569d009b053b0e48bdbc3385198830a64040403828ed93bb33575fa76715e
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs	b82552b1dfb949023aef5c50915ae2ddfc4f3a040d927f7a5c3bc12a20ca7e82
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs	7e16ff91667f68464afcf657d8023211dbfd412e01918ec49bc5d2b18f42e916
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs	fb7cbde6a9951c1d437f4a71546935c906f9f69d1424a89b4722fa07e6fdace1
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs	d7cc587508fe7cdea7103fce857be6e82702e08597f30011067ccb5ab9fbcfee
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs	3adfdce20d777d2910d121c92f17aab9c22098b8316ca30f8b658ed564f8d3ae
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs	0dbb2861040513765a76fb7f3dff6f6527deba499f49d5dd51ed4977fa8b087a
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs	d20f5a64e380e4ae7f338d0ed28dbb3b587460fd35bfe383a7696fe91955167f
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs	390c5267531b9960a3cb75248338a235ccd2d96d088073f6e8cc697a1b5a17d7
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs	731488374aa437490c659c3e8aa11393110caf6e2e6f590e1f77ff95f3e9d27f
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs	2cbc2c2eb75b180a6780c571b5c9fd7a94c8bf12f63e1a3d38154ece95675219
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs	e525fa34c1067cb4519d846f9c4e46bc2ed6c2b0c150370ad9e5cb7ef91e6359
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs	a9af9650d6f87638d5f145004199ceb292b974c881a164af40543714cb92d831
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json	3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs	140366d57a193695f6feb3d06b6ba671eea53e5159d29b79a2f986f6ec4dc01d
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs	2b56ad96f59553a10f76417f1ec2c47330009052461182d5ecb152b91c8edc83
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs	34300bcea0a8e3e798846e8ffef7c60d05063ec286de25449ec79bba6926a9fc
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs	bc7b22ac77976705dc01125585ab9e8a4d9e8320f4e465bd38585d5ae80c013d
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs	a7c57aa7133e8ed33c41478191b19c527a2297d043444f3b4e46f689da917179
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs	3b5891fb2749b4a7da252b1b2c9062c8514c0332db5cae5ba44ed787ae708987
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs	8be3e3ddc4a28eaa0baf551ad8cfe422c93dad3cc15449d14aeb2228bb4fd924
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs	601bd157393ee9966ea599637a52c50e584f4d1dbdd14ab3ff8846b9878fb8f8
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs	f5d4b49c5d3ae6ceb716dbf15a9626aa231a50906327886b71eb5c5a65d3ffc5
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs	f7c7f1efc7cf2fdc41d4a559ef7ed2e85222d6920b58c644fe37651721ebea36
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs	b1990262eca967701807d7042126b2c4038e28b2ea10b0de79a58e40b92bc003
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs	9894641d0b129279e85159a8a3ca57d02c976e77cca9c545eadb04c0343a0ab5
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs	329e66096b91561d39e694d84ac86c3d425e54d7805b89bc92f71e86b763fb3f
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs	c0bbe51e3866f9f8b71b825d1de2fd776521ed8ac5d78c4423c91cb6a98cbf39
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs	e35c2e9330b85d234788e472b2ee4c9f9e7b97eb8ca07d32385caa9ea3cd6ac3
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs	2e44d5fd5c8109567b5c3e34a5d786a8829ed15c934ad39e74c9fed652ea086c
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs	f375e5e650bb41dd83d0faa3cda2d4675b34f9ef40fc6610b97b1cfc64302413
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs	6aa92a717b01a2c764e0bf8d6798fa6d49ef9cfa48cd0fa25673f9d8cc40459c
-ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs	84ba6de6947275429d298a30ec7dc80a2551ad3a87b20e80392799e00bd02672
-ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1	a17ef20fe0b62808c8fd556b234b802b28d6ad31d09837f683b74560d4e41b19
-ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1	61a7604e1a8ebd092e31cfdbaaf1f0c7a9d86daa628b2ea5f16e3d1c611f5847
-ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1	b8e4c84e970316f187b6d8f4fa7a420d68a26a646365a9e871330ff2f52abcd6
-```
-
-## Search And Candidate Audit
-
-- Windows/combinations/solver invocations/candidates: `8/24/30/6`
-- Failed combinations/max intervals/search stop: `18/2/None`
-- Strict/physical/excursion-qualified counts: `0/3/0`
-
-Every attempt, hard node, interval control, solver exit, closure/seam metric,
-candidate digest, evaluator stop gate, and measurement gate trace is retained
-in the candidate audit evidence linked below. Null means `not evaluated`; it is never rendered as zero.
-
-```json
-[
-    {
-        "StableKey":  "single-turn/s0/r0/w1/seed0",
-        "CandidateSha256":  "0679cda975628b2713d662c4b7a9516831b9d865f7a387a8cbff96fc0a76f6a8",
-        "StrictCandidateSha256":  "0679cda975628b2713d662c4b7a9516831b9d865f7a387a8cbff96fc0a76f6a8",
-        "MeasurementCandidateSha256":  "0679cda975628b2713d662c4b7a9516831b9d865f7a387a8cbff96fc0a76f6a8",
-        "StrictCandidateContent":  {
-                                       "PreparedSegmentDirection":  0,
-                                       "SegmentIndex":  0,
-                                       "WindowStartArcLengthMeters":  1.8999999999999981,
-                                       "WindowEndArcLengthMeters":  2.0999999999999983,
-                                       "DirectionSign":  1,
-                                       "HardNodes":  [
-                                                         {
-                                                             "X":  2.7076608659177555,
-                                                             "Y":  1.7136150003593424,
-                                                             "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                             "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                             "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                             "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                         },
-                                                         {
-                                                             "X":  2.7764244476704873,
-                                                             "Y":  1.786210614200662,
-                                                             "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                             "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                         },
-                                                         {
-                                                             "X":  2.8436656720787941,
-                                                             "Y":  1.8602282995202657,
-                                                             "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0,
-                                                             "VehicleCurvaturePerMeter":  0
-                                                         }
-                                                     ],
-                                       "CandidatePoints":  [
-                                                               {
-                                                                   "X":  2.7076608659177555,
-                                                                   "Y":  1.7136150003593424,
-                                                                   "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                   "HeadingRadians":  0.79166666666666563,
-                                                                   "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7216585404159015,
-                                                                   "Y":  1.7279002807977561,
-                                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                   "HeadingRadians":  0.79933337655263847,
-                                                                   "UnwrappedHeadingRadians":  0.79933337655263847,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7355462804754538,
-                                                                   "Y":  1.7422924527500654,
-                                                                   "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                   "HeadingRadians":  0.807333381316923,
-                                                                   "UnwrappedHeadingRadians":  0.807333381316923,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7493087549485469,
-                                                                   "Y":  1.7568044286397639,
-                                                                   "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                   "HeadingRadians":  0.8166666919149419,
-                                                                   "UnwrappedHeadingRadians":  0.8166666919149419,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7629303026032415,
-                                                                   "Y":  1.771448757055422,
-                                                                   "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                   "HeadingRadians":  0.82633333108200946,
-                                                                   "UnwrappedHeadingRadians":  0.82633333108200946,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7764244476704873,
-                                                                   "Y":  1.786210614200662,
-                                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7898568979963914,
-                                                                   "Y":  1.8010284915422041,
-                                                                   "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                   "HeadingRadians":  0.834666667171262,
-                                                                   "UnwrappedHeadingRadians":  0.834666667171262,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8032942868818131,
-                                                                   "Y":  1.8158418905900111,
-                                                                   "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                   "HeadingRadians":  0.8333333334352736,
-                                                                   "UnwrappedHeadingRadians":  0.8333333334352736,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8167514155081106,
-                                                                   "Y":  1.8306373604856185,
-                                                                   "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                   "HeadingRadians":  0.83233333309080682,
-                                                                   "UnwrappedHeadingRadians":  0.83233333309080682,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8302134757231938,
-                                                                   "Y":  1.8454283444093615,
-                                                                   "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                   "HeadingRadians":  0.83266666648232845,
-                                                                   "UnwrappedHeadingRadians":  0.83266666648232845,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8436656720787941,
-                                                                   "Y":  1.8602282995202657,
-                                                                   "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               }
-                                                           ]
-                                   },
-        "MeasurementCandidateContent":  {
-                                            "PreparedSegmentDirection":  0,
-                                            "SegmentIndex":  0,
-                                            "WindowStartArcLengthMeters":  1.8999999999999981,
-                                            "WindowEndArcLengthMeters":  2.0999999999999983,
-                                            "DirectionSign":  1,
-                                            "HardNodes":  [
-                                                              {
-                                                                  "X":  2.7076608659177555,
-                                                                  "Y":  1.7136150003593424,
-                                                                  "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                  "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                  "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                  "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                              },
-                                                              {
-                                                                  "X":  2.7764244476704873,
-                                                                  "Y":  1.786210614200662,
-                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                  "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                              },
-                                                              {
-                                                                  "X":  2.8436656720787941,
-                                                                  "Y":  1.8602282995202657,
-                                                                  "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0,
-                                                                  "VehicleCurvaturePerMeter":  0
-                                                              }
-                                                          ],
-                                            "CandidatePoints":  [
-                                                                    {
-                                                                        "X":  2.7076608659177555,
-                                                                        "Y":  1.7136150003593424,
-                                                                        "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                        "HeadingRadians":  0.79166666666666563,
-                                                                        "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7216585404159015,
-                                                                        "Y":  1.7279002807977561,
-                                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                        "HeadingRadians":  0.79933337655263847,
-                                                                        "UnwrappedHeadingRadians":  0.79933337655263847,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7355462804754538,
-                                                                        "Y":  1.7422924527500654,
-                                                                        "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                        "HeadingRadians":  0.807333381316923,
-                                                                        "UnwrappedHeadingRadians":  0.807333381316923,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7493087549485469,
-                                                                        "Y":  1.7568044286397639,
-                                                                        "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                        "HeadingRadians":  0.8166666919149419,
-                                                                        "UnwrappedHeadingRadians":  0.8166666919149419,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7629303026032415,
-                                                                        "Y":  1.771448757055422,
-                                                                        "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                        "HeadingRadians":  0.82633333108200946,
-                                                                        "UnwrappedHeadingRadians":  0.82633333108200946,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7764244476704873,
-                                                                        "Y":  1.786210614200662,
-                                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7898568979963914,
-                                                                        "Y":  1.8010284915422041,
-                                                                        "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                        "HeadingRadians":  0.834666667171262,
-                                                                        "UnwrappedHeadingRadians":  0.834666667171262,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8032942868818131,
-                                                                        "Y":  1.8158418905900111,
-                                                                        "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                        "HeadingRadians":  0.8333333334352736,
-                                                                        "UnwrappedHeadingRadians":  0.8333333334352736,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8167514155081106,
-                                                                        "Y":  1.8306373604856185,
-                                                                        "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                        "HeadingRadians":  0.83233333309080682,
-                                                                        "UnwrappedHeadingRadians":  0.83233333309080682,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8302134757231938,
-                                                                        "Y":  1.8454283444093615,
-                                                                        "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                        "HeadingRadians":  0.83266666648232845,
-                                                                        "UnwrappedHeadingRadians":  0.83266666648232845,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8436656720787941,
-                                                                        "Y":  1.8602282995202657,
-                                                                        "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    }
-                                                                ]
-                                        },
-        "StrictDelayedCGenRangeSnapshot":  "not evaluated",
-        "StrictRecord":  {
-                             "StableKey":  "single-turn/s0/r0/w1/seed0",
-                             "CandidateSha256":  "0679cda975628b2713d662c4b7a9516831b9d865f7a387a8cbff96fc0a76f6a8",
-                             "CandidateContent":  {
-                                                      "PreparedSegmentDirection":  0,
-                                                      "SegmentIndex":  0,
-                                                      "WindowStartArcLengthMeters":  1.8999999999999981,
-                                                      "WindowEndArcLengthMeters":  2.0999999999999983,
-                                                      "DirectionSign":  1,
-                                                      "HardNodes":  [
-                                                                        {
-                                                                            "X":  2.7076608659177555,
-                                                                            "Y":  1.7136150003593424,
-                                                                            "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                            "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                            "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                            "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                        },
-                                                                        {
-                                                                            "X":  2.7764244476704873,
-                                                                            "Y":  1.786210614200662,
-                                                                            "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                            "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                                        },
-                                                                        {
-                                                                            "X":  2.8436656720787941,
-                                                                            "Y":  1.8602282995202657,
-                                                                            "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0,
-                                                                            "VehicleCurvaturePerMeter":  0
-                                                                        }
-                                                                    ],
-                                                      "CandidatePoints":  [
-                                                                              {
-                                                                                  "X":  2.7076608659177555,
-                                                                                  "Y":  1.7136150003593424,
-                                                                                  "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                  "HeadingRadians":  0.79166666666666563,
-                                                                                  "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7216585404159015,
-                                                                                  "Y":  1.7279002807977561,
-                                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                  "HeadingRadians":  0.79933337655263847,
-                                                                                  "UnwrappedHeadingRadians":  0.79933337655263847,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7355462804754538,
-                                                                                  "Y":  1.7422924527500654,
-                                                                                  "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                  "HeadingRadians":  0.807333381316923,
-                                                                                  "UnwrappedHeadingRadians":  0.807333381316923,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7493087549485469,
-                                                                                  "Y":  1.7568044286397639,
-                                                                                  "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                  "HeadingRadians":  0.8166666919149419,
-                                                                                  "UnwrappedHeadingRadians":  0.8166666919149419,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7629303026032415,
-                                                                                  "Y":  1.771448757055422,
-                                                                                  "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                  "HeadingRadians":  0.82633333108200946,
-                                                                                  "UnwrappedHeadingRadians":  0.82633333108200946,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7764244476704873,
-                                                                                  "Y":  1.786210614200662,
-                                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7898568979963914,
-                                                                                  "Y":  1.8010284915422041,
-                                                                                  "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                                  "HeadingRadians":  0.834666667171262,
-                                                                                  "UnwrappedHeadingRadians":  0.834666667171262,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8032942868818131,
-                                                                                  "Y":  1.8158418905900111,
-                                                                                  "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                                  "HeadingRadians":  0.8333333334352736,
-                                                                                  "UnwrappedHeadingRadians":  0.8333333334352736,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8167514155081106,
-                                                                                  "Y":  1.8306373604856185,
-                                                                                  "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                                  "HeadingRadians":  0.83233333309080682,
-                                                                                  "UnwrappedHeadingRadians":  0.83233333309080682,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8302134757231938,
-                                                                                  "Y":  1.8454283444093615,
-                                                                                  "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                                  "HeadingRadians":  0.83266666648232845,
-                                                                                  "UnwrappedHeadingRadians":  0.83266666648232845,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8436656720787941,
-                                                                                  "Y":  1.8602282995202657,
-                                                                                  "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              }
-                                                                          ]
-                                                  },
-                             "RecordKind":  "Canonical",
-                             "CandidateIndex":  0,
-                             "CanonicalCandidateIndex":  0,
-                             "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed0",
-                             "EquivalenceParameters":  [
-                                                           0,
-                                                           0.25,
-                                                           0.5,
-                                                           0.75,
-                                                           1
-                                                       ],
-                             "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                             "EvaluatorExecutedDirectly":  true,
-                             "EvaluatorResult":  "CurvatureExceeded",
-                             "StopGate":  "VehicleCurvature",
-                             "MeasurementTrace":  "not evaluated",
-                             "EvaluatorElapsedMilliseconds":  11.0804,
-                             "RawMinimumCurvature":  "not evaluated",
-                             "RawMaximumCurvature":  "not evaluated",
-                             "CandidateMinimumCurvature":  "not evaluated",
-                             "CandidateMaximumCurvature":  "not evaluated",
-                             "DelayedCGenRangeSnapshot":  "not evaluated",
-                             "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                             "MaximumDeviationLimitMeters":  0.1,
-                             "MinimumPeakGradientImprovementRatio":  0.2,
-                             "MaximumVariationCostRegressionRatio":  0.02,
-                             "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                             "DirectionSign":  1,
-                             "HardNodes":  [
-                                               {
-                                                   "X":  2.7076608659177555,
-                                                   "Y":  1.7136150003593424,
-                                                   "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                   "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                   "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                   "VehicleCurvaturePerMeter":  0.41666666666666663
-                                               },
-                                               {
-                                                   "X":  2.7764244476704873,
-                                                   "Y":  1.786210614200662,
-                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                   "VehicleCurvaturePerMeter":  0.20833333333333331
-                                               },
-                                               {
-                                                   "X":  2.8436656720787941,
-                                                   "Y":  1.8602282995202657,
-                                                   "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0,
-                                                   "VehicleCurvaturePerMeter":  0
-                                               }
-                                           ],
-                             "Intervals":  [
-                                               {
-                                                   "IntervalIndex":  0,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.10000075784381711,
-                                                   "GeometricC0":  0.41666666666666663,
-                                                   "GeometricC1":  0.20833169450289493,
-                                                   "GeometricC2":  0.83332234152921,
-                                                   "GeometricC3":  0.20833333333333331,
-                                                   "VehicleC0":  0.41666666666666663,
-                                                   "VehicleC1":  0.20833169450289493,
-                                                   "VehicleC2":  0.83332234152921,
-                                                   "VehicleC3":  0.20833333333333331,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  744,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  3639,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.3639,
-                                                   "PreOverwritePositionErrorMeters":  2.1346517098741323E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  0.20833333333333348,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.49456530714619418,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.749528153736446
-                                               },
-                                               {
-                                                   "IntervalIndex":  1,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.10000003444667947,
-                                                   "GeometricC0":  0.20833333333333331,
-                                                   "GeometricC1":  -0.41666665486791415,
-                                                   "GeometricC2":  0.20833332153458084,
-                                                   "GeometricC3":  0,
-                                                   "VehicleC0":  0.20833333333333331,
-                                                   "VehicleC1":  -0.41666665486791415,
-                                                   "VehicleC2":  0.20833332153458084,
-                                                   "VehicleC3":  0,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  648,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1472,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1472,
-                                                   "PreOverwritePositionErrorMeters":  2.1362552671271875E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  -0.077903907886512636,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.74999318728737
-                                               }
-                                           ],
-                             "CandidateDiagnostic":  {
-                                                         "WindowIndex":  1,
-                                                         "SeedIndex":  0,
-                                                         "CombinationAttemptIndex":  4,
-                                                         "SolverInvocationCount":  5,
-                                                         "MaximumPreOverwritePositionErrorMeters":  2.1362552671271875E-12,
-                                                         "MaximumPostOverwritePositionErrorMeters":  0,
-                                                         "MaximumTangentSeamErrorRadians":  0,
-                                                         "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                         "MinimumAdjacentPointDistanceMeters":  0.020000005266670992
-                                                     }
-                         },
-        "MeasurementRecord":  {
-                                  "StableKey":  "single-turn/s0/r0/w1/seed0",
-                                  "CandidateSha256":  "0679cda975628b2713d662c4b7a9516831b9d865f7a387a8cbff96fc0a76f6a8",
-                                  "CandidateContent":  {
-                                                           "PreparedSegmentDirection":  0,
-                                                           "SegmentIndex":  0,
-                                                           "WindowStartArcLengthMeters":  1.8999999999999981,
-                                                           "WindowEndArcLengthMeters":  2.0999999999999983,
-                                                           "DirectionSign":  1,
-                                                           "HardNodes":  [
-                                                                             {
-                                                                                 "X":  2.7076608659177555,
-                                                                                 "Y":  1.7136150003593424,
-                                                                                 "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                 "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                                 "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.7764244476704873,
-                                                                                 "Y":  1.786210614200662,
-                                                                                 "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                                 "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.8436656720787941,
-                                                                                 "Y":  1.8602282995202657,
-                                                                                 "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0,
-                                                                                 "VehicleCurvaturePerMeter":  0
-                                                                             }
-                                                                         ],
-                                                           "CandidatePoints":  [
-                                                                                   {
-                                                                                       "X":  2.7076608659177555,
-                                                                                       "Y":  1.7136150003593424,
-                                                                                       "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                       "HeadingRadians":  0.79166666666666563,
-                                                                                       "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7216585404159015,
-                                                                                       "Y":  1.7279002807977561,
-                                                                                       "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                       "HeadingRadians":  0.79933337655263847,
-                                                                                       "UnwrappedHeadingRadians":  0.79933337655263847,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7355462804754538,
-                                                                                       "Y":  1.7422924527500654,
-                                                                                       "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                       "HeadingRadians":  0.807333381316923,
-                                                                                       "UnwrappedHeadingRadians":  0.807333381316923,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7493087549485469,
-                                                                                       "Y":  1.7568044286397639,
-                                                                                       "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                       "HeadingRadians":  0.8166666919149419,
-                                                                                       "UnwrappedHeadingRadians":  0.8166666919149419,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7629303026032415,
-                                                                                       "Y":  1.771448757055422,
-                                                                                       "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                       "HeadingRadians":  0.82633333108200946,
-                                                                                       "UnwrappedHeadingRadians":  0.82633333108200946,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7764244476704873,
-                                                                                       "Y":  1.786210614200662,
-                                                                                       "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7898568979963914,
-                                                                                       "Y":  1.8010284915422041,
-                                                                                       "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                                       "HeadingRadians":  0.834666667171262,
-                                                                                       "UnwrappedHeadingRadians":  0.834666667171262,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8032942868818131,
-                                                                                       "Y":  1.8158418905900111,
-                                                                                       "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                                       "HeadingRadians":  0.8333333334352736,
-                                                                                       "UnwrappedHeadingRadians":  0.8333333334352736,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8167514155081106,
-                                                                                       "Y":  1.8306373604856185,
-                                                                                       "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                                       "HeadingRadians":  0.83233333309080682,
-                                                                                       "UnwrappedHeadingRadians":  0.83233333309080682,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8302134757231938,
-                                                                                       "Y":  1.8454283444093615,
-                                                                                       "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                                       "HeadingRadians":  0.83266666648232845,
-                                                                                       "UnwrappedHeadingRadians":  0.83266666648232845,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8436656720787941,
-                                                                                       "Y":  1.8602282995202657,
-                                                                                       "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   }
-                                                                               ]
-                                                       },
-                                  "RecordKind":  "Canonical",
-                                  "CandidateIndex":  0,
-                                  "CanonicalCandidateIndex":  0,
-                                  "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed0",
-                                  "EquivalenceParameters":  [
-                                                                0,
-                                                                0.25,
-                                                                0.5,
-                                                                0.75,
-                                                                1
-                                                            ],
-                                  "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                                  "EvaluatorExecutedDirectly":  true,
-                                  "EvaluatorResult":  "CurvatureExceeded",
-                                  "StopGate":  "VehicleCurvature",
-                                  "MeasurementTrace":  {
-                                                           "CandidateIndex":  0,
-                                                           "RawRangeExceeded":  "not evaluated",
-                                                           "CollisionAndMapBoundaryPassed":  "not evaluated",
-                                                           "ExecutedGates":  [
-                                                                                 "InputAnalysis",
-                                                                                 "VehicleCurvature"
-                                                                             ],
-                                                           "PassedGates":  "InputAnalysis",
-                                                           "StopGate":  "VehicleCurvature",
-                                                           "Result":  "CurvatureExceeded",
-                                                           "Reason":  "局部 G2 候选超过车辆曲率上限。",
-                                                           "RawMinimumCurvature":  "not evaluated",
-                                                           "RawMaximumCurvature":  "not evaluated",
-                                                           "CandidateMinimumCurvature":  "not evaluated",
-                                                           "CandidateMaximumCurvature":  "not evaluated",
-                                                           "Eminus":  "not evaluated",
-                                                           "Eplus":  "not evaluated",
-                                                           "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                                           "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  812.30026014210557,
-                                                           "MaximumDeviationMeters":  "not evaluated",
-                                                           "MaximumDeviationLimitMeters":  "not evaluated",
-                                                           "MinimumBodyClearanceMeters":  "not evaluated",
-                                                           "MinimumClearanceReserveMeters":  "not evaluated",
-                                                           "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "MinimumPeakGradientImprovementRatio":  "not evaluated",
-                                                           "RawCurvatureVariationCost":  "not evaluated",
-                                                           "CandidateCurvatureVariationCost":  "not evaluated",
-                                                           "MaximumVariationCostRegressionRatio":  "not evaluated",
-                                                           "AbsolutePathLengthChangeMeters":  "not evaluated"
-                                                       },
-                                  "EvaluatorElapsedMilliseconds":  0.0754,
-                                  "RawMinimumCurvature":  "not evaluated",
-                                  "RawMaximumCurvature":  "not evaluated",
-                                  "CandidateMinimumCurvature":  "not evaluated",
-                                  "CandidateMaximumCurvature":  "not evaluated",
-                                  "DelayedCGenRangeSnapshot":  "not evaluated",
-                                  "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                                  "MaximumDeviationLimitMeters":  0.1,
-                                  "MinimumPeakGradientImprovementRatio":  0.2,
-                                  "MaximumVariationCostRegressionRatio":  0.02,
-                                  "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                  "DirectionSign":  1,
-                                  "HardNodes":  [
-                                                    {
-                                                        "X":  2.7076608659177555,
-                                                        "Y":  1.7136150003593424,
-                                                        "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                        "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                        "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                        "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                    },
-                                                    {
-                                                        "X":  2.7764244476704873,
-                                                        "Y":  1.786210614200662,
-                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                        "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                    },
-                                                    {
-                                                        "X":  2.8436656720787941,
-                                                        "Y":  1.8602282995202657,
-                                                        "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0,
-                                                        "VehicleCurvaturePerMeter":  0
-                                                    }
-                                                ],
-                                  "Intervals":  [
-                                                    {
-                                                        "IntervalIndex":  0,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.10000075784381711,
-                                                        "GeometricC0":  0.41666666666666663,
-                                                        "GeometricC1":  0.20833169450289493,
-                                                        "GeometricC2":  0.83332234152921,
-                                                        "GeometricC3":  0.20833333333333331,
-                                                        "VehicleC0":  0.41666666666666663,
-                                                        "VehicleC1":  0.20833169450289493,
-                                                        "VehicleC2":  0.83332234152921,
-                                                        "VehicleC3":  0.20833333333333331,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  744,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1665,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1665,
-                                                        "PreOverwritePositionErrorMeters":  2.1346517098741323E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  0.20833333333333348,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.49456530714619418,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.749528153736446
-                                                    },
-                                                    {
-                                                        "IntervalIndex":  1,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.10000003444667947,
-                                                        "GeometricC0":  0.20833333333333331,
-                                                        "GeometricC1":  -0.41666665486791415,
-                                                        "GeometricC2":  0.20833332153458084,
-                                                        "GeometricC3":  0,
-                                                        "VehicleC0":  0.20833333333333331,
-                                                        "VehicleC1":  -0.41666665486791415,
-                                                        "VehicleC2":  0.20833332153458084,
-                                                        "VehicleC3":  0,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  648,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1492,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1492,
-                                                        "PreOverwritePositionErrorMeters":  2.1362552671271875E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  -0.077903907886512636,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.74999318728737
-                                                    }
-                                                ],
-                                  "CandidateDiagnostic":  {
-                                                              "WindowIndex":  1,
-                                                              "SeedIndex":  0,
-                                                              "CombinationAttemptIndex":  4,
-                                                              "SolverInvocationCount":  5,
-                                                              "MaximumPreOverwritePositionErrorMeters":  2.1362552671271875E-12,
-                                                              "MaximumPostOverwritePositionErrorMeters":  0,
-                                                              "MaximumTangentSeamErrorRadians":  0,
-                                                              "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                              "MinimumAdjacentPointDistanceMeters":  0.020000005266670992
-                                                          }
-                              },
-        "SourceIdentity":  {
-                               "SharedBaselineCommit":  "da9157b17a7e4206ed4cdebd1985ee2577fae480",
-                               "FrozenCoreCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "StrictRootCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementRootCommit":  "c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825",
-                               "StrictRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\strict-root-3006aa0",
-                               "MeasurementRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\measurement-root-3006aa0",
-                               "FixtureSha256":  "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
-                               "SharedBaselineEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "StrictEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "MeasurementEvaluatorSha256":  "ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e",
-                               "MeasurementCommitParent":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementCommitChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "MeasurementCommitHistoryProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-commit-history-proof.json",
-                               "MeasurementCommitHistoryProofSha256":  "b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0",
-                               "ProbeSourceManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea",
-                               "AllowedChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "StrictAllowedDifferenceSummary":  "Complete strict-root source diff from the shared baseline.",
-                               "StrictAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictAllowedDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "StrictSharedBaselineDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictSharedBaselineDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "MeasurementAllowedDifferenceSummary":  "Only the measurement evaluator raw-range branch and trace capture differ.",
-                               "MeasurementAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-evaluator.diff",
-                               "MeasurementEvaluatorDiffSha256":  "d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd",
-                               "StrictEvaluatorHashProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-evaluator-hash-proof.json",
-                               "StrictEvaluatorHashProofSha256":  "a0869c7b26f8e203dca2361dd39169d60f0a8fd3c9dd58c9697aed7abd5c94e7",
-                               "FrozenCoreJsonPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core.json",
-                               "FrozenCoreJsonSha256":  "08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa",
-                               "FrozenCoreManifestPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core-probe-source-manifest.txt",
-                               "FrozenCoreManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea"
-                           },
-        "RecordKind":  "Canonical",
-        "CanonicalCandidateIndex":  0,
-        "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed0",
-        "EquivalenceParameters":  [
-                                      0,
-                                      0.25,
-                                      0.5,
-                                      0.75,
-                                      1
-                                  ],
-        "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-        "EvaluatorExecutedDirectly":  true,
-        "StrictResult":  "CurvatureExceeded",
-        "StrictStopGate":  "VehicleCurvature",
-        "MeasurementResult":  "CurvatureExceeded",
-        "MeasurementStopGate":  "VehicleCurvature",
-        "MeasurementReason":  "局部 G2 候选超过车辆曲率上限。",
-        "MeasurementExecutedGates":  [
-                                         "InputAnalysis",
-                                         "VehicleCurvature"
-                                     ],
-        "MeasurementPassedGates":  "InputAnalysis",
-        "RawRangeExceeded":  "not evaluated",
-        "RawMinimumCurvature":  "not evaluated",
-        "RawMaximumCurvature":  "not evaluated",
-        "CandidateMinimumCurvature":  "not evaluated",
-        "CandidateMaximumCurvature":  "not evaluated",
-        "HardPhysicalQualified":  false,
-        "Eminus":  "not evaluated",
-        "Eplus":  "not evaluated",
-        "Rminus":  "not evaluated",
-        "Rplus":  "not evaluated",
-        "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-        "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  812.30026014210557,
-        "AnalyticVehicleCurvatureUtilization":  0.99998680983505184,
-        "DiscreteVehicleCurvatureUtilization":  974.7603121705265,
-        "MaximumDeviationMeters":  "not evaluated",
-        "MaximumDeviationLimitMeters":  "not evaluated",
-        "CollisionAndMapBoundaryPassed":  "not evaluated",
-        "MinimumBodyClearanceMeters":  "not evaluated",
-        "MinimumClearanceReserveMeters":  "not evaluated",
-        "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "MinimumPeakGradientImprovementRatio":  "not evaluated",
-        "PeakGradientImprovementPercent":  "not evaluated",
-        "RawCurvatureVariationCost":  "not evaluated",
-        "CandidateCurvatureVariationCost":  "not evaluated",
-        "MaximumVariationCostRegressionRatio":  "not evaluated",
-        "VariationCostRegressionPercent":  "not evaluated",
-        "AbsolutePathLengthChangeMeters":  "not evaluated",
-        "StrictEvaluatorElapsedMilliseconds":  11.0804,
-        "MeasurementEvaluatorElapsedMilliseconds":  0.0754,
-        "StrictPipelineStatus":  "Unchanged",
-        "MeasurementPipelineStatus":  "Unchanged",
-        "Intervals":  [
-                          {
-                              "IntervalIndex":  0,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.10000075784381711,
-                              "GeometricC0":  0.41666666666666663,
-                              "GeometricC1":  0.20833169450289493,
-                              "GeometricC2":  0.83332234152921,
-                              "GeometricC3":  0.20833333333333331,
-                              "VehicleC0":  0.41666666666666663,
-                              "VehicleC1":  0.20833169450289493,
-                              "VehicleC2":  0.83332234152921,
-                              "VehicleC3":  0.20833333333333331,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  744,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  2.1346517098741323E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  0.20833333333333348,
-                              "AnalyticMaximumCurvaturePerMeter":  0.49456530714619418,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.749528153736446
-                          },
-                          {
-                              "IntervalIndex":  1,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.10000003444667947,
-                              "GeometricC0":  0.20833333333333331,
-                              "GeometricC1":  -0.41666665486791415,
-                              "GeometricC2":  0.20833332153458084,
-                              "GeometricC3":  0,
-                              "VehicleC0":  0.20833333333333331,
-                              "VehicleC1":  -0.41666665486791415,
-                              "VehicleC2":  0.20833332153458084,
-                              "VehicleC3":  0,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  648,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  2.1362552671271875E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  -0.077903907886512636,
-                              "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.74999318728737
-                          }
-                      ]
-    },
-    {
-        "StableKey":  "single-turn/s0/r0/w1/seed1",
-        "CandidateSha256":  "04384f5075c7427cb57d028f225573787e02bb6f5e93883793301255de3e9b44",
-        "StrictCandidateSha256":  "04384f5075c7427cb57d028f225573787e02bb6f5e93883793301255de3e9b44",
-        "MeasurementCandidateSha256":  "04384f5075c7427cb57d028f225573787e02bb6f5e93883793301255de3e9b44",
-        "StrictCandidateContent":  {
-                                       "PreparedSegmentDirection":  0,
-                                       "SegmentIndex":  0,
-                                       "WindowStartArcLengthMeters":  1.8999999999999981,
-                                       "WindowEndArcLengthMeters":  2.0999999999999983,
-                                       "DirectionSign":  1,
-                                       "HardNodes":  [
-                                                         {
-                                                             "X":  2.7076608659177555,
-                                                             "Y":  1.7136150003593424,
-                                                             "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                             "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                             "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                             "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                         },
-                                                         {
-                                                             "X":  2.7764244476704873,
-                                                             "Y":  1.786210614200662,
-                                                             "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                             "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                         },
-                                                         {
-                                                             "X":  2.8436656720787941,
-                                                             "Y":  1.8602282995202657,
-                                                             "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0,
-                                                             "VehicleCurvaturePerMeter":  0
-                                                         }
-                                                     ],
-                                       "CandidatePoints":  [
-                                                               {
-                                                                   "X":  2.7076608659177555,
-                                                                   "Y":  1.7136150003593424,
-                                                                   "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                   "HeadingRadians":  0.79166666666666563,
-                                                                   "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7216585404159463,
-                                                                   "Y":  1.7279002807977135,
-                                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                   "HeadingRadians":  0.799333376544441,
-                                                                   "UnwrappedHeadingRadians":  0.799333376544441,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7355462804756989,
-                                                                   "Y":  1.7422924527498314,
-                                                                   "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                   "HeadingRadians":  0.80733338129847854,
-                                                                   "UnwrappedHeadingRadians":  0.80733338129847854,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7493087549490753,
-                                                                   "Y":  1.7568044286392626,
-                                                                   "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                   "HeadingRadians":  0.81666669189649721,
-                                                                   "UnwrappedHeadingRadians":  0.81666669189649721,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7629303026039733,
-                                                                   "Y":  1.7714487570547326,
-                                                                   "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                   "HeadingRadians":  0.82633333107381179,
-                                                                   "UnwrappedHeadingRadians":  0.82633333107381179,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7764244476704873,
-                                                                   "Y":  1.786210614200662,
-                                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7898568980346585,
-                                                                   "Y":  1.8010284915074986,
-                                                                   "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                   "HeadingRadians":  0.83466666032119752,
-                                                                   "UnwrappedHeadingRadians":  0.83466666032119752,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8032942870914961,
-                                                                   "Y":  1.8158418903997506,
-                                                                   "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                   "HeadingRadians":  0.83333331802263244,
-                                                                   "UnwrappedHeadingRadians":  0.83333331802263244,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8167514159586933,
-                                                                   "Y":  1.830637360076214,
-                                                                   "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                   "HeadingRadians":  0.832333317678167,
-                                                                   "UnwrappedHeadingRadians":  0.832333317678167,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8302134763449427,
-                                                                   "Y":  1.8454283438441279,
-                                                                   "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                   "HeadingRadians":  0.83266665963226649,
-                                                                   "UnwrappedHeadingRadians":  0.83266665963226649,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8436656720787941,
-                                                                   "Y":  1.8602282995202657,
-                                                                   "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               }
-                                                           ]
-                                   },
-        "MeasurementCandidateContent":  {
-                                            "PreparedSegmentDirection":  0,
-                                            "SegmentIndex":  0,
-                                            "WindowStartArcLengthMeters":  1.8999999999999981,
-                                            "WindowEndArcLengthMeters":  2.0999999999999983,
-                                            "DirectionSign":  1,
-                                            "HardNodes":  [
-                                                              {
-                                                                  "X":  2.7076608659177555,
-                                                                  "Y":  1.7136150003593424,
-                                                                  "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                  "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                  "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                  "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                              },
-                                                              {
-                                                                  "X":  2.7764244476704873,
-                                                                  "Y":  1.786210614200662,
-                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                  "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                              },
-                                                              {
-                                                                  "X":  2.8436656720787941,
-                                                                  "Y":  1.8602282995202657,
-                                                                  "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0,
-                                                                  "VehicleCurvaturePerMeter":  0
-                                                              }
-                                                          ],
-                                            "CandidatePoints":  [
-                                                                    {
-                                                                        "X":  2.7076608659177555,
-                                                                        "Y":  1.7136150003593424,
-                                                                        "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                        "HeadingRadians":  0.79166666666666563,
-                                                                        "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7216585404159463,
-                                                                        "Y":  1.7279002807977135,
-                                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                        "HeadingRadians":  0.799333376544441,
-                                                                        "UnwrappedHeadingRadians":  0.799333376544441,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7355462804756989,
-                                                                        "Y":  1.7422924527498314,
-                                                                        "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                        "HeadingRadians":  0.80733338129847854,
-                                                                        "UnwrappedHeadingRadians":  0.80733338129847854,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7493087549490753,
-                                                                        "Y":  1.7568044286392626,
-                                                                        "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                        "HeadingRadians":  0.81666669189649721,
-                                                                        "UnwrappedHeadingRadians":  0.81666669189649721,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7629303026039733,
-                                                                        "Y":  1.7714487570547326,
-                                                                        "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                        "HeadingRadians":  0.82633333107381179,
-                                                                        "UnwrappedHeadingRadians":  0.82633333107381179,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7764244476704873,
-                                                                        "Y":  1.786210614200662,
-                                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7898568980346585,
-                                                                        "Y":  1.8010284915074986,
-                                                                        "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                        "HeadingRadians":  0.83466666032119752,
-                                                                        "UnwrappedHeadingRadians":  0.83466666032119752,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8032942870914961,
-                                                                        "Y":  1.8158418903997506,
-                                                                        "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                        "HeadingRadians":  0.83333331802263244,
-                                                                        "UnwrappedHeadingRadians":  0.83333331802263244,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8167514159586933,
-                                                                        "Y":  1.830637360076214,
-                                                                        "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                        "HeadingRadians":  0.832333317678167,
-                                                                        "UnwrappedHeadingRadians":  0.832333317678167,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8302134763449427,
-                                                                        "Y":  1.8454283438441279,
-                                                                        "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                        "HeadingRadians":  0.83266665963226649,
-                                                                        "UnwrappedHeadingRadians":  0.83266665963226649,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8436656720787941,
-                                                                        "Y":  1.8602282995202657,
-                                                                        "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    }
-                                                                ]
-                                        },
-        "StrictDelayedCGenRangeSnapshot":  "not evaluated",
-        "StrictRecord":  {
-                             "StableKey":  "single-turn/s0/r0/w1/seed1",
-                             "CandidateSha256":  "04384f5075c7427cb57d028f225573787e02bb6f5e93883793301255de3e9b44",
-                             "CandidateContent":  {
-                                                      "PreparedSegmentDirection":  0,
-                                                      "SegmentIndex":  0,
-                                                      "WindowStartArcLengthMeters":  1.8999999999999981,
-                                                      "WindowEndArcLengthMeters":  2.0999999999999983,
-                                                      "DirectionSign":  1,
-                                                      "HardNodes":  [
-                                                                        {
-                                                                            "X":  2.7076608659177555,
-                                                                            "Y":  1.7136150003593424,
-                                                                            "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                            "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                            "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                            "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                        },
-                                                                        {
-                                                                            "X":  2.7764244476704873,
-                                                                            "Y":  1.786210614200662,
-                                                                            "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                            "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                                        },
-                                                                        {
-                                                                            "X":  2.8436656720787941,
-                                                                            "Y":  1.8602282995202657,
-                                                                            "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0,
-                                                                            "VehicleCurvaturePerMeter":  0
-                                                                        }
-                                                                    ],
-                                                      "CandidatePoints":  [
-                                                                              {
-                                                                                  "X":  2.7076608659177555,
-                                                                                  "Y":  1.7136150003593424,
-                                                                                  "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                  "HeadingRadians":  0.79166666666666563,
-                                                                                  "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7216585404159463,
-                                                                                  "Y":  1.7279002807977135,
-                                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                  "HeadingRadians":  0.799333376544441,
-                                                                                  "UnwrappedHeadingRadians":  0.799333376544441,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7355462804756989,
-                                                                                  "Y":  1.7422924527498314,
-                                                                                  "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                  "HeadingRadians":  0.80733338129847854,
-                                                                                  "UnwrappedHeadingRadians":  0.80733338129847854,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7493087549490753,
-                                                                                  "Y":  1.7568044286392626,
-                                                                                  "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                  "HeadingRadians":  0.81666669189649721,
-                                                                                  "UnwrappedHeadingRadians":  0.81666669189649721,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7629303026039733,
-                                                                                  "Y":  1.7714487570547326,
-                                                                                  "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                  "HeadingRadians":  0.82633333107381179,
-                                                                                  "UnwrappedHeadingRadians":  0.82633333107381179,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7764244476704873,
-                                                                                  "Y":  1.786210614200662,
-                                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7898568980346585,
-                                                                                  "Y":  1.8010284915074986,
-                                                                                  "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                                  "HeadingRadians":  0.83466666032119752,
-                                                                                  "UnwrappedHeadingRadians":  0.83466666032119752,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8032942870914961,
-                                                                                  "Y":  1.8158418903997506,
-                                                                                  "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                                  "HeadingRadians":  0.83333331802263244,
-                                                                                  "UnwrappedHeadingRadians":  0.83333331802263244,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8167514159586933,
-                                                                                  "Y":  1.830637360076214,
-                                                                                  "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                                  "HeadingRadians":  0.832333317678167,
-                                                                                  "UnwrappedHeadingRadians":  0.832333317678167,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8302134763449427,
-                                                                                  "Y":  1.8454283438441279,
-                                                                                  "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                                  "HeadingRadians":  0.83266665963226649,
-                                                                                  "UnwrappedHeadingRadians":  0.83266665963226649,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8436656720787941,
-                                                                                  "Y":  1.8602282995202657,
-                                                                                  "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              }
-                                                                          ]
-                                                  },
-                             "RecordKind":  "Canonical",
-                             "CandidateIndex":  1,
-                             "CanonicalCandidateIndex":  1,
-                             "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed1",
-                             "EquivalenceParameters":  [
-                                                           0,
-                                                           0.25,
-                                                           0.5,
-                                                           0.75,
-                                                           1
-                                                       ],
-                             "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                             "EvaluatorExecutedDirectly":  true,
-                             "EvaluatorResult":  "CurvatureExceeded",
-                             "StopGate":  "VehicleCurvature",
-                             "MeasurementTrace":  "not evaluated",
-                             "EvaluatorElapsedMilliseconds":  0.0756,
-                             "RawMinimumCurvature":  "not evaluated",
-                             "RawMaximumCurvature":  "not evaluated",
-                             "CandidateMinimumCurvature":  "not evaluated",
-                             "CandidateMaximumCurvature":  "not evaluated",
-                             "DelayedCGenRangeSnapshot":  "not evaluated",
-                             "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                             "MaximumDeviationLimitMeters":  0.1,
-                             "MinimumPeakGradientImprovementRatio":  0.2,
-                             "MaximumVariationCostRegressionRatio":  0.02,
-                             "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                             "DirectionSign":  1,
-                             "HardNodes":  [
-                                               {
-                                                   "X":  2.7076608659177555,
-                                                   "Y":  1.7136150003593424,
-                                                   "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                   "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                   "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                   "VehicleCurvaturePerMeter":  0.41666666666666663
-                                               },
-                                               {
-                                                   "X":  2.7764244476704873,
-                                                   "Y":  1.786210614200662,
-                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                   "VehicleCurvaturePerMeter":  0.20833333333333331
-                                               },
-                                               {
-                                                   "X":  2.8436656720787941,
-                                                   "Y":  1.8602282995202657,
-                                                   "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0,
-                                                   "VehicleCurvaturePerMeter":  0
-                                               }
-                                           ],
-                             "Intervals":  [
-                                               {
-                                                   "IntervalIndex":  0,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.10000075784382205,
-                                                   "GeometricC0":  0.41666666666666663,
-                                                   "GeometricC1":  0.20833169236808693,
-                                                   "GeometricC2":  0.83332234366393587,
-                                                   "GeometricC3":  0.20833333333333331,
-                                                   "VehicleC0":  0.41666666666666663,
-                                                   "VehicleC1":  0.20833169236808693,
-                                                   "VehicleC2":  0.83332234366393587,
-                                                   "VehicleC3":  0.20833333333333331,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  744,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1293,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1293,
-                                                   "PreOverwritePositionErrorMeters":  1.0674018583076525E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  0.20833333333333359,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.49456530757130723,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.74952821777681
-                                               },
-                                               {
-                                                   "IntervalIndex":  1,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.10000003444655119,
-                                                   "GeometricC0":  0.20833333333333331,
-                                                   "GeometricC1":  -0.41666843873780318,
-                                                   "GeometricC2":  0.20833510540446987,
-                                                   "GeometricC3":  0,
-                                                   "VehicleC0":  0.20833333333333331,
-                                                   "VehicleC1":  -0.41666843873780318,
-                                                   "VehicleC2":  0.20833510540446987,
-                                                   "VehicleC3":  0,
-                                                   "OuterIterationCount":  5,
-                                                   "DampingTrialCount":  5,
-                                                   "IntegrationEvaluationCount":  600,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  955,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.0955,
-                                                   "PreOverwritePositionErrorMeters":  8.8979905795765965E-10,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  -0.0779042631466575,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.750046703389661
-                                               }
-                                           ],
-                             "CandidateDiagnostic":  {
-                                                         "WindowIndex":  1,
-                                                         "SeedIndex":  1,
-                                                         "CombinationAttemptIndex":  5,
-                                                         "SolverInvocationCount":  7,
-                                                         "MaximumPreOverwritePositionErrorMeters":  8.8979905795765965E-10,
-                                                         "MaximumPostOverwritePositionErrorMeters":  0,
-                                                         "MaximumTangentSeamErrorRadians":  0,
-                                                         "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                         "MinimumAdjacentPointDistanceMeters":  0.020000005266658936
-                                                     }
-                         },
-        "MeasurementRecord":  {
-                                  "StableKey":  "single-turn/s0/r0/w1/seed1",
-                                  "CandidateSha256":  "04384f5075c7427cb57d028f225573787e02bb6f5e93883793301255de3e9b44",
-                                  "CandidateContent":  {
-                                                           "PreparedSegmentDirection":  0,
-                                                           "SegmentIndex":  0,
-                                                           "WindowStartArcLengthMeters":  1.8999999999999981,
-                                                           "WindowEndArcLengthMeters":  2.0999999999999983,
-                                                           "DirectionSign":  1,
-                                                           "HardNodes":  [
-                                                                             {
-                                                                                 "X":  2.7076608659177555,
-                                                                                 "Y":  1.7136150003593424,
-                                                                                 "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                 "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                                 "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.7764244476704873,
-                                                                                 "Y":  1.786210614200662,
-                                                                                 "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                                 "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.8436656720787941,
-                                                                                 "Y":  1.8602282995202657,
-                                                                                 "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0,
-                                                                                 "VehicleCurvaturePerMeter":  0
-                                                                             }
-                                                                         ],
-                                                           "CandidatePoints":  [
-                                                                                   {
-                                                                                       "X":  2.7076608659177555,
-                                                                                       "Y":  1.7136150003593424,
-                                                                                       "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                       "HeadingRadians":  0.79166666666666563,
-                                                                                       "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7216585404159463,
-                                                                                       "Y":  1.7279002807977135,
-                                                                                       "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                       "HeadingRadians":  0.799333376544441,
-                                                                                       "UnwrappedHeadingRadians":  0.799333376544441,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7355462804756989,
-                                                                                       "Y":  1.7422924527498314,
-                                                                                       "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                       "HeadingRadians":  0.80733338129847854,
-                                                                                       "UnwrappedHeadingRadians":  0.80733338129847854,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7493087549490753,
-                                                                                       "Y":  1.7568044286392626,
-                                                                                       "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                       "HeadingRadians":  0.81666669189649721,
-                                                                                       "UnwrappedHeadingRadians":  0.81666669189649721,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7629303026039733,
-                                                                                       "Y":  1.7714487570547326,
-                                                                                       "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                       "HeadingRadians":  0.82633333107381179,
-                                                                                       "UnwrappedHeadingRadians":  0.82633333107381179,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7764244476704873,
-                                                                                       "Y":  1.786210614200662,
-                                                                                       "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7898568980346585,
-                                                                                       "Y":  1.8010284915074986,
-                                                                                       "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                                       "HeadingRadians":  0.83466666032119752,
-                                                                                       "UnwrappedHeadingRadians":  0.83466666032119752,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8032942870914961,
-                                                                                       "Y":  1.8158418903997506,
-                                                                                       "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                                       "HeadingRadians":  0.83333331802263244,
-                                                                                       "UnwrappedHeadingRadians":  0.83333331802263244,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8167514159586933,
-                                                                                       "Y":  1.830637360076214,
-                                                                                       "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                                       "HeadingRadians":  0.832333317678167,
-                                                                                       "UnwrappedHeadingRadians":  0.832333317678167,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8302134763449427,
-                                                                                       "Y":  1.8454283438441279,
-                                                                                       "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                                       "HeadingRadians":  0.83266665963226649,
-                                                                                       "UnwrappedHeadingRadians":  0.83266665963226649,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8436656720787941,
-                                                                                       "Y":  1.8602282995202657,
-                                                                                       "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   }
-                                                                               ]
-                                                       },
-                                  "RecordKind":  "Canonical",
-                                  "CandidateIndex":  1,
-                                  "CanonicalCandidateIndex":  1,
-                                  "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed1",
-                                  "EquivalenceParameters":  [
-                                                                0,
-                                                                0.25,
-                                                                0.5,
-                                                                0.75,
-                                                                1
-                                                            ],
-                                  "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                                  "EvaluatorExecutedDirectly":  true,
-                                  "EvaluatorResult":  "CurvatureExceeded",
-                                  "StopGate":  "VehicleCurvature",
-                                  "MeasurementTrace":  {
-                                                           "CandidateIndex":  1,
-                                                           "RawRangeExceeded":  "not evaluated",
-                                                           "CollisionAndMapBoundaryPassed":  "not evaluated",
-                                                           "ExecutedGates":  [
-                                                                                 "InputAnalysis",
-                                                                                 "VehicleCurvature"
-                                                                             ],
-                                                           "PassedGates":  "InputAnalysis",
-                                                           "StopGate":  "VehicleCurvature",
-                                                           "Result":  "CurvatureExceeded",
-                                                           "Reason":  "局部 G2 候选超过车辆曲率上限。",
-                                                           "RawMinimumCurvature":  "not evaluated",
-                                                           "RawMaximumCurvature":  "not evaluated",
-                                                           "CandidateMinimumCurvature":  "not evaluated",
-                                                           "CandidateMaximumCurvature":  "not evaluated",
-                                                           "Eminus":  "not evaluated",
-                                                           "Eplus":  "not evaluated",
-                                                           "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                                           "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  812.236738687796,
-                                                           "MaximumDeviationMeters":  "not evaluated",
-                                                           "MaximumDeviationLimitMeters":  "not evaluated",
-                                                           "MinimumBodyClearanceMeters":  "not evaluated",
-                                                           "MinimumClearanceReserveMeters":  "not evaluated",
-                                                           "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "MinimumPeakGradientImprovementRatio":  "not evaluated",
-                                                           "RawCurvatureVariationCost":  "not evaluated",
-                                                           "CandidateCurvatureVariationCost":  "not evaluated",
-                                                           "MaximumVariationCostRegressionRatio":  "not evaluated",
-                                                           "AbsolutePathLengthChangeMeters":  "not evaluated"
-                                                       },
-                                  "EvaluatorElapsedMilliseconds":  0.0823,
-                                  "RawMinimumCurvature":  "not evaluated",
-                                  "RawMaximumCurvature":  "not evaluated",
-                                  "CandidateMinimumCurvature":  "not evaluated",
-                                  "CandidateMaximumCurvature":  "not evaluated",
-                                  "DelayedCGenRangeSnapshot":  "not evaluated",
-                                  "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                                  "MaximumDeviationLimitMeters":  0.1,
-                                  "MinimumPeakGradientImprovementRatio":  0.2,
-                                  "MaximumVariationCostRegressionRatio":  0.02,
-                                  "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                  "DirectionSign":  1,
-                                  "HardNodes":  [
-                                                    {
-                                                        "X":  2.7076608659177555,
-                                                        "Y":  1.7136150003593424,
-                                                        "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                        "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                        "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                        "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                    },
-                                                    {
-                                                        "X":  2.7764244476704873,
-                                                        "Y":  1.786210614200662,
-                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                        "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                    },
-                                                    {
-                                                        "X":  2.8436656720787941,
-                                                        "Y":  1.8602282995202657,
-                                                        "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0,
-                                                        "VehicleCurvaturePerMeter":  0
-                                                    }
-                                                ],
-                                  "Intervals":  [
-                                                    {
-                                                        "IntervalIndex":  0,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.10000075784382205,
-                                                        "GeometricC0":  0.41666666666666663,
-                                                        "GeometricC1":  0.20833169236808693,
-                                                        "GeometricC2":  0.83332234366393587,
-                                                        "GeometricC3":  0.20833333333333331,
-                                                        "VehicleC0":  0.41666666666666663,
-                                                        "VehicleC1":  0.20833169236808693,
-                                                        "VehicleC2":  0.83332234366393587,
-                                                        "VehicleC3":  0.20833333333333331,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  744,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1752,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1752,
-                                                        "PreOverwritePositionErrorMeters":  1.0674018583076525E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  0.20833333333333359,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.49456530757130723,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.74952821777681
-                                                    },
-                                                    {
-                                                        "IntervalIndex":  1,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.10000003444655119,
-                                                        "GeometricC0":  0.20833333333333331,
-                                                        "GeometricC1":  -0.41666843873780318,
-                                                        "GeometricC2":  0.20833510540446987,
-                                                        "GeometricC3":  0,
-                                                        "VehicleC0":  0.20833333333333331,
-                                                        "VehicleC1":  -0.41666843873780318,
-                                                        "VehicleC2":  0.20833510540446987,
-                                                        "VehicleC3":  0,
-                                                        "OuterIterationCount":  5,
-                                                        "DampingTrialCount":  5,
-                                                        "IntegrationEvaluationCount":  600,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1408,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1408,
-                                                        "PreOverwritePositionErrorMeters":  8.8979905795765965E-10,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  -0.0779042631466575,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.750046703389661
-                                                    }
-                                                ],
-                                  "CandidateDiagnostic":  {
-                                                              "WindowIndex":  1,
-                                                              "SeedIndex":  1,
-                                                              "CombinationAttemptIndex":  5,
-                                                              "SolverInvocationCount":  7,
-                                                              "MaximumPreOverwritePositionErrorMeters":  8.8979905795765965E-10,
-                                                              "MaximumPostOverwritePositionErrorMeters":  0,
-                                                              "MaximumTangentSeamErrorRadians":  0,
-                                                              "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                              "MinimumAdjacentPointDistanceMeters":  0.020000005266658936
-                                                          }
-                              },
-        "SourceIdentity":  {
-                               "SharedBaselineCommit":  "da9157b17a7e4206ed4cdebd1985ee2577fae480",
-                               "FrozenCoreCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "StrictRootCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementRootCommit":  "c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825",
-                               "StrictRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\strict-root-3006aa0",
-                               "MeasurementRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\measurement-root-3006aa0",
-                               "FixtureSha256":  "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
-                               "SharedBaselineEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "StrictEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "MeasurementEvaluatorSha256":  "ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e",
-                               "MeasurementCommitParent":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementCommitChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "MeasurementCommitHistoryProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-commit-history-proof.json",
-                               "MeasurementCommitHistoryProofSha256":  "b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0",
-                               "ProbeSourceManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea",
-                               "AllowedChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "StrictAllowedDifferenceSummary":  "Complete strict-root source diff from the shared baseline.",
-                               "StrictAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictAllowedDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "StrictSharedBaselineDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictSharedBaselineDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "MeasurementAllowedDifferenceSummary":  "Only the measurement evaluator raw-range branch and trace capture differ.",
-                               "MeasurementAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-evaluator.diff",
-                               "MeasurementEvaluatorDiffSha256":  "d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd",
-                               "StrictEvaluatorHashProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-evaluator-hash-proof.json",
-                               "StrictEvaluatorHashProofSha256":  "a0869c7b26f8e203dca2361dd39169d60f0a8fd3c9dd58c9697aed7abd5c94e7",
-                               "FrozenCoreJsonPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core.json",
-                               "FrozenCoreJsonSha256":  "08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa",
-                               "FrozenCoreManifestPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core-probe-source-manifest.txt",
-                               "FrozenCoreManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea"
-                           },
-        "RecordKind":  "Canonical",
-        "CanonicalCandidateIndex":  1,
-        "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed1",
-        "EquivalenceParameters":  [
-                                      0,
-                                      0.25,
-                                      0.5,
-                                      0.75,
-                                      1
-                                  ],
-        "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-        "EvaluatorExecutedDirectly":  true,
-        "StrictResult":  "CurvatureExceeded",
-        "StrictStopGate":  "VehicleCurvature",
-        "MeasurementResult":  "CurvatureExceeded",
-        "MeasurementStopGate":  "VehicleCurvature",
-        "MeasurementReason":  "局部 G2 候选超过车辆曲率上限。",
-        "MeasurementExecutedGates":  [
-                                         "InputAnalysis",
-                                         "VehicleCurvature"
-                                     ],
-        "MeasurementPassedGates":  "InputAnalysis",
-        "RawRangeExceeded":  "not evaluated",
-        "RawMinimumCurvature":  "not evaluated",
-        "RawMaximumCurvature":  "not evaluated",
-        "CandidateMinimumCurvature":  "not evaluated",
-        "CandidateMaximumCurvature":  "not evaluated",
-        "HardPhysicalQualified":  false,
-        "Eminus":  "not evaluated",
-        "Eplus":  "not evaluated",
-        "Rminus":  "not evaluated",
-        "Rplus":  "not evaluated",
-        "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-        "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  812.236738687796,
-        "AnalyticVehicleCurvatureUtilization":  0.99998681239672282,
-        "DiscreteVehicleCurvatureUtilization":  974.684086425355,
-        "MaximumDeviationMeters":  "not evaluated",
-        "MaximumDeviationLimitMeters":  "not evaluated",
-        "CollisionAndMapBoundaryPassed":  "not evaluated",
-        "MinimumBodyClearanceMeters":  "not evaluated",
-        "MinimumClearanceReserveMeters":  "not evaluated",
-        "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "MinimumPeakGradientImprovementRatio":  "not evaluated",
-        "PeakGradientImprovementPercent":  "not evaluated",
-        "RawCurvatureVariationCost":  "not evaluated",
-        "CandidateCurvatureVariationCost":  "not evaluated",
-        "MaximumVariationCostRegressionRatio":  "not evaluated",
-        "VariationCostRegressionPercent":  "not evaluated",
-        "AbsolutePathLengthChangeMeters":  "not evaluated",
-        "StrictEvaluatorElapsedMilliseconds":  0.0756,
-        "MeasurementEvaluatorElapsedMilliseconds":  0.0823,
-        "StrictPipelineStatus":  "Unchanged",
-        "MeasurementPipelineStatus":  "Unchanged",
-        "Intervals":  [
-                          {
-                              "IntervalIndex":  0,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.10000075784382205,
-                              "GeometricC0":  0.41666666666666663,
-                              "GeometricC1":  0.20833169236808693,
-                              "GeometricC2":  0.83332234366393587,
-                              "GeometricC3":  0.20833333333333331,
-                              "VehicleC0":  0.41666666666666663,
-                              "VehicleC1":  0.20833169236808693,
-                              "VehicleC2":  0.83332234366393587,
-                              "VehicleC3":  0.20833333333333331,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  744,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  1.0674018583076525E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  0.20833333333333359,
-                              "AnalyticMaximumCurvaturePerMeter":  0.49456530757130723,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.74952821777681
-                          },
-                          {
-                              "IntervalIndex":  1,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.10000003444655119,
-                              "GeometricC0":  0.20833333333333331,
-                              "GeometricC1":  -0.41666843873780318,
-                              "GeometricC2":  0.20833510540446987,
-                              "GeometricC3":  0,
-                              "VehicleC0":  0.20833333333333331,
-                              "VehicleC1":  -0.41666843873780318,
-                              "VehicleC2":  0.20833510540446987,
-                              "VehicleC3":  0,
-                              "OuterIterationCount":  5,
-                              "DampingTrialCount":  5,
-                              "IntegrationEvaluationCount":  600,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  8.8979905795765965E-10,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  -0.0779042631466575,
-                              "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.750046703389661
-                          }
-                      ]
-    },
-    {
-        "StableKey":  "single-turn/s0/r0/w1/seed2",
-        "CandidateSha256":  "716f4a34522dbd2e054e4d92781b4b51e77b4477146c4801c9520e9ea081da85",
-        "StrictCandidateSha256":  "716f4a34522dbd2e054e4d92781b4b51e77b4477146c4801c9520e9ea081da85",
-        "MeasurementCandidateSha256":  "716f4a34522dbd2e054e4d92781b4b51e77b4477146c4801c9520e9ea081da85",
-        "StrictCandidateContent":  {
-                                       "PreparedSegmentDirection":  0,
-                                       "SegmentIndex":  0,
-                                       "WindowStartArcLengthMeters":  1.8999999999999981,
-                                       "WindowEndArcLengthMeters":  2.0999999999999983,
-                                       "DirectionSign":  1,
-                                       "HardNodes":  [
-                                                         {
-                                                             "X":  2.7076608659177555,
-                                                             "Y":  1.7136150003593424,
-                                                             "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                             "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                             "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                             "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                         },
-                                                         {
-                                                             "X":  2.7764244476704873,
-                                                             "Y":  1.786210614200662,
-                                                             "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                             "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                         },
-                                                         {
-                                                             "X":  2.8436656720787941,
-                                                             "Y":  1.8602282995202657,
-                                                             "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0,
-                                                             "VehicleCurvaturePerMeter":  0
-                                                         }
-                                                     ],
-                                       "CandidatePoints":  [
-                                                               {
-                                                                   "X":  2.7076608659177555,
-                                                                   "Y":  1.7136150003593424,
-                                                                   "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                   "HeadingRadians":  0.79166666666666563,
-                                                                   "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7216585404158562,
-                                                                   "Y":  1.7279002807977986,
-                                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                   "HeadingRadians":  0.79933337656086634,
-                                                                   "UnwrappedHeadingRadians":  0.79933337656086634,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7355462804752078,
-                                                                   "Y":  1.7422924527503,
-                                                                   "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                   "HeadingRadians":  0.8073333813354362,
-                                                                   "UnwrappedHeadingRadians":  0.8073333813354362,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7493087549480162,
-                                                                   "Y":  1.7568044286402671,
-                                                                   "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                   "HeadingRadians":  0.81666669193345531,
-                                                                   "UnwrappedHeadingRadians":  0.81666669193345531,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7629303026025065,
-                                                                   "Y":  1.7714487570561139,
-                                                                   "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                   "HeadingRadians":  0.82633333109023777,
-                                                                   "UnwrappedHeadingRadians":  0.82633333109023777,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7764244476704873,
-                                                                   "Y":  1.786210614200662,
-                                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7898568979962848,
-                                                                   "Y":  1.8010284915423009,
-                                                                   "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                   "HeadingRadians":  0.8346666671903521,
-                                                                   "UnwrappedHeadingRadians":  0.8346666671903521,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8032942868812287,
-                                                                   "Y":  1.8158418905905414,
-                                                                   "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                   "HeadingRadians":  0.83333333347822636,
-                                                                   "UnwrappedHeadingRadians":  0.83333333347822636,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8167514155068547,
-                                                                   "Y":  1.8306373604867596,
-                                                                   "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                   "HeadingRadians":  0.83233333313375946,
-                                                                   "UnwrappedHeadingRadians":  0.83233333313375946,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.830213475721461,
-                                                                   "Y":  1.8454283444109369,
-                                                                   "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                   "HeadingRadians":  0.83266666650141852,
-                                                                   "UnwrappedHeadingRadians":  0.83266666650141852,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8436656720787941,
-                                                                   "Y":  1.8602282995202657,
-                                                                   "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               }
-                                                           ]
-                                   },
-        "MeasurementCandidateContent":  {
-                                            "PreparedSegmentDirection":  0,
-                                            "SegmentIndex":  0,
-                                            "WindowStartArcLengthMeters":  1.8999999999999981,
-                                            "WindowEndArcLengthMeters":  2.0999999999999983,
-                                            "DirectionSign":  1,
-                                            "HardNodes":  [
-                                                              {
-                                                                  "X":  2.7076608659177555,
-                                                                  "Y":  1.7136150003593424,
-                                                                  "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                  "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                  "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                  "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                              },
-                                                              {
-                                                                  "X":  2.7764244476704873,
-                                                                  "Y":  1.786210614200662,
-                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                  "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                              },
-                                                              {
-                                                                  "X":  2.8436656720787941,
-                                                                  "Y":  1.8602282995202657,
-                                                                  "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0,
-                                                                  "VehicleCurvaturePerMeter":  0
-                                                              }
-                                                          ],
-                                            "CandidatePoints":  [
-                                                                    {
-                                                                        "X":  2.7076608659177555,
-                                                                        "Y":  1.7136150003593424,
-                                                                        "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                        "HeadingRadians":  0.79166666666666563,
-                                                                        "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7216585404158562,
-                                                                        "Y":  1.7279002807977986,
-                                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                        "HeadingRadians":  0.79933337656086634,
-                                                                        "UnwrappedHeadingRadians":  0.79933337656086634,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7355462804752078,
-                                                                        "Y":  1.7422924527503,
-                                                                        "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                        "HeadingRadians":  0.8073333813354362,
-                                                                        "UnwrappedHeadingRadians":  0.8073333813354362,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7493087549480162,
-                                                                        "Y":  1.7568044286402671,
-                                                                        "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                        "HeadingRadians":  0.81666669193345531,
-                                                                        "UnwrappedHeadingRadians":  0.81666669193345531,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7629303026025065,
-                                                                        "Y":  1.7714487570561139,
-                                                                        "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                        "HeadingRadians":  0.82633333109023777,
-                                                                        "UnwrappedHeadingRadians":  0.82633333109023777,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7764244476704873,
-                                                                        "Y":  1.786210614200662,
-                                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7898568979962848,
-                                                                        "Y":  1.8010284915423009,
-                                                                        "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                        "HeadingRadians":  0.8346666671903521,
-                                                                        "UnwrappedHeadingRadians":  0.8346666671903521,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8032942868812287,
-                                                                        "Y":  1.8158418905905414,
-                                                                        "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                        "HeadingRadians":  0.83333333347822636,
-                                                                        "UnwrappedHeadingRadians":  0.83333333347822636,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8167514155068547,
-                                                                        "Y":  1.8306373604867596,
-                                                                        "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                        "HeadingRadians":  0.83233333313375946,
-                                                                        "UnwrappedHeadingRadians":  0.83233333313375946,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.830213475721461,
-                                                                        "Y":  1.8454283444109369,
-                                                                        "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                        "HeadingRadians":  0.83266666650141852,
-                                                                        "UnwrappedHeadingRadians":  0.83266666650141852,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8436656720787941,
-                                                                        "Y":  1.8602282995202657,
-                                                                        "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    }
-                                                                ]
-                                        },
-        "StrictDelayedCGenRangeSnapshot":  "not evaluated",
-        "StrictRecord":  {
-                             "StableKey":  "single-turn/s0/r0/w1/seed2",
-                             "CandidateSha256":  "716f4a34522dbd2e054e4d92781b4b51e77b4477146c4801c9520e9ea081da85",
-                             "CandidateContent":  {
-                                                      "PreparedSegmentDirection":  0,
-                                                      "SegmentIndex":  0,
-                                                      "WindowStartArcLengthMeters":  1.8999999999999981,
-                                                      "WindowEndArcLengthMeters":  2.0999999999999983,
-                                                      "DirectionSign":  1,
-                                                      "HardNodes":  [
-                                                                        {
-                                                                            "X":  2.7076608659177555,
-                                                                            "Y":  1.7136150003593424,
-                                                                            "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                            "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                            "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                            "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                        },
-                                                                        {
-                                                                            "X":  2.7764244476704873,
-                                                                            "Y":  1.786210614200662,
-                                                                            "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                            "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                                        },
-                                                                        {
-                                                                            "X":  2.8436656720787941,
-                                                                            "Y":  1.8602282995202657,
-                                                                            "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0,
-                                                                            "VehicleCurvaturePerMeter":  0
-                                                                        }
-                                                                    ],
-                                                      "CandidatePoints":  [
-                                                                              {
-                                                                                  "X":  2.7076608659177555,
-                                                                                  "Y":  1.7136150003593424,
-                                                                                  "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                  "HeadingRadians":  0.79166666666666563,
-                                                                                  "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7216585404158562,
-                                                                                  "Y":  1.7279002807977986,
-                                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                  "HeadingRadians":  0.79933337656086634,
-                                                                                  "UnwrappedHeadingRadians":  0.79933337656086634,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7355462804752078,
-                                                                                  "Y":  1.7422924527503,
-                                                                                  "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                  "HeadingRadians":  0.8073333813354362,
-                                                                                  "UnwrappedHeadingRadians":  0.8073333813354362,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7493087549480162,
-                                                                                  "Y":  1.7568044286402671,
-                                                                                  "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                  "HeadingRadians":  0.81666669193345531,
-                                                                                  "UnwrappedHeadingRadians":  0.81666669193345531,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7629303026025065,
-                                                                                  "Y":  1.7714487570561139,
-                                                                                  "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                  "HeadingRadians":  0.82633333109023777,
-                                                                                  "UnwrappedHeadingRadians":  0.82633333109023777,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7764244476704873,
-                                                                                  "Y":  1.786210614200662,
-                                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7898568979962848,
-                                                                                  "Y":  1.8010284915423009,
-                                                                                  "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                                  "HeadingRadians":  0.8346666671903521,
-                                                                                  "UnwrappedHeadingRadians":  0.8346666671903521,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8032942868812287,
-                                                                                  "Y":  1.8158418905905414,
-                                                                                  "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                                  "HeadingRadians":  0.83333333347822636,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333347822636,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8167514155068547,
-                                                                                  "Y":  1.8306373604867596,
-                                                                                  "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                                  "HeadingRadians":  0.83233333313375946,
-                                                                                  "UnwrappedHeadingRadians":  0.83233333313375946,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.830213475721461,
-                                                                                  "Y":  1.8454283444109369,
-                                                                                  "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                                  "HeadingRadians":  0.83266666650141852,
-                                                                                  "UnwrappedHeadingRadians":  0.83266666650141852,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8436656720787941,
-                                                                                  "Y":  1.8602282995202657,
-                                                                                  "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              }
-                                                                          ]
-                                                  },
-                             "RecordKind":  "Canonical",
-                             "CandidateIndex":  2,
-                             "CanonicalCandidateIndex":  2,
-                             "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed2",
-                             "EquivalenceParameters":  [
-                                                           0,
-                                                           0.25,
-                                                           0.5,
-                                                           0.75,
-                                                           1
-                                                       ],
-                             "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                             "EvaluatorExecutedDirectly":  true,
-                             "EvaluatorResult":  "CurvatureExceeded",
-                             "StopGate":  "VehicleCurvature",
-                             "MeasurementTrace":  "not evaluated",
-                             "EvaluatorElapsedMilliseconds":  0.076,
-                             "RawMinimumCurvature":  "not evaluated",
-                             "RawMaximumCurvature":  "not evaluated",
-                             "CandidateMinimumCurvature":  "not evaluated",
-                             "CandidateMaximumCurvature":  "not evaluated",
-                             "DelayedCGenRangeSnapshot":  "not evaluated",
-                             "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                             "MaximumDeviationLimitMeters":  0.1,
-                             "MinimumPeakGradientImprovementRatio":  0.2,
-                             "MaximumVariationCostRegressionRatio":  0.02,
-                             "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                             "DirectionSign":  1,
-                             "HardNodes":  [
-                                               {
-                                                   "X":  2.7076608659177555,
-                                                   "Y":  1.7136150003593424,
-                                                   "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                   "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                   "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                   "VehicleCurvaturePerMeter":  0.41666666666666663
-                                               },
-                                               {
-                                                   "X":  2.7764244476704873,
-                                                   "Y":  1.786210614200662,
-                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                   "VehicleCurvaturePerMeter":  0.20833333333333331
-                                               },
-                                               {
-                                                   "X":  2.8436656720787941,
-                                                   "Y":  1.8602282995202657,
-                                                   "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0,
-                                                   "VehicleCurvaturePerMeter":  0
-                                               }
-                                           ],
-                             "Intervals":  [
-                                               {
-                                                   "IntervalIndex":  0,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.10000075784381147,
-                                                   "GeometricC0":  0.41666666666666663,
-                                                   "GeometricC1":  0.20833169664566714,
-                                                   "GeometricC2":  0.83332233938653189,
-                                                   "GeometricC3":  0.20833333333333331,
-                                                   "VehicleC0":  0.41666666666666663,
-                                                   "VehicleC1":  0.20833169664566714,
-                                                   "VehicleC2":  0.83332233938653189,
-                                                   "VehicleC3":  0.20833333333333331,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  744,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1220,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.122,
-                                                   "PreOverwritePositionErrorMeters":  3.206163462427291E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  0.20833333333333359,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.4945653067195005,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.749528089457648
-                                               },
-                                               {
-                                                   "IntervalIndex":  1,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.10000003444668006,
-                                                   "GeometricC0":  0.20833333333333331,
-                                                   "GeometricC1":  -0.41666664989654018,
-                                                   "GeometricC2":  0.20833331656320686,
-                                                   "GeometricC3":  0,
-                                                   "VehicleC0":  0.20833333333333331,
-                                                   "VehicleC1":  -0.41666664989654018,
-                                                   "VehicleC2":  0.20833331656320686,
-                                                   "VehicleC3":  0,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  624,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1027,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1027,
-                                                   "PreOverwritePositionErrorMeters":  4.6218404233562664E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  -0.07790390689645843,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.749993038146094
-                                               }
-                                           ],
-                             "CandidateDiagnostic":  {
-                                                         "WindowIndex":  1,
-                                                         "SeedIndex":  2,
-                                                         "CombinationAttemptIndex":  6,
-                                                         "SolverInvocationCount":  9,
-                                                         "MaximumPreOverwritePositionErrorMeters":  4.6218404233562664E-12,
-                                                         "MaximumPostOverwritePositionErrorMeters":  0,
-                                                         "MaximumTangentSeamErrorRadians":  0,
-                                                         "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                         "MinimumAdjacentPointDistanceMeters":  0.020000005266671134
-                                                     }
-                         },
-        "MeasurementRecord":  {
-                                  "StableKey":  "single-turn/s0/r0/w1/seed2",
-                                  "CandidateSha256":  "716f4a34522dbd2e054e4d92781b4b51e77b4477146c4801c9520e9ea081da85",
-                                  "CandidateContent":  {
-                                                           "PreparedSegmentDirection":  0,
-                                                           "SegmentIndex":  0,
-                                                           "WindowStartArcLengthMeters":  1.8999999999999981,
-                                                           "WindowEndArcLengthMeters":  2.0999999999999983,
-                                                           "DirectionSign":  1,
-                                                           "HardNodes":  [
-                                                                             {
-                                                                                 "X":  2.7076608659177555,
-                                                                                 "Y":  1.7136150003593424,
-                                                                                 "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                 "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                                 "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.7764244476704873,
-                                                                                 "Y":  1.786210614200662,
-                                                                                 "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                                                 "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.8436656720787941,
-                                                                                 "Y":  1.8602282995202657,
-                                                                                 "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0,
-                                                                                 "VehicleCurvaturePerMeter":  0
-                                                                             }
-                                                                         ],
-                                                           "CandidatePoints":  [
-                                                                                   {
-                                                                                       "X":  2.7076608659177555,
-                                                                                       "Y":  1.7136150003593424,
-                                                                                       "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                                                       "HeadingRadians":  0.79166666666666563,
-                                                                                       "UnwrappedHeadingRadians":  0.79166666666666563,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7216585404158562,
-                                                                                       "Y":  1.7279002807977986,
-                                                                                       "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                       "HeadingRadians":  0.79933337656086634,
-                                                                                       "UnwrappedHeadingRadians":  0.79933337656086634,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7355462804752078,
-                                                                                       "Y":  1.7422924527503,
-                                                                                       "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                       "HeadingRadians":  0.8073333813354362,
-                                                                                       "UnwrappedHeadingRadians":  0.8073333813354362,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7493087549480162,
-                                                                                       "Y":  1.7568044286402671,
-                                                                                       "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                       "HeadingRadians":  0.81666669193345531,
-                                                                                       "UnwrappedHeadingRadians":  0.81666669193345531,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7629303026025065,
-                                                                                       "Y":  1.7714487570561139,
-                                                                                       "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                       "HeadingRadians":  0.82633333109023777,
-                                                                                       "UnwrappedHeadingRadians":  0.82633333109023777,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7764244476704873,
-                                                                                       "Y":  1.786210614200662,
-                                                                                       "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7898568979962848,
-                                                                                       "Y":  1.8010284915423009,
-                                                                                       "ReferenceArcLengthMeters":  2.0199999999999982,
-                                                                                       "HeadingRadians":  0.8346666671903521,
-                                                                                       "UnwrappedHeadingRadians":  0.8346666671903521,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8032942868812287,
-                                                                                       "Y":  1.8158418905905414,
-                                                                                       "ReferenceArcLengthMeters":  2.0399999999999983,
-                                                                                       "HeadingRadians":  0.83333333347822636,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333347822636,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8167514155068547,
-                                                                                       "Y":  1.8306373604867596,
-                                                                                       "ReferenceArcLengthMeters":  2.0599999999999983,
-                                                                                       "HeadingRadians":  0.83233333313375946,
-                                                                                       "UnwrappedHeadingRadians":  0.83233333313375946,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.830213475721461,
-                                                                                       "Y":  1.8454283444109369,
-                                                                                       "ReferenceArcLengthMeters":  2.0799999999999983,
-                                                                                       "HeadingRadians":  0.83266666650141852,
-                                                                                       "UnwrappedHeadingRadians":  0.83266666650141852,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8436656720787941,
-                                                                                       "Y":  1.8602282995202657,
-                                                                                       "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   }
-                                                                               ]
-                                                       },
-                                  "RecordKind":  "Canonical",
-                                  "CandidateIndex":  2,
-                                  "CanonicalCandidateIndex":  2,
-                                  "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed2",
-                                  "EquivalenceParameters":  [
-                                                                0,
-                                                                0.25,
-                                                                0.5,
-                                                                0.75,
-                                                                1
-                                                            ],
-                                  "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                                  "EvaluatorExecutedDirectly":  true,
-                                  "EvaluatorResult":  "CurvatureExceeded",
-                                  "StopGate":  "VehicleCurvature",
-                                  "MeasurementTrace":  {
-                                                           "CandidateIndex":  2,
-                                                           "RawRangeExceeded":  "not evaluated",
-                                                           "CollisionAndMapBoundaryPassed":  "not evaluated",
-                                                           "ExecutedGates":  [
-                                                                                 "InputAnalysis",
-                                                                                 "VehicleCurvature"
-                                                                             ],
-                                                           "PassedGates":  "InputAnalysis",
-                                                           "StopGate":  "VehicleCurvature",
-                                                           "Result":  "CurvatureExceeded",
-                                                           "Reason":  "局部 G2 候选超过车辆曲率上限。",
-                                                           "RawMinimumCurvature":  "not evaluated",
-                                                           "RawMaximumCurvature":  "not evaluated",
-                                                           "CandidateMinimumCurvature":  "not evaluated",
-                                                           "CandidateMaximumCurvature":  "not evaluated",
-                                                           "Eminus":  "not evaluated",
-                                                           "Eplus":  "not evaluated",
-                                                           "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                                           "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  812.30046889589539,
-                                                           "MaximumDeviationMeters":  "not evaluated",
-                                                           "MaximumDeviationLimitMeters":  "not evaluated",
-                                                           "MinimumBodyClearanceMeters":  "not evaluated",
-                                                           "MinimumClearanceReserveMeters":  "not evaluated",
-                                                           "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "MinimumPeakGradientImprovementRatio":  "not evaluated",
-                                                           "RawCurvatureVariationCost":  "not evaluated",
-                                                           "CandidateCurvatureVariationCost":  "not evaluated",
-                                                           "MaximumVariationCostRegressionRatio":  "not evaluated",
-                                                           "AbsolutePathLengthChangeMeters":  "not evaluated"
-                                                       },
-                                  "EvaluatorElapsedMilliseconds":  0.0659,
-                                  "RawMinimumCurvature":  "not evaluated",
-                                  "RawMaximumCurvature":  "not evaluated",
-                                  "CandidateMinimumCurvature":  "not evaluated",
-                                  "CandidateMaximumCurvature":  "not evaluated",
-                                  "DelayedCGenRangeSnapshot":  "not evaluated",
-                                  "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                                  "MaximumDeviationLimitMeters":  0.1,
-                                  "MinimumPeakGradientImprovementRatio":  0.2,
-                                  "MaximumVariationCostRegressionRatio":  0.02,
-                                  "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                  "DirectionSign":  1,
-                                  "HardNodes":  [
-                                                    {
-                                                        "X":  2.7076608659177555,
-                                                        "Y":  1.7136150003593424,
-                                                        "ReferenceArcLengthMeters":  1.8999999999999981,
-                                                        "TravelUnwrappedHeadingRadians":  0.79166666666666563,
-                                                        "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                        "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                    },
-                                                    {
-                                                        "X":  2.7764244476704873,
-                                                        "Y":  1.786210614200662,
-                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0.20833333333333331,
-                                                        "VehicleCurvaturePerMeter":  0.20833333333333331
-                                                    },
-                                                    {
-                                                        "X":  2.8436656720787941,
-                                                        "Y":  1.8602282995202657,
-                                                        "ReferenceArcLengthMeters":  2.0999999999999983,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0,
-                                                        "VehicleCurvaturePerMeter":  0
-                                                    }
-                                                ],
-                                  "Intervals":  [
-                                                    {
-                                                        "IntervalIndex":  0,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.10000075784381147,
-                                                        "GeometricC0":  0.41666666666666663,
-                                                        "GeometricC1":  0.20833169664566714,
-                                                        "GeometricC2":  0.83332233938653189,
-                                                        "GeometricC3":  0.20833333333333331,
-                                                        "VehicleC0":  0.41666666666666663,
-                                                        "VehicleC1":  0.20833169664566714,
-                                                        "VehicleC2":  0.83332233938653189,
-                                                        "VehicleC3":  0.20833333333333331,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  744,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1616,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1616,
-                                                        "PreOverwritePositionErrorMeters":  3.206163462427291E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  0.20833333333333359,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.4945653067195005,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.749528089457648
-                                                    },
-                                                    {
-                                                        "IntervalIndex":  1,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.10000003444668006,
-                                                        "GeometricC0":  0.20833333333333331,
-                                                        "GeometricC1":  -0.41666664989654018,
-                                                        "GeometricC2":  0.20833331656320686,
-                                                        "GeometricC3":  0,
-                                                        "VehicleC0":  0.20833333333333331,
-                                                        "VehicleC1":  -0.41666664989654018,
-                                                        "VehicleC2":  0.20833331656320686,
-                                                        "VehicleC3":  0,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  624,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1336,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1336,
-                                                        "PreOverwritePositionErrorMeters":  4.6218404233562664E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  -0.07790390689645843,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.749993038146094
-                                                    }
-                                                ],
-                                  "CandidateDiagnostic":  {
-                                                              "WindowIndex":  1,
-                                                              "SeedIndex":  2,
-                                                              "CombinationAttemptIndex":  6,
-                                                              "SolverInvocationCount":  9,
-                                                              "MaximumPreOverwritePositionErrorMeters":  4.6218404233562664E-12,
-                                                              "MaximumPostOverwritePositionErrorMeters":  0,
-                                                              "MaximumTangentSeamErrorRadians":  0,
-                                                              "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                              "MinimumAdjacentPointDistanceMeters":  0.020000005266671134
-                                                          }
-                              },
-        "SourceIdentity":  {
-                               "SharedBaselineCommit":  "da9157b17a7e4206ed4cdebd1985ee2577fae480",
-                               "FrozenCoreCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "StrictRootCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementRootCommit":  "c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825",
-                               "StrictRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\strict-root-3006aa0",
-                               "MeasurementRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\measurement-root-3006aa0",
-                               "FixtureSha256":  "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
-                               "SharedBaselineEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "StrictEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "MeasurementEvaluatorSha256":  "ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e",
-                               "MeasurementCommitParent":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementCommitChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "MeasurementCommitHistoryProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-commit-history-proof.json",
-                               "MeasurementCommitHistoryProofSha256":  "b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0",
-                               "ProbeSourceManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea",
-                               "AllowedChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "StrictAllowedDifferenceSummary":  "Complete strict-root source diff from the shared baseline.",
-                               "StrictAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictAllowedDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "StrictSharedBaselineDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictSharedBaselineDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "MeasurementAllowedDifferenceSummary":  "Only the measurement evaluator raw-range branch and trace capture differ.",
-                               "MeasurementAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-evaluator.diff",
-                               "MeasurementEvaluatorDiffSha256":  "d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd",
-                               "StrictEvaluatorHashProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-evaluator-hash-proof.json",
-                               "StrictEvaluatorHashProofSha256":  "a0869c7b26f8e203dca2361dd39169d60f0a8fd3c9dd58c9697aed7abd5c94e7",
-                               "FrozenCoreJsonPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core.json",
-                               "FrozenCoreJsonSha256":  "08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa",
-                               "FrozenCoreManifestPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core-probe-source-manifest.txt",
-                               "FrozenCoreManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea"
-                           },
-        "RecordKind":  "Canonical",
-        "CanonicalCandidateIndex":  2,
-        "EvaluationSourceStableKey":  "single-turn/s0/r0/w1/seed2",
-        "EquivalenceParameters":  [
-                                      0,
-                                      0.25,
-                                      0.5,
-                                      0.75,
-                                      1
-                                  ],
-        "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-        "EvaluatorExecutedDirectly":  true,
-        "StrictResult":  "CurvatureExceeded",
-        "StrictStopGate":  "VehicleCurvature",
-        "MeasurementResult":  "CurvatureExceeded",
-        "MeasurementStopGate":  "VehicleCurvature",
-        "MeasurementReason":  "局部 G2 候选超过车辆曲率上限。",
-        "MeasurementExecutedGates":  [
-                                         "InputAnalysis",
-                                         "VehicleCurvature"
-                                     ],
-        "MeasurementPassedGates":  "InputAnalysis",
-        "RawRangeExceeded":  "not evaluated",
-        "RawMinimumCurvature":  "not evaluated",
-        "RawMaximumCurvature":  "not evaluated",
-        "CandidateMinimumCurvature":  "not evaluated",
-        "CandidateMaximumCurvature":  "not evaluated",
-        "HardPhysicalQualified":  false,
-        "Eminus":  "not evaluated",
-        "Eplus":  "not evaluated",
-        "Rminus":  "not evaluated",
-        "Rplus":  "not evaluated",
-        "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-        "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  812.30046889589539,
-        "AnalyticVehicleCurvatureUtilization":  0.999986807263838,
-        "DiscreteVehicleCurvatureUtilization":  974.76056267507431,
-        "MaximumDeviationMeters":  "not evaluated",
-        "MaximumDeviationLimitMeters":  "not evaluated",
-        "CollisionAndMapBoundaryPassed":  "not evaluated",
-        "MinimumBodyClearanceMeters":  "not evaluated",
-        "MinimumClearanceReserveMeters":  "not evaluated",
-        "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "MinimumPeakGradientImprovementRatio":  "not evaluated",
-        "PeakGradientImprovementPercent":  "not evaluated",
-        "RawCurvatureVariationCost":  "not evaluated",
-        "CandidateCurvatureVariationCost":  "not evaluated",
-        "MaximumVariationCostRegressionRatio":  "not evaluated",
-        "VariationCostRegressionPercent":  "not evaluated",
-        "AbsolutePathLengthChangeMeters":  "not evaluated",
-        "StrictEvaluatorElapsedMilliseconds":  0.076,
-        "MeasurementEvaluatorElapsedMilliseconds":  0.0659,
-        "StrictPipelineStatus":  "Unchanged",
-        "MeasurementPipelineStatus":  "Unchanged",
-        "Intervals":  [
-                          {
-                              "IntervalIndex":  0,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.10000075784381147,
-                              "GeometricC0":  0.41666666666666663,
-                              "GeometricC1":  0.20833169664566714,
-                              "GeometricC2":  0.83332233938653189,
-                              "GeometricC3":  0.20833333333333331,
-                              "VehicleC0":  0.41666666666666663,
-                              "VehicleC1":  0.20833169664566714,
-                              "VehicleC2":  0.83332233938653189,
-                              "VehicleC3":  0.20833333333333331,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  744,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  3.206163462427291E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  0.20833333333333359,
-                              "AnalyticMaximumCurvaturePerMeter":  0.4945653067195005,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.749528089457648
-                          },
-                          {
-                              "IntervalIndex":  1,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.10000003444668006,
-                              "GeometricC0":  0.20833333333333331,
-                              "GeometricC1":  -0.41666664989654018,
-                              "GeometricC2":  0.20833331656320686,
-                              "GeometricC3":  0,
-                              "VehicleC0":  0.20833333333333331,
-                              "VehicleC1":  -0.41666664989654018,
-                              "VehicleC2":  0.20833331656320686,
-                              "VehicleC3":  0,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  624,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  4.6218404233562664E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  -0.07790390689645843,
-                              "AnalyticMaximumCurvaturePerMeter":  0.20833333333333331,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.749993038146094
-                          }
-                      ]
-    },
-    {
-        "StableKey":  "single-turn/s0/r0/w5/seed0",
-        "CandidateSha256":  "304ca1bd7d4ddf1d75b2608efa83dd3c5244da5179bf151410c2cbf2c2a67fd6",
-        "StrictCandidateSha256":  "304ca1bd7d4ddf1d75b2608efa83dd3c5244da5179bf151410c2cbf2c2a67fd6",
-        "MeasurementCandidateSha256":  "304ca1bd7d4ddf1d75b2608efa83dd3c5244da5179bf151410c2cbf2c2a67fd6",
-        "StrictCandidateContent":  {
-                                       "PreparedSegmentDirection":  0,
-                                       "SegmentIndex":  0,
-                                       "WindowStartArcLengthMeters":  1.9199999999999982,
-                                       "WindowEndArcLengthMeters":  2.1199999999999983,
-                                       "DirectionSign":  1,
-                                       "HardNodes":  [
-                                                         {
-                                                             "X":  2.7216397035494579,
-                                                             "Y":  1.7279184433567971,
-                                                             "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                             "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                             "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                         },
-                                                         {
-                                                             "X":  2.7764244476704873,
-                                                             "Y":  1.786210614200662,
-                                                             "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                             "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                         },
-                                                         {
-                                                             "X":  2.8571139169604551,
-                                                             "Y":  1.8750318365841865,
-                                                             "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0,
-                                                             "VehicleCurvaturePerMeter":  0
-                                                         }
-                                                     ],
-                                       "CandidatePoints":  [
-                                                               {
-                                                                   "X":  2.7216397035494579,
-                                                                   "Y":  1.7279184433567971,
-                                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                   "HeadingRadians":  0.799999999999999,
-                                                                   "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.735518820517751,
-                                                                   "Y":  1.742318704119348,
-                                                                   "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                   "HeadingRadians":  0.80751185019762106,
-                                                                   "UnwrappedHeadingRadians":  0.80751185019762106,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7492871654436315,
-                                                                   "Y":  1.7568248978037275,
-                                                                   "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                   "HeadingRadians":  0.81576181249913349,
-                                                                   "UnwrappedHeadingRadians":  0.81576181249913349,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7629234692767439,
-                                                                   "Y":  1.7714552535762529,
-                                                                   "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                   "HeadingRadians":  0.82542852160468672,
-                                                                   "UnwrappedHeadingRadians":  0.82542852160468672,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7764244476704873,
-                                                                   "Y":  1.786210614200662,
-                                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7925350453990685,
-                                                                   "Y":  1.803999635249512,
-                                                                   "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                   "HeadingRadians":  0.83525333479661956,
-                                                                   "UnwrappedHeadingRadians":  0.83525333479661956,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8086541800369123,
-                                                                   "Y":  1.8217809211925349,
-                                                                   "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                   "HeadingRadians":  0.83333333354000838,
-                                                                   "UnwrappedHeadingRadians":  0.83333333354000838,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8248074262449872,
-                                                                   "Y":  1.8395312269494442,
-                                                                   "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                   "HeadingRadians":  0.831893332511435,
-                                                                   "UnwrappedHeadingRadians":  0.831893332511435,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8409691920180231,
-                                                                   "Y":  1.8572737784938431,
-                                                                   "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                   "HeadingRadians":  0.83237333273947278,
-                                                                   "UnwrappedHeadingRadians":  0.83237333273947278,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8571139169604551,
-                                                                   "Y":  1.8750318365841865,
-                                                                   "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               }
-                                                           ]
-                                   },
-        "MeasurementCandidateContent":  {
-                                            "PreparedSegmentDirection":  0,
-                                            "SegmentIndex":  0,
-                                            "WindowStartArcLengthMeters":  1.9199999999999982,
-                                            "WindowEndArcLengthMeters":  2.1199999999999983,
-                                            "DirectionSign":  1,
-                                            "HardNodes":  [
-                                                              {
-                                                                  "X":  2.7216397035494579,
-                                                                  "Y":  1.7279184433567971,
-                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                  "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                  "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                              },
-                                                              {
-                                                                  "X":  2.7764244476704873,
-                                                                  "Y":  1.786210614200662,
-                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                  "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                              },
-                                                              {
-                                                                  "X":  2.8571139169604551,
-                                                                  "Y":  1.8750318365841865,
-                                                                  "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0,
-                                                                  "VehicleCurvaturePerMeter":  0
-                                                              }
-                                                          ],
-                                            "CandidatePoints":  [
-                                                                    {
-                                                                        "X":  2.7216397035494579,
-                                                                        "Y":  1.7279184433567971,
-                                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                        "HeadingRadians":  0.799999999999999,
-                                                                        "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.735518820517751,
-                                                                        "Y":  1.742318704119348,
-                                                                        "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                        "HeadingRadians":  0.80751185019762106,
-                                                                        "UnwrappedHeadingRadians":  0.80751185019762106,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7492871654436315,
-                                                                        "Y":  1.7568248978037275,
-                                                                        "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                        "HeadingRadians":  0.81576181249913349,
-                                                                        "UnwrappedHeadingRadians":  0.81576181249913349,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7629234692767439,
-                                                                        "Y":  1.7714552535762529,
-                                                                        "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                        "HeadingRadians":  0.82542852160468672,
-                                                                        "UnwrappedHeadingRadians":  0.82542852160468672,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7764244476704873,
-                                                                        "Y":  1.786210614200662,
-                                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7925350453990685,
-                                                                        "Y":  1.803999635249512,
-                                                                        "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                        "HeadingRadians":  0.83525333479661956,
-                                                                        "UnwrappedHeadingRadians":  0.83525333479661956,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8086541800369123,
-                                                                        "Y":  1.8217809211925349,
-                                                                        "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                        "HeadingRadians":  0.83333333354000838,
-                                                                        "UnwrappedHeadingRadians":  0.83333333354000838,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8248074262449872,
-                                                                        "Y":  1.8395312269494442,
-                                                                        "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                        "HeadingRadians":  0.831893332511435,
-                                                                        "UnwrappedHeadingRadians":  0.831893332511435,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8409691920180231,
-                                                                        "Y":  1.8572737784938431,
-                                                                        "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                        "HeadingRadians":  0.83237333273947278,
-                                                                        "UnwrappedHeadingRadians":  0.83237333273947278,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8571139169604551,
-                                                                        "Y":  1.8750318365841865,
-                                                                        "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    }
-                                                                ]
-                                        },
-        "StrictDelayedCGenRangeSnapshot":  "not evaluated",
-        "StrictRecord":  {
-                             "StableKey":  "single-turn/s0/r0/w5/seed0",
-                             "CandidateSha256":  "304ca1bd7d4ddf1d75b2608efa83dd3c5244da5179bf151410c2cbf2c2a67fd6",
-                             "CandidateContent":  {
-                                                      "PreparedSegmentDirection":  0,
-                                                      "SegmentIndex":  0,
-                                                      "WindowStartArcLengthMeters":  1.9199999999999982,
-                                                      "WindowEndArcLengthMeters":  2.1199999999999983,
-                                                      "DirectionSign":  1,
-                                                      "HardNodes":  [
-                                                                        {
-                                                                            "X":  2.7216397035494579,
-                                                                            "Y":  1.7279184433567971,
-                                                                            "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                            "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                            "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                        },
-                                                                        {
-                                                                            "X":  2.7764244476704873,
-                                                                            "Y":  1.786210614200662,
-                                                                            "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                            "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                                        },
-                                                                        {
-                                                                            "X":  2.8571139169604551,
-                                                                            "Y":  1.8750318365841865,
-                                                                            "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0,
-                                                                            "VehicleCurvaturePerMeter":  0
-                                                                        }
-                                                                    ],
-                                                      "CandidatePoints":  [
-                                                                              {
-                                                                                  "X":  2.7216397035494579,
-                                                                                  "Y":  1.7279184433567971,
-                                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                  "HeadingRadians":  0.799999999999999,
-                                                                                  "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.735518820517751,
-                                                                                  "Y":  1.742318704119348,
-                                                                                  "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                  "HeadingRadians":  0.80751185019762106,
-                                                                                  "UnwrappedHeadingRadians":  0.80751185019762106,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7492871654436315,
-                                                                                  "Y":  1.7568248978037275,
-                                                                                  "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                  "HeadingRadians":  0.81576181249913349,
-                                                                                  "UnwrappedHeadingRadians":  0.81576181249913349,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7629234692767439,
-                                                                                  "Y":  1.7714552535762529,
-                                                                                  "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                  "HeadingRadians":  0.82542852160468672,
-                                                                                  "UnwrappedHeadingRadians":  0.82542852160468672,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7764244476704873,
-                                                                                  "Y":  1.786210614200662,
-                                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7925350453990685,
-                                                                                  "Y":  1.803999635249512,
-                                                                                  "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                                  "HeadingRadians":  0.83525333479661956,
-                                                                                  "UnwrappedHeadingRadians":  0.83525333479661956,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8086541800369123,
-                                                                                  "Y":  1.8217809211925349,
-                                                                                  "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                                  "HeadingRadians":  0.83333333354000838,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333354000838,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8248074262449872,
-                                                                                  "Y":  1.8395312269494442,
-                                                                                  "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                                  "HeadingRadians":  0.831893332511435,
-                                                                                  "UnwrappedHeadingRadians":  0.831893332511435,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8409691920180231,
-                                                                                  "Y":  1.8572737784938431,
-                                                                                  "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                                  "HeadingRadians":  0.83237333273947278,
-                                                                                  "UnwrappedHeadingRadians":  0.83237333273947278,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8571139169604551,
-                                                                                  "Y":  1.8750318365841865,
-                                                                                  "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              }
-                                                                          ]
-                                                  },
-                             "RecordKind":  "Canonical",
-                             "CandidateIndex":  3,
-                             "CanonicalCandidateIndex":  3,
-                             "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed0",
-                             "EquivalenceParameters":  [
-                                                           0,
-                                                           0.25,
-                                                           0.5,
-                                                           0.75,
-                                                           1
-                                                       ],
-                             "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                             "EvaluatorExecutedDirectly":  true,
-                             "EvaluatorResult":  "CurvatureOvershoot",
-                             "StopGate":  "RawCurvatureRange",
-                             "MeasurementTrace":  "not evaluated",
-                             "EvaluatorElapsedMilliseconds":  0.7533,
-                             "RawMinimumCurvature":  8.21565038222615E-14,
-                             "RawMaximumCurvature":  0.41667044179866869,
-                             "CandidateMinimumCurvature":  -0.035866224026958556,
-                             "CandidateMaximumCurvature":  0.41190753085946763,
-                             "DelayedCGenRangeSnapshot":  "not evaluated",
-                             "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                             "MaximumDeviationLimitMeters":  0.1,
-                             "MinimumPeakGradientImprovementRatio":  0.2,
-                             "MaximumVariationCostRegressionRatio":  0.02,
-                             "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                             "DirectionSign":  1,
-                             "HardNodes":  [
-                                               {
-                                                   "X":  2.7216397035494579,
-                                                   "Y":  1.7279184433567971,
-                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                   "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                   "VehicleCurvaturePerMeter":  0.41666666666666663
-                                               },
-                                               {
-                                                   "X":  2.7764244476704873,
-                                                   "Y":  1.786210614200662,
-                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                   "VehicleCurvaturePerMeter":  0.24999999999999997
-                                               },
-                                               {
-                                                   "X":  2.8571139169604551,
-                                                   "Y":  1.8750318365841865,
-                                                   "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0,
-                                                   "VehicleCurvaturePerMeter":  0
-                                               }
-                                           ],
-                             "Intervals":  [
-                                               {
-                                                   "IntervalIndex":  0,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.079999924153614538,
-                                                   "GeometricC0":  0.41666666666666663,
-                                                   "GeometricC1":  0.18490900890188469,
-                                                   "GeometricC2":  0.81509257123264345,
-                                                   "GeometricC3":  0.24999999999999997,
-                                                   "VehicleC0":  0.41666666666666663,
-                                                   "VehicleC1":  0.18490900890188469,
-                                                   "VehicleC2":  0.81509257123264345,
-                                                   "VehicleC3":  0.24999999999999997,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  744,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1193,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1193,
-                                                   "PreOverwritePositionErrorMeters":  7.5072718751555112E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  0.25,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.49287523731059291,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  21.190991511975508
-                                               },
-                                               {
-                                                   "IntervalIndex":  1,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.12000008571444681,
-                                                   "GeometricC0":  0.24999999999999997,
-                                                   "GeometricC1":  -0.49999998006598478,
-                                                   "GeometricC2":  0.24999998006598481,
-                                                   "GeometricC3":  0,
-                                                   "VehicleC0":  0.24999999999999997,
-                                                   "VehicleC1":  -0.49999998006598478,
-                                                   "VehicleC2":  0.24999998006598481,
-                                                   "VehicleC3":  0,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  648,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1075,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1075,
-                                                   "PreOverwritePositionErrorMeters":  8.66606526828704E-13,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  -0.093484688313616032,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.749986108777229
-                                               }
-                                           ],
-                             "CandidateDiagnostic":  {
-                                                         "WindowIndex":  5,
-                                                         "SeedIndex":  0,
-                                                         "CombinationAttemptIndex":  16,
-                                                         "SolverInvocationCount":  20,
-                                                         "MaximumPreOverwritePositionErrorMeters":  7.5072718751555112E-12,
-                                                         "MaximumPostOverwritePositionErrorMeters":  0,
-                                                         "MaximumTangentSeamErrorRadians":  0,
-                                                         "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                         "MinimumAdjacentPointDistanceMeters":  0.019999902306251972
-                                                     }
-                         },
-        "MeasurementRecord":  {
-                                  "StableKey":  "single-turn/s0/r0/w5/seed0",
-                                  "CandidateSha256":  "304ca1bd7d4ddf1d75b2608efa83dd3c5244da5179bf151410c2cbf2c2a67fd6",
-                                  "CandidateContent":  {
-                                                           "PreparedSegmentDirection":  0,
-                                                           "SegmentIndex":  0,
-                                                           "WindowStartArcLengthMeters":  1.9199999999999982,
-                                                           "WindowEndArcLengthMeters":  2.1199999999999983,
-                                                           "DirectionSign":  1,
-                                                           "HardNodes":  [
-                                                                             {
-                                                                                 "X":  2.7216397035494579,
-                                                                                 "Y":  1.7279184433567971,
-                                                                                 "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                                 "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                                 "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.7764244476704873,
-                                                                                 "Y":  1.786210614200662,
-                                                                                 "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                                 "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.8571139169604551,
-                                                                                 "Y":  1.8750318365841865,
-                                                                                 "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0,
-                                                                                 "VehicleCurvaturePerMeter":  0
-                                                                             }
-                                                                         ],
-                                                           "CandidatePoints":  [
-                                                                                   {
-                                                                                       "X":  2.7216397035494579,
-                                                                                       "Y":  1.7279184433567971,
-                                                                                       "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                       "HeadingRadians":  0.799999999999999,
-                                                                                       "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.735518820517751,
-                                                                                       "Y":  1.742318704119348,
-                                                                                       "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                       "HeadingRadians":  0.80751185019762106,
-                                                                                       "UnwrappedHeadingRadians":  0.80751185019762106,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7492871654436315,
-                                                                                       "Y":  1.7568248978037275,
-                                                                                       "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                       "HeadingRadians":  0.81576181249913349,
-                                                                                       "UnwrappedHeadingRadians":  0.81576181249913349,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7629234692767439,
-                                                                                       "Y":  1.7714552535762529,
-                                                                                       "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                       "HeadingRadians":  0.82542852160468672,
-                                                                                       "UnwrappedHeadingRadians":  0.82542852160468672,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7764244476704873,
-                                                                                       "Y":  1.786210614200662,
-                                                                                       "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7925350453990685,
-                                                                                       "Y":  1.803999635249512,
-                                                                                       "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                                       "HeadingRadians":  0.83525333479661956,
-                                                                                       "UnwrappedHeadingRadians":  0.83525333479661956,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8086541800369123,
-                                                                                       "Y":  1.8217809211925349,
-                                                                                       "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                                       "HeadingRadians":  0.83333333354000838,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333354000838,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8248074262449872,
-                                                                                       "Y":  1.8395312269494442,
-                                                                                       "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                                       "HeadingRadians":  0.831893332511435,
-                                                                                       "UnwrappedHeadingRadians":  0.831893332511435,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8409691920180231,
-                                                                                       "Y":  1.8572737784938431,
-                                                                                       "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                                       "HeadingRadians":  0.83237333273947278,
-                                                                                       "UnwrappedHeadingRadians":  0.83237333273947278,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8571139169604551,
-                                                                                       "Y":  1.8750318365841865,
-                                                                                       "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   }
-                                                                               ]
-                                                       },
-                                  "RecordKind":  "Canonical",
-                                  "CandidateIndex":  3,
-                                  "CanonicalCandidateIndex":  3,
-                                  "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed0",
-                                  "EquivalenceParameters":  [
-                                                                0,
-                                                                0.25,
-                                                                0.5,
-                                                                0.75,
-                                                                1
-                                                            ],
-                                  "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                                  "EvaluatorExecutedDirectly":  true,
-                                  "EvaluatorResult":  "InsufficientClearance",
-                                  "StopGate":  "Clearance",
-                                  "MeasurementTrace":  {
-                                                           "CandidateIndex":  3,
-                                                           "RawRangeExceeded":  true,
-                                                           "CollisionAndMapBoundaryPassed":  true,
-                                                           "ExecutedGates":  [
-                                                                                 "InputAnalysis",
-                                                                                 "VehicleCurvature",
-                                                                                 "RawCurvatureRange",
-                                                                                 "Deviation",
-                                                                                 "FullPathAnalysis",
-                                                                                 "CollisionAndMapBoundary",
-                                                                                 "Clearance"
-                                                                             ],
-                                                           "PassedGates":  [
-                                                                               "InputAnalysis",
-                                                                               "VehicleCurvature",
-                                                                               "Deviation",
-                                                                               "FullPathAnalysis",
-                                                                               "CollisionAndMapBoundary"
-                                                                           ],
-                                                           "StopGate":  "Clearance",
-                                                           "Result":  "InsufficientClearance",
-                                                           "Reason":  "局部 G2 候选净空不足。",
-                                                           "RawMinimumCurvature":  8.21565038222615E-14,
-                                                           "RawMaximumCurvature":  0.41667044179866869,
-                                                           "CandidateMinimumCurvature":  -0.035866224026958556,
-                                                           "CandidateMaximumCurvature":  0.41190753085946763,
-                                                           "Eminus":  0.035866224027040712,
-                                                           "Eplus":  0,
-                                                           "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                                           "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  0.41190753085946763,
-                                                           "MaximumDeviationMeters":  6.2208064797947889E-05,
-                                                           "MaximumDeviationLimitMeters":  0.1,
-                                                           "MinimumBodyClearanceMeters":  "not evaluated",
-                                                           "MinimumClearanceReserveMeters":  0.02,
-                                                           "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "MinimumPeakGradientImprovementRatio":  "not evaluated",
-                                                           "RawCurvatureVariationCost":  "not evaluated",
-                                                           "CandidateCurvatureVariationCost":  "not evaluated",
-                                                           "MaximumVariationCostRegressionRatio":  "not evaluated",
-                                                           "AbsolutePathLengthChangeMeters":  "not evaluated"
-                                                       },
-                                  "EvaluatorElapsedMilliseconds":  1.6731,
-                                  "RawMinimumCurvature":  8.21565038222615E-14,
-                                  "RawMaximumCurvature":  0.41667044179866869,
-                                  "CandidateMinimumCurvature":  -0.035866224026958556,
-                                  "CandidateMaximumCurvature":  0.41190753085946763,
-                                  "DelayedCGenRangeSnapshot":  "not evaluated",
-                                  "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                                  "MaximumDeviationLimitMeters":  0.1,
-                                  "MinimumPeakGradientImprovementRatio":  0.2,
-                                  "MaximumVariationCostRegressionRatio":  0.02,
-                                  "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                  "DirectionSign":  1,
-                                  "HardNodes":  [
-                                                    {
-                                                        "X":  2.7216397035494579,
-                                                        "Y":  1.7279184433567971,
-                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                        "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                        "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                    },
-                                                    {
-                                                        "X":  2.7764244476704873,
-                                                        "Y":  1.786210614200662,
-                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                        "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                    },
-                                                    {
-                                                        "X":  2.8571139169604551,
-                                                        "Y":  1.8750318365841865,
-                                                        "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0,
-                                                        "VehicleCurvaturePerMeter":  0
-                                                    }
-                                                ],
-                                  "Intervals":  [
-                                                    {
-                                                        "IntervalIndex":  0,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.079999924153614538,
-                                                        "GeometricC0":  0.41666666666666663,
-                                                        "GeometricC1":  0.18490900890188469,
-                                                        "GeometricC2":  0.81509257123264345,
-                                                        "GeometricC3":  0.24999999999999997,
-                                                        "VehicleC0":  0.41666666666666663,
-                                                        "VehicleC1":  0.18490900890188469,
-                                                        "VehicleC2":  0.81509257123264345,
-                                                        "VehicleC3":  0.24999999999999997,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  744,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1686,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1686,
-                                                        "PreOverwritePositionErrorMeters":  7.5072718751555112E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  0.25,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.49287523731059291,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  21.190991511975508
-                                                    },
-                                                    {
-                                                        "IntervalIndex":  1,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.12000008571444681,
-                                                        "GeometricC0":  0.24999999999999997,
-                                                        "GeometricC1":  -0.49999998006598478,
-                                                        "GeometricC2":  0.24999998006598481,
-                                                        "GeometricC3":  0,
-                                                        "VehicleC0":  0.24999999999999997,
-                                                        "VehicleC1":  -0.49999998006598478,
-                                                        "VehicleC2":  0.24999998006598481,
-                                                        "VehicleC3":  0,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  648,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1470,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.147,
-                                                        "PreOverwritePositionErrorMeters":  8.66606526828704E-13,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  -0.093484688313616032,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.749986108777229
-                                                    }
-                                                ],
-                                  "CandidateDiagnostic":  {
-                                                              "WindowIndex":  5,
-                                                              "SeedIndex":  0,
-                                                              "CombinationAttemptIndex":  16,
-                                                              "SolverInvocationCount":  20,
-                                                              "MaximumPreOverwritePositionErrorMeters":  7.5072718751555112E-12,
-                                                              "MaximumPostOverwritePositionErrorMeters":  0,
-                                                              "MaximumTangentSeamErrorRadians":  0,
-                                                              "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                              "MinimumAdjacentPointDistanceMeters":  0.019999902306251972
-                                                          }
-                              },
-        "SourceIdentity":  {
-                               "SharedBaselineCommit":  "da9157b17a7e4206ed4cdebd1985ee2577fae480",
-                               "FrozenCoreCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "StrictRootCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementRootCommit":  "c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825",
-                               "StrictRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\strict-root-3006aa0",
-                               "MeasurementRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\measurement-root-3006aa0",
-                               "FixtureSha256":  "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
-                               "SharedBaselineEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "StrictEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "MeasurementEvaluatorSha256":  "ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e",
-                               "MeasurementCommitParent":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementCommitChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "MeasurementCommitHistoryProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-commit-history-proof.json",
-                               "MeasurementCommitHistoryProofSha256":  "b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0",
-                               "ProbeSourceManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea",
-                               "AllowedChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "StrictAllowedDifferenceSummary":  "Complete strict-root source diff from the shared baseline.",
-                               "StrictAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictAllowedDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "StrictSharedBaselineDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictSharedBaselineDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "MeasurementAllowedDifferenceSummary":  "Only the measurement evaluator raw-range branch and trace capture differ.",
-                               "MeasurementAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-evaluator.diff",
-                               "MeasurementEvaluatorDiffSha256":  "d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd",
-                               "StrictEvaluatorHashProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-evaluator-hash-proof.json",
-                               "StrictEvaluatorHashProofSha256":  "a0869c7b26f8e203dca2361dd39169d60f0a8fd3c9dd58c9697aed7abd5c94e7",
-                               "FrozenCoreJsonPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core.json",
-                               "FrozenCoreJsonSha256":  "08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa",
-                               "FrozenCoreManifestPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core-probe-source-manifest.txt",
-                               "FrozenCoreManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea"
-                           },
-        "RecordKind":  "Canonical",
-        "CanonicalCandidateIndex":  3,
-        "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed0",
-        "EquivalenceParameters":  [
-                                      0,
-                                      0.25,
-                                      0.5,
-                                      0.75,
-                                      1
-                                  ],
-        "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-        "EvaluatorExecutedDirectly":  true,
-        "StrictResult":  "CurvatureOvershoot",
-        "StrictStopGate":  "RawCurvatureRange",
-        "MeasurementResult":  "InsufficientClearance",
-        "MeasurementStopGate":  "Clearance",
-        "MeasurementReason":  "局部 G2 候选净空不足。",
-        "MeasurementExecutedGates":  [
-                                         "InputAnalysis",
-                                         "VehicleCurvature",
-                                         "RawCurvatureRange",
-                                         "Deviation",
-                                         "FullPathAnalysis",
-                                         "CollisionAndMapBoundary",
-                                         "Clearance"
-                                     ],
-        "MeasurementPassedGates":  [
-                                       "InputAnalysis",
-                                       "VehicleCurvature",
-                                       "Deviation",
-                                       "FullPathAnalysis",
-                                       "CollisionAndMapBoundary"
-                                   ],
-        "RawRangeExceeded":  true,
-        "RawMinimumCurvature":  8.21565038222615E-14,
-        "RawMaximumCurvature":  0.41667044179866869,
-        "CandidateMinimumCurvature":  -0.035866224026958556,
-        "CandidateMaximumCurvature":  0.41190753085946763,
-        "HardPhysicalQualified":  true,
-        "Eminus":  0.035866224027040712,
-        "Eplus":  0,
-        "Rminus":  0.0860781577695353,
-        "Rplus":  0,
-        "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-        "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  0.41190753085946763,
-        "AnalyticVehicleCurvatureUtilization":  0.97811108547917214,
-        "DiscreteVehicleCurvatureUtilization":  0.494289037031361,
-        "MaximumDeviationMeters":  6.2208064797947889E-05,
-        "MaximumDeviationLimitMeters":  0.1,
-        "CollisionAndMapBoundaryPassed":  true,
-        "MinimumBodyClearanceMeters":  "not evaluated",
-        "MinimumClearanceReserveMeters":  0.02,
-        "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "MinimumPeakGradientImprovementRatio":  "not evaluated",
-        "PeakGradientImprovementPercent":  "not evaluated",
-        "RawCurvatureVariationCost":  "not evaluated",
-        "CandidateCurvatureVariationCost":  "not evaluated",
-        "MaximumVariationCostRegressionRatio":  "not evaluated",
-        "VariationCostRegressionPercent":  "not evaluated",
-        "AbsolutePathLengthChangeMeters":  "not evaluated",
-        "StrictEvaluatorElapsedMilliseconds":  0.7533,
-        "MeasurementEvaluatorElapsedMilliseconds":  1.6731,
-        "StrictPipelineStatus":  "Unchanged",
-        "MeasurementPipelineStatus":  "Unchanged",
-        "Intervals":  [
-                          {
-                              "IntervalIndex":  0,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.079999924153614538,
-                              "GeometricC0":  0.41666666666666663,
-                              "GeometricC1":  0.18490900890188469,
-                              "GeometricC2":  0.81509257123264345,
-                              "GeometricC3":  0.24999999999999997,
-                              "VehicleC0":  0.41666666666666663,
-                              "VehicleC1":  0.18490900890188469,
-                              "VehicleC2":  0.81509257123264345,
-                              "VehicleC3":  0.24999999999999997,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  744,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  7.5072718751555112E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  0.25,
-                              "AnalyticMaximumCurvaturePerMeter":  0.49287523731059291,
-                              "AnalyticMaximumGradientPerSquareMeter":  21.190991511975508
-                          },
-                          {
-                              "IntervalIndex":  1,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.12000008571444681,
-                              "GeometricC0":  0.24999999999999997,
-                              "GeometricC1":  -0.49999998006598478,
-                              "GeometricC2":  0.24999998006598481,
-                              "GeometricC3":  0,
-                              "VehicleC0":  0.24999999999999997,
-                              "VehicleC1":  -0.49999998006598478,
-                              "VehicleC2":  0.24999998006598481,
-                              "VehicleC3":  0,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  648,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  8.66606526828704E-13,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  -0.093484688313616032,
-                              "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.749986108777229
-                          }
-                      ]
-    },
-    {
-        "StableKey":  "single-turn/s0/r0/w5/seed1",
-        "CandidateSha256":  "0c3e01f24eef8ced5cfc16b5cc2b5939c2cc4fd34cbdc3efd2dfba11abbf471f",
-        "StrictCandidateSha256":  "0c3e01f24eef8ced5cfc16b5cc2b5939c2cc4fd34cbdc3efd2dfba11abbf471f",
-        "MeasurementCandidateSha256":  "0c3e01f24eef8ced5cfc16b5cc2b5939c2cc4fd34cbdc3efd2dfba11abbf471f",
-        "StrictCandidateContent":  {
-                                       "PreparedSegmentDirection":  0,
-                                       "SegmentIndex":  0,
-                                       "WindowStartArcLengthMeters":  1.9199999999999982,
-                                       "WindowEndArcLengthMeters":  2.1199999999999983,
-                                       "DirectionSign":  1,
-                                       "HardNodes":  [
-                                                         {
-                                                             "X":  2.7216397035494579,
-                                                             "Y":  1.7279184433567971,
-                                                             "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                             "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                             "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                         },
-                                                         {
-                                                             "X":  2.7764244476704873,
-                                                             "Y":  1.786210614200662,
-                                                             "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                             "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                         },
-                                                         {
-                                                             "X":  2.8571139169604551,
-                                                             "Y":  1.8750318365841865,
-                                                             "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0,
-                                                             "VehicleCurvaturePerMeter":  0
-                                                         }
-                                                     ],
-                                       "CandidatePoints":  [
-                                                               {
-                                                                   "X":  2.7216397035494579,
-                                                                   "Y":  1.7279184433567971,
-                                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                   "HeadingRadians":  0.799999999999999,
-                                                                   "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7355188205180503,
-                                                                   "Y":  1.7423187041190662,
-                                                                   "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                   "HeadingRadians":  0.80751185014529236,
-                                                                   "UnwrappedHeadingRadians":  0.80751185014529236,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7492871654450752,
-                                                                   "Y":  1.7568248978023655,
-                                                                   "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                   "HeadingRadians":  0.81576181240610368,
-                                                                   "UnwrappedHeadingRadians":  0.81576181240610368,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.762923469279341,
-                                                                   "Y":  1.7714552535738202,
-                                                                   "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                   "HeadingRadians":  0.82542852155235713,
-                                                                   "UnwrappedHeadingRadians":  0.82542852155235713,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7764244476704873,
-                                                                   "Y":  1.786210614200662,
-                                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7925350453916531,
-                                                                   "Y":  1.8039996352562,
-                                                                   "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                   "HeadingRadians":  0.83525333590000572,
-                                                                   "UnwrappedHeadingRadians":  0.83525333590000572,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8086541799963407,
-                                                                   "Y":  1.8217809212292646,
-                                                                   "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                   "HeadingRadians":  0.83333333602263038,
-                                                                   "UnwrappedHeadingRadians":  0.83333333602263038,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8248074261578475,
-                                                                   "Y":  1.8395312270285291,
-                                                                   "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                   "HeadingRadians":  0.831893334994058,
-                                                                   "UnwrappedHeadingRadians":  0.831893334994058,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8409691918977966,
-                                                                   "Y":  1.8572737786030467,
-                                                                   "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                   "HeadingRadians":  0.832373333842861,
-                                                                   "UnwrappedHeadingRadians":  0.832373333842861,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8571139169604551,
-                                                                   "Y":  1.8750318365841865,
-                                                                   "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               }
-                                                           ]
-                                   },
-        "MeasurementCandidateContent":  {
-                                            "PreparedSegmentDirection":  0,
-                                            "SegmentIndex":  0,
-                                            "WindowStartArcLengthMeters":  1.9199999999999982,
-                                            "WindowEndArcLengthMeters":  2.1199999999999983,
-                                            "DirectionSign":  1,
-                                            "HardNodes":  [
-                                                              {
-                                                                  "X":  2.7216397035494579,
-                                                                  "Y":  1.7279184433567971,
-                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                  "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                  "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                              },
-                                                              {
-                                                                  "X":  2.7764244476704873,
-                                                                  "Y":  1.786210614200662,
-                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                  "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                              },
-                                                              {
-                                                                  "X":  2.8571139169604551,
-                                                                  "Y":  1.8750318365841865,
-                                                                  "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0,
-                                                                  "VehicleCurvaturePerMeter":  0
-                                                              }
-                                                          ],
-                                            "CandidatePoints":  [
-                                                                    {
-                                                                        "X":  2.7216397035494579,
-                                                                        "Y":  1.7279184433567971,
-                                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                        "HeadingRadians":  0.799999999999999,
-                                                                        "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7355188205180503,
-                                                                        "Y":  1.7423187041190662,
-                                                                        "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                        "HeadingRadians":  0.80751185014529236,
-                                                                        "UnwrappedHeadingRadians":  0.80751185014529236,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7492871654450752,
-                                                                        "Y":  1.7568248978023655,
-                                                                        "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                        "HeadingRadians":  0.81576181240610368,
-                                                                        "UnwrappedHeadingRadians":  0.81576181240610368,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.762923469279341,
-                                                                        "Y":  1.7714552535738202,
-                                                                        "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                        "HeadingRadians":  0.82542852155235713,
-                                                                        "UnwrappedHeadingRadians":  0.82542852155235713,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7764244476704873,
-                                                                        "Y":  1.786210614200662,
-                                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7925350453916531,
-                                                                        "Y":  1.8039996352562,
-                                                                        "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                        "HeadingRadians":  0.83525333590000572,
-                                                                        "UnwrappedHeadingRadians":  0.83525333590000572,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8086541799963407,
-                                                                        "Y":  1.8217809212292646,
-                                                                        "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                        "HeadingRadians":  0.83333333602263038,
-                                                                        "UnwrappedHeadingRadians":  0.83333333602263038,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8248074261578475,
-                                                                        "Y":  1.8395312270285291,
-                                                                        "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                        "HeadingRadians":  0.831893334994058,
-                                                                        "UnwrappedHeadingRadians":  0.831893334994058,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8409691918977966,
-                                                                        "Y":  1.8572737786030467,
-                                                                        "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                        "HeadingRadians":  0.832373333842861,
-                                                                        "UnwrappedHeadingRadians":  0.832373333842861,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8571139169604551,
-                                                                        "Y":  1.8750318365841865,
-                                                                        "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    }
-                                                                ]
-                                        },
-        "StrictDelayedCGenRangeSnapshot":  "not evaluated",
-        "StrictRecord":  {
-                             "StableKey":  "single-turn/s0/r0/w5/seed1",
-                             "CandidateSha256":  "0c3e01f24eef8ced5cfc16b5cc2b5939c2cc4fd34cbdc3efd2dfba11abbf471f",
-                             "CandidateContent":  {
-                                                      "PreparedSegmentDirection":  0,
-                                                      "SegmentIndex":  0,
-                                                      "WindowStartArcLengthMeters":  1.9199999999999982,
-                                                      "WindowEndArcLengthMeters":  2.1199999999999983,
-                                                      "DirectionSign":  1,
-                                                      "HardNodes":  [
-                                                                        {
-                                                                            "X":  2.7216397035494579,
-                                                                            "Y":  1.7279184433567971,
-                                                                            "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                            "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                            "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                        },
-                                                                        {
-                                                                            "X":  2.7764244476704873,
-                                                                            "Y":  1.786210614200662,
-                                                                            "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                            "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                                        },
-                                                                        {
-                                                                            "X":  2.8571139169604551,
-                                                                            "Y":  1.8750318365841865,
-                                                                            "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0,
-                                                                            "VehicleCurvaturePerMeter":  0
-                                                                        }
-                                                                    ],
-                                                      "CandidatePoints":  [
-                                                                              {
-                                                                                  "X":  2.7216397035494579,
-                                                                                  "Y":  1.7279184433567971,
-                                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                  "HeadingRadians":  0.799999999999999,
-                                                                                  "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7355188205180503,
-                                                                                  "Y":  1.7423187041190662,
-                                                                                  "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                  "HeadingRadians":  0.80751185014529236,
-                                                                                  "UnwrappedHeadingRadians":  0.80751185014529236,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7492871654450752,
-                                                                                  "Y":  1.7568248978023655,
-                                                                                  "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                  "HeadingRadians":  0.81576181240610368,
-                                                                                  "UnwrappedHeadingRadians":  0.81576181240610368,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.762923469279341,
-                                                                                  "Y":  1.7714552535738202,
-                                                                                  "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                  "HeadingRadians":  0.82542852155235713,
-                                                                                  "UnwrappedHeadingRadians":  0.82542852155235713,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7764244476704873,
-                                                                                  "Y":  1.786210614200662,
-                                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7925350453916531,
-                                                                                  "Y":  1.8039996352562,
-                                                                                  "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                                  "HeadingRadians":  0.83525333590000572,
-                                                                                  "UnwrappedHeadingRadians":  0.83525333590000572,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8086541799963407,
-                                                                                  "Y":  1.8217809212292646,
-                                                                                  "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                                  "HeadingRadians":  0.83333333602263038,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333602263038,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8248074261578475,
-                                                                                  "Y":  1.8395312270285291,
-                                                                                  "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                                  "HeadingRadians":  0.831893334994058,
-                                                                                  "UnwrappedHeadingRadians":  0.831893334994058,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8409691918977966,
-                                                                                  "Y":  1.8572737786030467,
-                                                                                  "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                                  "HeadingRadians":  0.832373333842861,
-                                                                                  "UnwrappedHeadingRadians":  0.832373333842861,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8571139169604551,
-                                                                                  "Y":  1.8750318365841865,
-                                                                                  "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              }
-                                                                          ]
-                                                  },
-                             "RecordKind":  "Canonical",
-                             "CandidateIndex":  4,
-                             "CanonicalCandidateIndex":  4,
-                             "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed1",
-                             "EquivalenceParameters":  [
-                                                           0,
-                                                           0.25,
-                                                           0.5,
-                                                           0.75,
-                                                           1
-                                                       ],
-                             "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                             "EvaluatorExecutedDirectly":  true,
-                             "EvaluatorResult":  "CurvatureOvershoot",
-                             "StopGate":  "RawCurvatureRange",
-                             "MeasurementTrace":  "not evaluated",
-                             "EvaluatorElapsedMilliseconds":  0.0677,
-                             "RawMinimumCurvature":  8.21565038222615E-14,
-                             "RawMaximumCurvature":  0.41667044179866869,
-                             "CandidateMinimumCurvature":  -0.035866199469035173,
-                             "CandidateMaximumCurvature":  0.41190753082515014,
-                             "DelayedCGenRangeSnapshot":  "not evaluated",
-                             "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                             "MaximumDeviationLimitMeters":  0.1,
-                             "MinimumPeakGradientImprovementRatio":  0.2,
-                             "MaximumVariationCostRegressionRatio":  0.02,
-                             "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                             "DirectionSign":  1,
-                             "HardNodes":  [
-                                               {
-                                                   "X":  2.7216397035494579,
-                                                   "Y":  1.7279184433567971,
-                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                   "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                   "VehicleCurvaturePerMeter":  0.41666666666666663
-                                               },
-                                               {
-                                                   "X":  2.7764244476704873,
-                                                   "Y":  1.786210614200662,
-                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                   "VehicleCurvaturePerMeter":  0.24999999999999997
-                                               },
-                                               {
-                                                   "X":  2.8571139169604551,
-                                                   "Y":  1.8750318365841865,
-                                                   "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0,
-                                                   "VehicleCurvaturePerMeter":  0
-                                               }
-                                           ],
-                             "Intervals":  [
-                                               {
-                                                   "IntervalIndex":  0,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.079999924153630289,
-                                                   "GeometricC0":  0.41666666666666663,
-                                                   "GeometricC1":  0.18490899649776424,
-                                                   "GeometricC2":  0.81509258363643566,
-                                                   "GeometricC3":  0.24999999999999997,
-                                                   "VehicleC0":  0.41666666666666663,
-                                                   "VehicleC1":  0.18490899649776424,
-                                                   "VehicleC2":  0.81509258363643566,
-                                                   "VehicleC3":  0.24999999999999997,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  744,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1710,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.171,
-                                                   "PreOverwritePositionErrorMeters":  3.5382068915725518E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  0.24999999999999978,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.49287524010500611,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  21.190991977113988
-                                               },
-                                               {
-                                                   "IntervalIndex":  1,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.12000008571436285,
-                                                   "GeometricC0":  0.24999999999999997,
-                                                   "GeometricC1":  -0.49999974061573366,
-                                                   "GeometricC2":  0.24999974061573368,
-                                                   "GeometricC3":  0,
-                                                   "VehicleC0":  0.24999999999999997,
-                                                   "VehicleC1":  -0.49999974061573366,
-                                                   "VehicleC2":  0.24999974061573368,
-                                                   "VehicleC3":  0,
-                                                   "OuterIterationCount":  5,
-                                                   "DampingTrialCount":  5,
-                                                   "IntegrationEvaluationCount":  600,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1041,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1041,
-                                                   "PreOverwritePositionErrorMeters":  1.732707838987991E-10,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  -0.093484640626866927,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.749980122538343
-                                               }
-                                           ],
-                             "CandidateDiagnostic":  {
-                                                         "WindowIndex":  5,
-                                                         "SeedIndex":  1,
-                                                         "CombinationAttemptIndex":  17,
-                                                         "SolverInvocationCount":  22,
-                                                         "MaximumPreOverwritePositionErrorMeters":  1.732707838987991E-10,
-                                                         "MaximumPostOverwritePositionErrorMeters":  0,
-                                                         "MaximumTangentSeamErrorRadians":  0,
-                                                         "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                         "MinimumAdjacentPointDistanceMeters":  0.019999902306255073
-                                                     }
-                         },
-        "MeasurementRecord":  {
-                                  "StableKey":  "single-turn/s0/r0/w5/seed1",
-                                  "CandidateSha256":  "0c3e01f24eef8ced5cfc16b5cc2b5939c2cc4fd34cbdc3efd2dfba11abbf471f",
-                                  "CandidateContent":  {
-                                                           "PreparedSegmentDirection":  0,
-                                                           "SegmentIndex":  0,
-                                                           "WindowStartArcLengthMeters":  1.9199999999999982,
-                                                           "WindowEndArcLengthMeters":  2.1199999999999983,
-                                                           "DirectionSign":  1,
-                                                           "HardNodes":  [
-                                                                             {
-                                                                                 "X":  2.7216397035494579,
-                                                                                 "Y":  1.7279184433567971,
-                                                                                 "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                                 "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                                 "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.7764244476704873,
-                                                                                 "Y":  1.786210614200662,
-                                                                                 "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                                 "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.8571139169604551,
-                                                                                 "Y":  1.8750318365841865,
-                                                                                 "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0,
-                                                                                 "VehicleCurvaturePerMeter":  0
-                                                                             }
-                                                                         ],
-                                                           "CandidatePoints":  [
-                                                                                   {
-                                                                                       "X":  2.7216397035494579,
-                                                                                       "Y":  1.7279184433567971,
-                                                                                       "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                       "HeadingRadians":  0.799999999999999,
-                                                                                       "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7355188205180503,
-                                                                                       "Y":  1.7423187041190662,
-                                                                                       "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                       "HeadingRadians":  0.80751185014529236,
-                                                                                       "UnwrappedHeadingRadians":  0.80751185014529236,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7492871654450752,
-                                                                                       "Y":  1.7568248978023655,
-                                                                                       "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                       "HeadingRadians":  0.81576181240610368,
-                                                                                       "UnwrappedHeadingRadians":  0.81576181240610368,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.762923469279341,
-                                                                                       "Y":  1.7714552535738202,
-                                                                                       "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                       "HeadingRadians":  0.82542852155235713,
-                                                                                       "UnwrappedHeadingRadians":  0.82542852155235713,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7764244476704873,
-                                                                                       "Y":  1.786210614200662,
-                                                                                       "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7925350453916531,
-                                                                                       "Y":  1.8039996352562,
-                                                                                       "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                                       "HeadingRadians":  0.83525333590000572,
-                                                                                       "UnwrappedHeadingRadians":  0.83525333590000572,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8086541799963407,
-                                                                                       "Y":  1.8217809212292646,
-                                                                                       "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                                       "HeadingRadians":  0.83333333602263038,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333602263038,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8248074261578475,
-                                                                                       "Y":  1.8395312270285291,
-                                                                                       "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                                       "HeadingRadians":  0.831893334994058,
-                                                                                       "UnwrappedHeadingRadians":  0.831893334994058,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8409691918977966,
-                                                                                       "Y":  1.8572737786030467,
-                                                                                       "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                                       "HeadingRadians":  0.832373333842861,
-                                                                                       "UnwrappedHeadingRadians":  0.832373333842861,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8571139169604551,
-                                                                                       "Y":  1.8750318365841865,
-                                                                                       "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   }
-                                                                               ]
-                                                       },
-                                  "RecordKind":  "Canonical",
-                                  "CandidateIndex":  4,
-                                  "CanonicalCandidateIndex":  4,
-                                  "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed1",
-                                  "EquivalenceParameters":  [
-                                                                0,
-                                                                0.25,
-                                                                0.5,
-                                                                0.75,
-                                                                1
-                                                            ],
-                                  "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                                  "EvaluatorExecutedDirectly":  true,
-                                  "EvaluatorResult":  "InsufficientClearance",
-                                  "StopGate":  "Clearance",
-                                  "MeasurementTrace":  {
-                                                           "CandidateIndex":  4,
-                                                           "RawRangeExceeded":  true,
-                                                           "CollisionAndMapBoundaryPassed":  true,
-                                                           "ExecutedGates":  [
-                                                                                 "InputAnalysis",
-                                                                                 "VehicleCurvature",
-                                                                                 "RawCurvatureRange",
-                                                                                 "Deviation",
-                                                                                 "FullPathAnalysis",
-                                                                                 "CollisionAndMapBoundary",
-                                                                                 "Clearance"
-                                                                             ],
-                                                           "PassedGates":  [
-                                                                               "InputAnalysis",
-                                                                               "VehicleCurvature",
-                                                                               "Deviation",
-                                                                               "FullPathAnalysis",
-                                                                               "CollisionAndMapBoundary"
-                                                                           ],
-                                                           "StopGate":  "Clearance",
-                                                           "Result":  "InsufficientClearance",
-                                                           "Reason":  "局部 G2 候选净空不足。",
-                                                           "RawMinimumCurvature":  8.21565038222615E-14,
-                                                           "RawMaximumCurvature":  0.41667044179866869,
-                                                           "CandidateMinimumCurvature":  -0.035866199469035173,
-                                                           "CandidateMaximumCurvature":  0.41190753082515014,
-                                                           "Eminus":  0.03586619946911733,
-                                                           "Eplus":  0,
-                                                           "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                                           "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  0.41190753082515014,
-                                                           "MaximumDeviationMeters":  6.2208119525464827E-05,
-                                                           "MaximumDeviationLimitMeters":  0.1,
-                                                           "MinimumBodyClearanceMeters":  "not evaluated",
-                                                           "MinimumClearanceReserveMeters":  0.02,
-                                                           "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "MinimumPeakGradientImprovementRatio":  "not evaluated",
-                                                           "RawCurvatureVariationCost":  "not evaluated",
-                                                           "CandidateCurvatureVariationCost":  "not evaluated",
-                                                           "MaximumVariationCostRegressionRatio":  "not evaluated",
-                                                           "AbsolutePathLengthChangeMeters":  "not evaluated"
-                                                       },
-                                  "EvaluatorElapsedMilliseconds":  0.8792,
-                                  "RawMinimumCurvature":  8.21565038222615E-14,
-                                  "RawMaximumCurvature":  0.41667044179866869,
-                                  "CandidateMinimumCurvature":  -0.035866199469035173,
-                                  "CandidateMaximumCurvature":  0.41190753082515014,
-                                  "DelayedCGenRangeSnapshot":  "not evaluated",
-                                  "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                                  "MaximumDeviationLimitMeters":  0.1,
-                                  "MinimumPeakGradientImprovementRatio":  0.2,
-                                  "MaximumVariationCostRegressionRatio":  0.02,
-                                  "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                  "DirectionSign":  1,
-                                  "HardNodes":  [
-                                                    {
-                                                        "X":  2.7216397035494579,
-                                                        "Y":  1.7279184433567971,
-                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                        "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                        "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                    },
-                                                    {
-                                                        "X":  2.7764244476704873,
-                                                        "Y":  1.786210614200662,
-                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                        "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                    },
-                                                    {
-                                                        "X":  2.8571139169604551,
-                                                        "Y":  1.8750318365841865,
-                                                        "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0,
-                                                        "VehicleCurvaturePerMeter":  0
-                                                    }
-                                                ],
-                                  "Intervals":  [
-                                                    {
-                                                        "IntervalIndex":  0,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.079999924153630289,
-                                                        "GeometricC0":  0.41666666666666663,
-                                                        "GeometricC1":  0.18490899649776424,
-                                                        "GeometricC2":  0.81509258363643566,
-                                                        "GeometricC3":  0.24999999999999997,
-                                                        "VehicleC0":  0.41666666666666663,
-                                                        "VehicleC1":  0.18490899649776424,
-                                                        "VehicleC2":  0.81509258363643566,
-                                                        "VehicleC3":  0.24999999999999997,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  744,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1684,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1684,
-                                                        "PreOverwritePositionErrorMeters":  3.5382068915725518E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  0.24999999999999978,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.49287524010500611,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  21.190991977113988
-                                                    },
-                                                    {
-                                                        "IntervalIndex":  1,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.12000008571436285,
-                                                        "GeometricC0":  0.24999999999999997,
-                                                        "GeometricC1":  -0.49999974061573366,
-                                                        "GeometricC2":  0.24999974061573368,
-                                                        "GeometricC3":  0,
-                                                        "VehicleC0":  0.24999999999999997,
-                                                        "VehicleC1":  -0.49999974061573366,
-                                                        "VehicleC2":  0.24999974061573368,
-                                                        "VehicleC3":  0,
-                                                        "OuterIterationCount":  5,
-                                                        "DampingTrialCount":  5,
-                                                        "IntegrationEvaluationCount":  600,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1386,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1386,
-                                                        "PreOverwritePositionErrorMeters":  1.732707838987991E-10,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  -0.093484640626866927,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.749980122538343
-                                                    }
-                                                ],
-                                  "CandidateDiagnostic":  {
-                                                              "WindowIndex":  5,
-                                                              "SeedIndex":  1,
-                                                              "CombinationAttemptIndex":  17,
-                                                              "SolverInvocationCount":  22,
-                                                              "MaximumPreOverwritePositionErrorMeters":  1.732707838987991E-10,
-                                                              "MaximumPostOverwritePositionErrorMeters":  0,
-                                                              "MaximumTangentSeamErrorRadians":  0,
-                                                              "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                              "MinimumAdjacentPointDistanceMeters":  0.019999902306255073
-                                                          }
-                              },
-        "SourceIdentity":  {
-                               "SharedBaselineCommit":  "da9157b17a7e4206ed4cdebd1985ee2577fae480",
-                               "FrozenCoreCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "StrictRootCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementRootCommit":  "c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825",
-                               "StrictRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\strict-root-3006aa0",
-                               "MeasurementRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\measurement-root-3006aa0",
-                               "FixtureSha256":  "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
-                               "SharedBaselineEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "StrictEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "MeasurementEvaluatorSha256":  "ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e",
-                               "MeasurementCommitParent":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementCommitChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "MeasurementCommitHistoryProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-commit-history-proof.json",
-                               "MeasurementCommitHistoryProofSha256":  "b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0",
-                               "ProbeSourceManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea",
-                               "AllowedChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "StrictAllowedDifferenceSummary":  "Complete strict-root source diff from the shared baseline.",
-                               "StrictAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictAllowedDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "StrictSharedBaselineDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictSharedBaselineDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "MeasurementAllowedDifferenceSummary":  "Only the measurement evaluator raw-range branch and trace capture differ.",
-                               "MeasurementAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-evaluator.diff",
-                               "MeasurementEvaluatorDiffSha256":  "d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd",
-                               "StrictEvaluatorHashProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-evaluator-hash-proof.json",
-                               "StrictEvaluatorHashProofSha256":  "a0869c7b26f8e203dca2361dd39169d60f0a8fd3c9dd58c9697aed7abd5c94e7",
-                               "FrozenCoreJsonPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core.json",
-                               "FrozenCoreJsonSha256":  "08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa",
-                               "FrozenCoreManifestPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core-probe-source-manifest.txt",
-                               "FrozenCoreManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea"
-                           },
-        "RecordKind":  "Canonical",
-        "CanonicalCandidateIndex":  4,
-        "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed1",
-        "EquivalenceParameters":  [
-                                      0,
-                                      0.25,
-                                      0.5,
-                                      0.75,
-                                      1
-                                  ],
-        "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-        "EvaluatorExecutedDirectly":  true,
-        "StrictResult":  "CurvatureOvershoot",
-        "StrictStopGate":  "RawCurvatureRange",
-        "MeasurementResult":  "InsufficientClearance",
-        "MeasurementStopGate":  "Clearance",
-        "MeasurementReason":  "局部 G2 候选净空不足。",
-        "MeasurementExecutedGates":  [
-                                         "InputAnalysis",
-                                         "VehicleCurvature",
-                                         "RawCurvatureRange",
-                                         "Deviation",
-                                         "FullPathAnalysis",
-                                         "CollisionAndMapBoundary",
-                                         "Clearance"
-                                     ],
-        "MeasurementPassedGates":  [
-                                       "InputAnalysis",
-                                       "VehicleCurvature",
-                                       "Deviation",
-                                       "FullPathAnalysis",
-                                       "CollisionAndMapBoundary"
-                                   ],
-        "RawRangeExceeded":  true,
-        "RawMinimumCurvature":  8.21565038222615E-14,
-        "RawMaximumCurvature":  0.41667044179866869,
-        "CandidateMinimumCurvature":  -0.035866199469035173,
-        "CandidateMaximumCurvature":  0.41190753082515014,
-        "HardPhysicalQualified":  true,
-        "Eminus":  0.03586619946911733,
-        "Eplus":  0,
-        "Rminus":  0.08607809883105319,
-        "Rplus":  0,
-        "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-        "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  0.41190753082515014,
-        "AnalyticVehicleCurvatureUtilization":  0.97811110036372262,
-        "DiscreteVehicleCurvatureUtilization":  0.49428903699018006,
-        "MaximumDeviationMeters":  6.2208119525464827E-05,
-        "MaximumDeviationLimitMeters":  0.1,
-        "CollisionAndMapBoundaryPassed":  true,
-        "MinimumBodyClearanceMeters":  "not evaluated",
-        "MinimumClearanceReserveMeters":  0.02,
-        "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "MinimumPeakGradientImprovementRatio":  "not evaluated",
-        "PeakGradientImprovementPercent":  "not evaluated",
-        "RawCurvatureVariationCost":  "not evaluated",
-        "CandidateCurvatureVariationCost":  "not evaluated",
-        "MaximumVariationCostRegressionRatio":  "not evaluated",
-        "VariationCostRegressionPercent":  "not evaluated",
-        "AbsolutePathLengthChangeMeters":  "not evaluated",
-        "StrictEvaluatorElapsedMilliseconds":  0.0677,
-        "MeasurementEvaluatorElapsedMilliseconds":  0.8792,
-        "StrictPipelineStatus":  "Unchanged",
-        "MeasurementPipelineStatus":  "Unchanged",
-        "Intervals":  [
-                          {
-                              "IntervalIndex":  0,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.079999924153630289,
-                              "GeometricC0":  0.41666666666666663,
-                              "GeometricC1":  0.18490899649776424,
-                              "GeometricC2":  0.81509258363643566,
-                              "GeometricC3":  0.24999999999999997,
-                              "VehicleC0":  0.41666666666666663,
-                              "VehicleC1":  0.18490899649776424,
-                              "VehicleC2":  0.81509258363643566,
-                              "VehicleC3":  0.24999999999999997,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  744,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  3.5382068915725518E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  0.24999999999999978,
-                              "AnalyticMaximumCurvaturePerMeter":  0.49287524010500611,
-                              "AnalyticMaximumGradientPerSquareMeter":  21.190991977113988
-                          },
-                          {
-                              "IntervalIndex":  1,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.12000008571436285,
-                              "GeometricC0":  0.24999999999999997,
-                              "GeometricC1":  -0.49999974061573366,
-                              "GeometricC2":  0.24999974061573368,
-                              "GeometricC3":  0,
-                              "VehicleC0":  0.24999999999999997,
-                              "VehicleC1":  -0.49999974061573366,
-                              "VehicleC2":  0.24999974061573368,
-                              "VehicleC3":  0,
-                              "OuterIterationCount":  5,
-                              "DampingTrialCount":  5,
-                              "IntegrationEvaluationCount":  600,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  1.732707838987991E-10,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  -0.093484640626866927,
-                              "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.749980122538343
-                          }
-                      ]
-    },
-    {
-        "StableKey":  "single-turn/s0/r0/w5/seed2",
-        "CandidateSha256":  "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392",
-        "StrictCandidateSha256":  "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392",
-        "MeasurementCandidateSha256":  "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392",
-        "StrictCandidateContent":  {
-                                       "PreparedSegmentDirection":  0,
-                                       "SegmentIndex":  0,
-                                       "WindowStartArcLengthMeters":  1.9199999999999982,
-                                       "WindowEndArcLengthMeters":  2.1199999999999983,
-                                       "DirectionSign":  1,
-                                       "HardNodes":  [
-                                                         {
-                                                             "X":  2.7216397035494579,
-                                                             "Y":  1.7279184433567971,
-                                                             "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                             "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                             "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                         },
-                                                         {
-                                                             "X":  2.7764244476704873,
-                                                             "Y":  1.786210614200662,
-                                                             "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                             "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                         },
-                                                         {
-                                                             "X":  2.8571139169604551,
-                                                             "Y":  1.8750318365841865,
-                                                             "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                             "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                             "GeometricCurvaturePerMeter":  0,
-                                                             "VehicleCurvaturePerMeter":  0
-                                                         }
-                                                     ],
-                                       "CandidatePoints":  [
-                                                               {
-                                                                   "X":  2.7216397035494579,
-                                                                   "Y":  1.7279184433567971,
-                                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                   "HeadingRadians":  0.799999999999999,
-                                                                   "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7355188205174521,
-                                                                   "Y":  1.7423187041196302,
-                                                                   "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                   "HeadingRadians":  0.80751185024996908,
-                                                                   "UnwrappedHeadingRadians":  0.80751185024996908,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7492871654421882,
-                                                                   "Y":  1.7568248978050907,
-                                                                   "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                   "HeadingRadians":  0.8157618125921976,
-                                                                   "UnwrappedHeadingRadians":  0.8157618125921976,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7629234692741469,
-                                                                   "Y":  1.7714552535786874,
-                                                                   "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                   "HeadingRadians":  0.82542852165703573,
-                                                                   "UnwrappedHeadingRadians":  0.82542852165703573,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7764244476704873,
-                                                                   "Y":  1.786210614200662,
-                                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.7925350453990334,
-                                                                   "Y":  1.803999635249544,
-                                                                   "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                   "HeadingRadians":  0.835253334801853,
-                                                                   "UnwrappedHeadingRadians":  0.835253334801853,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.80865418003672,
-                                                                   "Y":  1.8217809211927092,
-                                                                   "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                   "HeadingRadians":  0.83333333355178374,
-                                                                   "UnwrappedHeadingRadians":  0.83333333355178374,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8248074262445737,
-                                                                   "Y":  1.8395312269498194,
-                                                                   "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                   "HeadingRadians":  0.8318933325232104,
-                                                                   "UnwrappedHeadingRadians":  0.8318933325232104,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8409691920174533,
-                                                                   "Y":  1.8572737784943611,
-                                                                   "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                   "HeadingRadians":  0.83237333274470626,
-                                                                   "UnwrappedHeadingRadians":  0.83237333274470626,
-                                                                   "Source":  4
-                                                               },
-                                                               {
-                                                                   "X":  2.8571139169604551,
-                                                                   "Y":  1.8750318365841865,
-                                                                   "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                   "HeadingRadians":  0.83333333333333237,
-                                                                   "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                   "Source":  4
-                                                               }
-                                                           ]
-                                   },
-        "MeasurementCandidateContent":  {
-                                            "PreparedSegmentDirection":  0,
-                                            "SegmentIndex":  0,
-                                            "WindowStartArcLengthMeters":  1.9199999999999982,
-                                            "WindowEndArcLengthMeters":  2.1199999999999983,
-                                            "DirectionSign":  1,
-                                            "HardNodes":  [
-                                                              {
-                                                                  "X":  2.7216397035494579,
-                                                                  "Y":  1.7279184433567971,
-                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                  "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                  "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                              },
-                                                              {
-                                                                  "X":  2.7764244476704873,
-                                                                  "Y":  1.786210614200662,
-                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                  "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                              },
-                                                              {
-                                                                  "X":  2.8571139169604551,
-                                                                  "Y":  1.8750318365841865,
-                                                                  "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                  "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                  "GeometricCurvaturePerMeter":  0,
-                                                                  "VehicleCurvaturePerMeter":  0
-                                                              }
-                                                          ],
-                                            "CandidatePoints":  [
-                                                                    {
-                                                                        "X":  2.7216397035494579,
-                                                                        "Y":  1.7279184433567971,
-                                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                        "HeadingRadians":  0.799999999999999,
-                                                                        "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7355188205174521,
-                                                                        "Y":  1.7423187041196302,
-                                                                        "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                        "HeadingRadians":  0.80751185024996908,
-                                                                        "UnwrappedHeadingRadians":  0.80751185024996908,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7492871654421882,
-                                                                        "Y":  1.7568248978050907,
-                                                                        "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                        "HeadingRadians":  0.8157618125921976,
-                                                                        "UnwrappedHeadingRadians":  0.8157618125921976,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7629234692741469,
-                                                                        "Y":  1.7714552535786874,
-                                                                        "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                        "HeadingRadians":  0.82542852165703573,
-                                                                        "UnwrappedHeadingRadians":  0.82542852165703573,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7764244476704873,
-                                                                        "Y":  1.786210614200662,
-                                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.7925350453990334,
-                                                                        "Y":  1.803999635249544,
-                                                                        "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                        "HeadingRadians":  0.835253334801853,
-                                                                        "UnwrappedHeadingRadians":  0.835253334801853,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.80865418003672,
-                                                                        "Y":  1.8217809211927092,
-                                                                        "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                        "HeadingRadians":  0.83333333355178374,
-                                                                        "UnwrappedHeadingRadians":  0.83333333355178374,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8248074262445737,
-                                                                        "Y":  1.8395312269498194,
-                                                                        "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                        "HeadingRadians":  0.8318933325232104,
-                                                                        "UnwrappedHeadingRadians":  0.8318933325232104,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8409691920174533,
-                                                                        "Y":  1.8572737784943611,
-                                                                        "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                        "HeadingRadians":  0.83237333274470626,
-                                                                        "UnwrappedHeadingRadians":  0.83237333274470626,
-                                                                        "Source":  4
-                                                                    },
-                                                                    {
-                                                                        "X":  2.8571139169604551,
-                                                                        "Y":  1.8750318365841865,
-                                                                        "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                        "HeadingRadians":  0.83333333333333237,
-                                                                        "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                        "Source":  4
-                                                                    }
-                                                                ]
-                                        },
-        "StrictDelayedCGenRangeSnapshot":  "not evaluated",
-        "StrictRecord":  {
-                             "StableKey":  "single-turn/s0/r0/w5/seed2",
-                             "CandidateSha256":  "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392",
-                             "CandidateContent":  {
-                                                      "PreparedSegmentDirection":  0,
-                                                      "SegmentIndex":  0,
-                                                      "WindowStartArcLengthMeters":  1.9199999999999982,
-                                                      "WindowEndArcLengthMeters":  2.1199999999999983,
-                                                      "DirectionSign":  1,
-                                                      "HardNodes":  [
-                                                                        {
-                                                                            "X":  2.7216397035494579,
-                                                                            "Y":  1.7279184433567971,
-                                                                            "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                            "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                            "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                        },
-                                                                        {
-                                                                            "X":  2.7764244476704873,
-                                                                            "Y":  1.786210614200662,
-                                                                            "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                            "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                                        },
-                                                                        {
-                                                                            "X":  2.8571139169604551,
-                                                                            "Y":  1.8750318365841865,
-                                                                            "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                            "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                            "GeometricCurvaturePerMeter":  0,
-                                                                            "VehicleCurvaturePerMeter":  0
-                                                                        }
-                                                                    ],
-                                                      "CandidatePoints":  [
-                                                                              {
-                                                                                  "X":  2.7216397035494579,
-                                                                                  "Y":  1.7279184433567971,
-                                                                                  "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                  "HeadingRadians":  0.799999999999999,
-                                                                                  "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7355188205174521,
-                                                                                  "Y":  1.7423187041196302,
-                                                                                  "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                  "HeadingRadians":  0.80751185024996908,
-                                                                                  "UnwrappedHeadingRadians":  0.80751185024996908,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7492871654421882,
-                                                                                  "Y":  1.7568248978050907,
-                                                                                  "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                  "HeadingRadians":  0.8157618125921976,
-                                                                                  "UnwrappedHeadingRadians":  0.8157618125921976,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7629234692741469,
-                                                                                  "Y":  1.7714552535786874,
-                                                                                  "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                  "HeadingRadians":  0.82542852165703573,
-                                                                                  "UnwrappedHeadingRadians":  0.82542852165703573,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7764244476704873,
-                                                                                  "Y":  1.786210614200662,
-                                                                                  "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.7925350453990334,
-                                                                                  "Y":  1.803999635249544,
-                                                                                  "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                                  "HeadingRadians":  0.835253334801853,
-                                                                                  "UnwrappedHeadingRadians":  0.835253334801853,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.80865418003672,
-                                                                                  "Y":  1.8217809211927092,
-                                                                                  "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                                  "HeadingRadians":  0.83333333355178374,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333355178374,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8248074262445737,
-                                                                                  "Y":  1.8395312269498194,
-                                                                                  "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                                  "HeadingRadians":  0.8318933325232104,
-                                                                                  "UnwrappedHeadingRadians":  0.8318933325232104,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8409691920174533,
-                                                                                  "Y":  1.8572737784943611,
-                                                                                  "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                                  "HeadingRadians":  0.83237333274470626,
-                                                                                  "UnwrappedHeadingRadians":  0.83237333274470626,
-                                                                                  "Source":  4
-                                                                              },
-                                                                              {
-                                                                                  "X":  2.8571139169604551,
-                                                                                  "Y":  1.8750318365841865,
-                                                                                  "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                  "HeadingRadians":  0.83333333333333237,
-                                                                                  "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                  "Source":  4
-                                                                              }
-                                                                          ]
-                                                  },
-                             "RecordKind":  "Canonical",
-                             "CandidateIndex":  5,
-                             "CanonicalCandidateIndex":  5,
-                             "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed2",
-                             "EquivalenceParameters":  [
-                                                           0,
-                                                           0.25,
-                                                           0.5,
-                                                           0.75,
-                                                           1
-                                                       ],
-                             "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                             "EvaluatorExecutedDirectly":  true,
-                             "EvaluatorResult":  "CurvatureOvershoot",
-                             "StopGate":  "RawCurvatureRange",
-                             "MeasurementTrace":  "not evaluated",
-                             "EvaluatorElapsedMilliseconds":  0.105,
-                             "RawMinimumCurvature":  8.21565038222615E-14,
-                             "RawMaximumCurvature":  0.41667044179866869,
-                             "CandidateMinimumCurvature":  -0.035866223552879793,
-                             "CandidateMaximumCurvature":  0.41190753089391469,
-                             "DelayedCGenRangeSnapshot":  "not evaluated",
-                             "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                             "MaximumDeviationLimitMeters":  0.1,
-                             "MinimumPeakGradientImprovementRatio":  0.2,
-                             "MaximumVariationCostRegressionRatio":  0.02,
-                             "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                             "DirectionSign":  1,
-                             "HardNodes":  [
-                                               {
-                                                   "X":  2.7216397035494579,
-                                                   "Y":  1.7279184433567971,
-                                                   "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                   "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                   "VehicleCurvaturePerMeter":  0.41666666666666663
-                                               },
-                                               {
-                                                   "X":  2.7764244476704873,
-                                                   "Y":  1.786210614200662,
-                                                   "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                   "VehicleCurvaturePerMeter":  0.24999999999999997
-                                               },
-                                               {
-                                                   "X":  2.8571139169604551,
-                                                   "Y":  1.8750318365841865,
-                                                   "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                   "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                   "GeometricCurvaturePerMeter":  0,
-                                                   "VehicleCurvaturePerMeter":  0
-                                               }
-                                           ],
-                             "Intervals":  [
-                                               {
-                                                   "IntervalIndex":  0,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.079999924153600924,
-                                                   "GeometricC0":  0.41666666666666663,
-                                                   "GeometricC1":  0.18490902131056236,
-                                                   "GeometricC2":  0.81509255882424947,
-                                                   "GeometricC3":  0.24999999999999997,
-                                                   "VehicleC0":  0.41666666666666663,
-                                                   "VehicleC1":  0.18490902131056236,
-                                                   "VehicleC2":  0.81509255882424947,
-                                                   "VehicleC3":  0.24999999999999997,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  744,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1208,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1208,
-                                                   "PreOverwritePositionErrorMeters":  1.147815539419054E-11,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  0.25000000000000011,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.49287523451513321,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  21.190991046663896
-                                               },
-                                               {
-                                                   "IntervalIndex":  1,
-                                                   "Accepted":  true,
-                                                   "Failure":  "None",
-                                                   "Reason":  "",
-                                                   "LengthMeters":  0.12000008571444666,
-                                                   "GeometricC0":  0.24999999999999997,
-                                                   "GeometricC1":  -0.49999997893024717,
-                                                   "GeometricC2":  0.2499999789302472,
-                                                   "GeometricC3":  0,
-                                                   "VehicleC0":  0.24999999999999997,
-                                                   "VehicleC1":  -0.49999997893024717,
-                                                   "VehicleC2":  0.2499999789302472,
-                                                   "VehicleC3":  0,
-                                                   "OuterIterationCount":  6,
-                                                   "DampingTrialCount":  6,
-                                                   "IntegrationEvaluationCount":  648,
-                                                   "MaximumIntegrationDepth":  0,
-                                                   "SolverElapsedStopwatchTicks":  1088,
-                                                   "StopwatchFrequency":  10000000,
-                                                   "SolverElapsedMilliseconds":  0.1088,
-                                                   "PreOverwritePositionErrorMeters":  1.6841317892909525E-12,
-                                                   "AnalyticHeadingErrorRadians":  0,
-                                                   "AnalyticMinimumCurvaturePerMeter":  -0.093484688087432849,
-                                                   "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                                                   "AnalyticMaximumGradientPerSquareMeter":  18.749986080383835
-                                               }
-                                           ],
-                             "CandidateDiagnostic":  {
-                                                         "WindowIndex":  5,
-                                                         "SeedIndex":  2,
-                                                         "CombinationAttemptIndex":  18,
-                                                         "SolverInvocationCount":  24,
-                                                         "MaximumPreOverwritePositionErrorMeters":  1.147815539419054E-11,
-                                                         "MaximumPostOverwritePositionErrorMeters":  0,
-                                                         "MaximumTangentSeamErrorRadians":  0,
-                                                         "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                         "MinimumAdjacentPointDistanceMeters":  0.01999990230624905
-                                                     }
-                         },
-        "MeasurementRecord":  {
-                                  "StableKey":  "single-turn/s0/r0/w5/seed2",
-                                  "CandidateSha256":  "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392",
-                                  "CandidateContent":  {
-                                                           "PreparedSegmentDirection":  0,
-                                                           "SegmentIndex":  0,
-                                                           "WindowStartArcLengthMeters":  1.9199999999999982,
-                                                           "WindowEndArcLengthMeters":  2.1199999999999983,
-                                                           "DirectionSign":  1,
-                                                           "HardNodes":  [
-                                                                             {
-                                                                                 "X":  2.7216397035494579,
-                                                                                 "Y":  1.7279184433567971,
-                                                                                 "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                                                 "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                                                 "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.7764244476704873,
-                                                                                 "Y":  1.786210614200662,
-                                                                                 "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                                                 "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                                             },
-                                                                             {
-                                                                                 "X":  2.8571139169604551,
-                                                                                 "Y":  1.8750318365841865,
-                                                                                 "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                 "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                 "GeometricCurvaturePerMeter":  0,
-                                                                                 "VehicleCurvaturePerMeter":  0
-                                                                             }
-                                                                         ],
-                                                           "CandidatePoints":  [
-                                                                                   {
-                                                                                       "X":  2.7216397035494579,
-                                                                                       "Y":  1.7279184433567971,
-                                                                                       "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                                                       "HeadingRadians":  0.799999999999999,
-                                                                                       "UnwrappedHeadingRadians":  0.799999999999999,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7355188205174521,
-                                                                                       "Y":  1.7423187041196302,
-                                                                                       "ReferenceArcLengthMeters":  1.9399999999999982,
-                                                                                       "HeadingRadians":  0.80751185024996908,
-                                                                                       "UnwrappedHeadingRadians":  0.80751185024996908,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7492871654421882,
-                                                                                       "Y":  1.7568248978050907,
-                                                                                       "ReferenceArcLengthMeters":  1.9599999999999982,
-                                                                                       "HeadingRadians":  0.8157618125921976,
-                                                                                       "UnwrappedHeadingRadians":  0.8157618125921976,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7629234692741469,
-                                                                                       "Y":  1.7714552535786874,
-                                                                                       "ReferenceArcLengthMeters":  1.9799999999999982,
-                                                                                       "HeadingRadians":  0.82542852165703573,
-                                                                                       "UnwrappedHeadingRadians":  0.82542852165703573,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7764244476704873,
-                                                                                       "Y":  1.786210614200662,
-                                                                                       "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.7925350453990334,
-                                                                                       "Y":  1.803999635249544,
-                                                                                       "ReferenceArcLengthMeters":  2.0239999999999982,
-                                                                                       "HeadingRadians":  0.835253334801853,
-                                                                                       "UnwrappedHeadingRadians":  0.835253334801853,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.80865418003672,
-                                                                                       "Y":  1.8217809211927092,
-                                                                                       "ReferenceArcLengthMeters":  2.0479999999999983,
-                                                                                       "HeadingRadians":  0.83333333355178374,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333355178374,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8248074262445737,
-                                                                                       "Y":  1.8395312269498194,
-                                                                                       "ReferenceArcLengthMeters":  2.0719999999999983,
-                                                                                       "HeadingRadians":  0.8318933325232104,
-                                                                                       "UnwrappedHeadingRadians":  0.8318933325232104,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8409691920174533,
-                                                                                       "Y":  1.8572737784943611,
-                                                                                       "ReferenceArcLengthMeters":  2.0959999999999983,
-                                                                                       "HeadingRadians":  0.83237333274470626,
-                                                                                       "UnwrappedHeadingRadians":  0.83237333274470626,
-                                                                                       "Source":  4
-                                                                                   },
-                                                                                   {
-                                                                                       "X":  2.8571139169604551,
-                                                                                       "Y":  1.8750318365841865,
-                                                                                       "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                                                       "HeadingRadians":  0.83333333333333237,
-                                                                                       "UnwrappedHeadingRadians":  0.83333333333333237,
-                                                                                       "Source":  4
-                                                                                   }
-                                                                               ]
-                                                       },
-                                  "RecordKind":  "Canonical",
-                                  "CandidateIndex":  5,
-                                  "CanonicalCandidateIndex":  5,
-                                  "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed2",
-                                  "EquivalenceParameters":  [
-                                                                0,
-                                                                0.25,
-                                                                0.5,
-                                                                0.75,
-                                                                1
-                                                            ],
-                                  "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-                                  "EvaluatorExecutedDirectly":  true,
-                                  "EvaluatorResult":  "InsufficientClearance",
-                                  "StopGate":  "Clearance",
-                                  "MeasurementTrace":  {
-                                                           "CandidateIndex":  5,
-                                                           "RawRangeExceeded":  true,
-                                                           "CollisionAndMapBoundaryPassed":  true,
-                                                           "ExecutedGates":  [
-                                                                                 "InputAnalysis",
-                                                                                 "VehicleCurvature",
-                                                                                 "RawCurvatureRange",
-                                                                                 "Deviation",
-                                                                                 "FullPathAnalysis",
-                                                                                 "CollisionAndMapBoundary",
-                                                                                 "Clearance"
-                                                                             ],
-                                                           "PassedGates":  [
-                                                                               "InputAnalysis",
-                                                                               "VehicleCurvature",
-                                                                               "Deviation",
-                                                                               "FullPathAnalysis",
-                                                                               "CollisionAndMapBoundary"
-                                                                           ],
-                                                           "StopGate":  "Clearance",
-                                                           "Result":  "InsufficientClearance",
-                                                           "Reason":  "局部 G2 候选净空不足。",
-                                                           "RawMinimumCurvature":  8.21565038222615E-14,
-                                                           "RawMaximumCurvature":  0.41667044179866869,
-                                                           "CandidateMinimumCurvature":  -0.035866223552879793,
-                                                           "CandidateMaximumCurvature":  0.41190753089391469,
-                                                           "Eminus":  0.03586622355296195,
-                                                           "Eplus":  0,
-                                                           "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                                           "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  0.41190753089391469,
-                                                           "MaximumDeviationMeters":  6.2208065057631088E-05,
-                                                           "MaximumDeviationLimitMeters":  0.1,
-                                                           "MinimumBodyClearanceMeters":  "not evaluated",
-                                                           "MinimumClearanceReserveMeters":  0.02,
-                                                           "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-                                                           "MinimumPeakGradientImprovementRatio":  "not evaluated",
-                                                           "RawCurvatureVariationCost":  "not evaluated",
-                                                           "CandidateCurvatureVariationCost":  "not evaluated",
-                                                           "MaximumVariationCostRegressionRatio":  "not evaluated",
-                                                           "AbsolutePathLengthChangeMeters":  "not evaluated"
-                                                       },
-                                  "EvaluatorElapsedMilliseconds":  1.0266,
-                                  "RawMinimumCurvature":  8.21565038222615E-14,
-                                  "RawMaximumCurvature":  0.41667044179866869,
-                                  "CandidateMinimumCurvature":  -0.035866223552879793,
-                                  "CandidateMaximumCurvature":  0.41190753089391469,
-                                  "DelayedCGenRangeSnapshot":  "not evaluated",
-                                  "AbsoluteCurvatureJumpFloorPerMeter":  0.001,
-                                  "MaximumDeviationLimitMeters":  0.1,
-                                  "MinimumPeakGradientImprovementRatio":  0.2,
-                                  "MaximumVariationCostRegressionRatio":  0.02,
-                                  "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-                                  "DirectionSign":  1,
-                                  "HardNodes":  [
-                                                    {
-                                                        "X":  2.7216397035494579,
-                                                        "Y":  1.7279184433567971,
-                                                        "ReferenceArcLengthMeters":  1.9199999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.799999999999999,
-                                                        "GeometricCurvaturePerMeter":  0.41666666666666663,
-                                                        "VehicleCurvaturePerMeter":  0.41666666666666663
-                                                    },
-                                                    {
-                                                        "X":  2.7764244476704873,
-                                                        "Y":  1.786210614200662,
-                                                        "ReferenceArcLengthMeters":  1.9999999999999982,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0.24999999999999997,
-                                                        "VehicleCurvaturePerMeter":  0.24999999999999997
-                                                    },
-                                                    {
-                                                        "X":  2.8571139169604551,
-                                                        "Y":  1.8750318365841865,
-                                                        "ReferenceArcLengthMeters":  2.1199999999999983,
-                                                        "TravelUnwrappedHeadingRadians":  0.83333333333333237,
-                                                        "GeometricCurvaturePerMeter":  0,
-                                                        "VehicleCurvaturePerMeter":  0
-                                                    }
-                                                ],
-                                  "Intervals":  [
-                                                    {
-                                                        "IntervalIndex":  0,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.079999924153600924,
-                                                        "GeometricC0":  0.41666666666666663,
-                                                        "GeometricC1":  0.18490902131056236,
-                                                        "GeometricC2":  0.81509255882424947,
-                                                        "GeometricC3":  0.24999999999999997,
-                                                        "VehicleC0":  0.41666666666666663,
-                                                        "VehicleC1":  0.18490902131056236,
-                                                        "VehicleC2":  0.81509255882424947,
-                                                        "VehicleC3":  0.24999999999999997,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  744,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1627,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1627,
-                                                        "PreOverwritePositionErrorMeters":  1.147815539419054E-11,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  0.25000000000000011,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.49287523451513321,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  21.190991046663896
-                                                    },
-                                                    {
-                                                        "IntervalIndex":  1,
-                                                        "Accepted":  true,
-                                                        "Failure":  "None",
-                                                        "Reason":  "",
-                                                        "LengthMeters":  0.12000008571444666,
-                                                        "GeometricC0":  0.24999999999999997,
-                                                        "GeometricC1":  -0.49999997893024717,
-                                                        "GeometricC2":  0.2499999789302472,
-                                                        "GeometricC3":  0,
-                                                        "VehicleC0":  0.24999999999999997,
-                                                        "VehicleC1":  -0.49999997893024717,
-                                                        "VehicleC2":  0.2499999789302472,
-                                                        "VehicleC3":  0,
-                                                        "OuterIterationCount":  6,
-                                                        "DampingTrialCount":  6,
-                                                        "IntegrationEvaluationCount":  648,
-                                                        "MaximumIntegrationDepth":  0,
-                                                        "SolverElapsedStopwatchTicks":  1426,
-                                                        "StopwatchFrequency":  10000000,
-                                                        "SolverElapsedMilliseconds":  0.1426,
-                                                        "PreOverwritePositionErrorMeters":  1.6841317892909525E-12,
-                                                        "AnalyticHeadingErrorRadians":  0,
-                                                        "AnalyticMinimumCurvaturePerMeter":  -0.093484688087432849,
-                                                        "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                                                        "AnalyticMaximumGradientPerSquareMeter":  18.749986080383835
-                                                    }
-                                                ],
-                                  "CandidateDiagnostic":  {
-                                                              "WindowIndex":  5,
-                                                              "SeedIndex":  2,
-                                                              "CombinationAttemptIndex":  18,
-                                                              "SolverInvocationCount":  24,
-                                                              "MaximumPreOverwritePositionErrorMeters":  1.147815539419054E-11,
-                                                              "MaximumPostOverwritePositionErrorMeters":  0,
-                                                              "MaximumTangentSeamErrorRadians":  0,
-                                                              "MaximumCurvatureSeamErrorPerMeter":  0,
-                                                              "MinimumAdjacentPointDistanceMeters":  0.01999990230624905
-                                                          }
-                              },
-        "SourceIdentity":  {
-                               "SharedBaselineCommit":  "da9157b17a7e4206ed4cdebd1985ee2577fae480",
-                               "FrozenCoreCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "StrictRootCommit":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementRootCommit":  "c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825",
-                               "StrictRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\strict-root-3006aa0",
-                               "MeasurementRootPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\measurement-root-3006aa0",
-                               "FixtureSha256":  "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
-                               "SharedBaselineEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "StrictEvaluatorSha256":  "929c24eb7acf4da621a397173375987801bd9e44d98193c173793d3a1ccf1aa3",
-                               "MeasurementEvaluatorSha256":  "ab2846dd32b7dd8ffb4464d567cb464d12f74b5b8f93c86ba9ff8d5daff7511e",
-                               "MeasurementCommitParent":  "3006aa0fd8dac8e4665ff3b89b7cda139e88e940",
-                               "MeasurementCommitChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "MeasurementCommitHistoryProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-commit-history-proof.json",
-                               "MeasurementCommitHistoryProofSha256":  "b3a3a7656ac9bcc638f881a968b09dc050462c4627f03b579ed62e62783692a0",
-                               "ProbeSourceManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea",
-                               "AllowedChangedPaths":  "ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs",
-                               "StrictAllowedDifferenceSummary":  "Complete strict-root source diff from the shared baseline.",
-                               "StrictAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictAllowedDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "StrictSharedBaselineDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-baseline.diff",
-                               "StrictSharedBaselineDifferenceSha256":  "188ec729925393428c7a6a58aabd128244a4bb1d3f47c2cec552d65a4ae11982",
-                               "MeasurementAllowedDifferenceSummary":  "Only the measurement evaluator raw-range branch and trace capture differ.",
-                               "MeasurementAllowedDifferencePath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\measurement-evaluator.diff",
-                               "MeasurementEvaluatorDiffSha256":  "d95e7c1007208e0647ccbb86771fb2e13e5a45f1c8b79ade2e2a3527762605fd",
-                               "StrictEvaluatorHashProofPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\strict-vs-shared-evaluator-hash-proof.json",
-                               "StrictEvaluatorHashProofSha256":  "a0869c7b26f8e203dca2361dd39169d60f0a8fd3c9dd58c9697aed7abd5c94e7",
-                               "FrozenCoreJsonPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core.json",
-                               "FrozenCoreJsonSha256":  "08ba3ccbf7fcc3d6d34a83d98f8adcec169f58280d3f3ae884602c489823ccfa",
-                               "FrozenCoreManifestPath":  "C:\\Users\\admin\\AppData\\Local\\Temp\\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\\evidence\\frozen-core-probe-source-manifest.txt",
-                               "FrozenCoreManifestSha256":  "9e2129a80db9bb8cff44ef4b4b5079c4e9fc7c4d3b65ab5a369c4197bdb9a8ea"
-                           },
-        "RecordKind":  "Canonical",
-        "CanonicalCandidateIndex":  5,
-        "EvaluationSourceStableKey":  "single-turn/s0/r0/w5/seed2",
-        "EquivalenceParameters":  [
-                                      0,
-                                      0.25,
-                                      0.5,
-                                      0.75,
-                                      1
-                                  ],
-        "MaximumEquivalenceCoordinateDifferenceMeters":  0,
-        "EvaluatorExecutedDirectly":  true,
-        "StrictResult":  "CurvatureOvershoot",
-        "StrictStopGate":  "RawCurvatureRange",
-        "MeasurementResult":  "InsufficientClearance",
-        "MeasurementStopGate":  "Clearance",
-        "MeasurementReason":  "局部 G2 候选净空不足。",
-        "MeasurementExecutedGates":  [
-                                         "InputAnalysis",
-                                         "VehicleCurvature",
-                                         "RawCurvatureRange",
-                                         "Deviation",
-                                         "FullPathAnalysis",
-                                         "CollisionAndMapBoundary",
-                                         "Clearance"
-                                     ],
-        "MeasurementPassedGates":  [
-                                       "InputAnalysis",
-                                       "VehicleCurvature",
-                                       "Deviation",
-                                       "FullPathAnalysis",
-                                       "CollisionAndMapBoundary"
-                                   ],
-        "RawRangeExceeded":  true,
-        "RawMinimumCurvature":  8.21565038222615E-14,
-        "RawMaximumCurvature":  0.41667044179866869,
-        "CandidateMinimumCurvature":  -0.035866223552879793,
-        "CandidateMaximumCurvature":  0.41190753089391469,
-        "HardPhysicalQualified":  true,
-        "Eminus":  0.03586622355296195,
-        "Eplus":  0,
-        "Rminus":  0.08607815663175658,
-        "Rplus":  0,
-        "VehicleMaximumCurvatureLimitPerMeter":  0.83333333333333337,
-        "CandidateMaximumAbsoluteVehicleCurvaturePerMeter":  0.41190753089391469,
-        "AnalyticVehicleCurvatureUtilization":  0.97811107058909919,
-        "DiscreteVehicleCurvatureUtilization":  0.49428903707269761,
-        "MaximumDeviationMeters":  6.2208065057631088E-05,
-        "MaximumDeviationLimitMeters":  0.1,
-        "CollisionAndMapBoundaryPassed":  true,
-        "MinimumBodyClearanceMeters":  "not evaluated",
-        "MinimumClearanceReserveMeters":  0.02,
-        "RawPeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "CandidatePeakCurvatureDerivativePerSquareMeter":  "not evaluated",
-        "MinimumPeakGradientImprovementRatio":  "not evaluated",
-        "PeakGradientImprovementPercent":  "not evaluated",
-        "RawCurvatureVariationCost":  "not evaluated",
-        "CandidateCurvatureVariationCost":  "not evaluated",
-        "MaximumVariationCostRegressionRatio":  "not evaluated",
-        "VariationCostRegressionPercent":  "not evaluated",
-        "AbsolutePathLengthChangeMeters":  "not evaluated",
-        "StrictEvaluatorElapsedMilliseconds":  0.105,
-        "MeasurementEvaluatorElapsedMilliseconds":  1.0266,
-        "StrictPipelineStatus":  "Unchanged",
-        "MeasurementPipelineStatus":  "Unchanged",
-        "Intervals":  [
-                          {
-                              "IntervalIndex":  0,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.079999924153600924,
-                              "GeometricC0":  0.41666666666666663,
-                              "GeometricC1":  0.18490902131056236,
-                              "GeometricC2":  0.81509255882424947,
-                              "GeometricC3":  0.24999999999999997,
-                              "VehicleC0":  0.41666666666666663,
-                              "VehicleC1":  0.18490902131056236,
-                              "VehicleC2":  0.81509255882424947,
-                              "VehicleC3":  0.24999999999999997,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  744,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  1.147815539419054E-11,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  0.25000000000000011,
-                              "AnalyticMaximumCurvaturePerMeter":  0.49287523451513321,
-                              "AnalyticMaximumGradientPerSquareMeter":  21.190991046663896
-                          },
-                          {
-                              "IntervalIndex":  1,
-                              "Accepted":  true,
-                              "Failure":  "None",
-                              "Reason":  "",
-                              "LengthMeters":  0.12000008571444666,
-                              "GeometricC0":  0.24999999999999997,
-                              "GeometricC1":  -0.49999997893024717,
-                              "GeometricC2":  0.2499999789302472,
-                              "GeometricC3":  0,
-                              "VehicleC0":  0.24999999999999997,
-                              "VehicleC1":  -0.49999997893024717,
-                              "VehicleC2":  0.2499999789302472,
-                              "VehicleC3":  0,
-                              "OuterIterationCount":  6,
-                              "DampingTrialCount":  6,
-                              "IntegrationEvaluationCount":  648,
-                              "MaximumIntegrationDepth":  0,
-                              "PreOverwritePositionErrorMeters":  1.6841317892909525E-12,
-                              "AnalyticHeadingErrorRadians":  0,
-                              "AnalyticMinimumCurvaturePerMeter":  -0.093484688087432849,
-                              "AnalyticMaximumCurvaturePerMeter":  0.24999999999999997,
-                              "AnalyticMaximumGradientPerSquareMeter":  18.749986080383835
-                          }
-                      ]
-    }
-]
-```
-
-## Performance And Determinism
-
-- Protocol: `5` warmups and `30` measured runs; unique signatures: `1`.
-- Solver median/worst: `4.9354500000000012` / `7.7368` ms (limits `10/25`).
-- Region median/worst: `10.09185` / `23.2227` ms (limits `25/50`).
-- Performance exceeded: `False`.
-
-## Pipeline And Environment
-
-- Strict pipeline/status key: `Unchanged` / `not evaluated`.
-- Measurement pipeline/status key: `Unchanged` / `not evaluated`.
-```json
-{
-    "Expected":  {
-                     "MachineName":  "DESKTOP-428CCNK",
-                     "CpuName":  "11th Gen Intel Core i5-1135G7 @ 2.40 GHz",
-                     "LogicalProcessorCount":  8,
-                     "TotalMemoryBytes":  16863318016,
-                     "OsDescription":  "Windows 10 Pro 10.0.19045",
-                     "PowerShellVersion":  "5.1.19041.7548",
-                     "DotNetSdkVersion":  "10.0.302",
-                     "BuildConfiguration":  "Debug",
-                     "TargetFramework":  "netstandard2.0"
-                 },
-    "Observed":  {
-                     "MachineName":  "DESKTOP-428CCNK",
-                     "CpuName":  "11th Gen Intel Core i5-1135G7 @ 2.40 GHz",
-                     "LogicalProcessorCount":  8,
-                     "TotalMemoryBytes":  16863318016,
-                     "OsDescription":  "Windows 10 Pro 10.0.19045",
-                     "PowerShellVersion":  "5.1.19041.7548",
-                     "DotNetSdkVersion":  "10.0.302",
-                     "BuildConfiguration":  "Debug",
-                     "TargetFramework":  "netstandard2.0"
-                 },
-    "FinalGateDiagnosticTimings":  {
-                                       "StrictChildElapsedMilliseconds":  13547.689,
-                                       "MeasurementChildElapsedMilliseconds":  25848.4503,
-                                       "PairingElapsedMilliseconds":  3993.6412,
-                                       "TwoRootTotalElapsedMilliseconds":  87721.701
-                                   }
-}
-```
-
-## Decision Trace
-
-- Invalid evidence: `False`; reasons: ``
-- Ordered resolution: invalid evidence -> performance -> strict -> physical -> quality -> excursion.
-- Result: `QUALITY_RED`.
-
-## Evidence Paths
-
-- Strict candidate audit JSON: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\strict-audit.json`
-- Measurement 5+30 candidate/timing JSON: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\measurement-batch.json`
-- Strict-vs-shared evaluator hash proof: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\strict-vs-shared-evaluator-hash-proof.json`
-- Strict-vs-shared-baseline complete diff: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\strict-vs-shared-baseline.diff`
-- Measurement evaluator commit `c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825` and diff: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\measurement-evaluator.diff`
-- Measurement commit history proof: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\measurement-commit-history-proof.json`
-- Frozen Task 3 JSON: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\frozen-core.json`
-- Frozen Task 3 canonical manifest: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\frozen-core-probe-source-manifest.txt`
-- Strict child canonical manifest: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\strict-audit-probe-source-manifest.txt`
-- Measurement child canonical manifest: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\measurement-batch-probe-source-manifest.txt`
-- 30-run timing JSON: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\timing-30-runs.json`
-- State decision trace: `C:\Users\admin\AppData\Local\Temp\local-g2-excursion-15d2c64ed6fd48159b6da4afff0b748f\evidence\state-decision-trace.json`
-
-The measurement root changes only the raw-range early-return behavior and
-non-behavioral trace capture. No probe source is authorized for production.
diff --git a/docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md b/docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md
deleted file mode 100644
index 7b6bf8b..0000000
--- a/docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md
+++ /dev/null
@@ -1,111 +0,0 @@
-# TrapMap完整图片导出与终端日志开关设计
-
-## 目标
-
-为`MovementTest.Trapmaptest.cs`增加两个相互独立的测试开关:
-
-- 成功建图后,将完整栅格地图独立渲染为300 DPI PNG,不依赖Clumsy当前视口、缩放或其他Painter。
-- 控制TrapMap调试信息是否同步打印到宿主进程终端,同时始终保留`DLog`日志。
-
-现有`TrapMapTest` Painter初始化和清理已经使用世界坐标调用`UI.GetPainter("TrapMapTest")`,本次不重复修改。两腿检测ROI继续使用车体局部Painter及`false`参数。
-
-## 用户开关与默认值
-
-在`TrapMapTest`手动编辑区增加:
-
-```csharp
-private const bool _saveFullMapImage = true;
-private const bool _enableTerminalDebugLog = true;
-```
-
-两个值传入`TrapMapBuilder`或共享日志/导出组件。关闭图片开关时不得创建目录、临时文件或PNG。关闭终端开关时只抑制`Console.WriteLine`,不得抑制`DLog`和必要的UI提示。
-
-## 图片内容
-
-图片从最终发布的`GridMapData`离屏渲染,包含完整地图边界而不是屏幕截图:
-
-- 白色背景。
-- 浅灰色完整栅格线,确保每个小格可见。
-- 红色占用栅格。
-- 蓝色车辆轮廓、几何中心和朝向。
-- 绿色工作站目标标记。
-- 黑色地图外边界。
-- 标题/图例区域,显示世界边界、分辨率、行列数、占据率、障碍物数量、轮胎层状态和输入来源。
-
-世界X轴在图片中向右;世界Y轴向上,因此从`Cells[row,col]`映射到位图时反转图像Y方向。工作站仅绘制标记,不写入占用数据。
-
-## 图片尺寸与文件约束
-
-- 每个栅格使用`4×4`像素。
-- PNG水平和垂直DPI都设置为`300`。
-- 包含边距和标题后,任一图片边长不得超过`4000`像素。
-- 最终PNG文件大小不得超过`50 * 1024 * 1024`字节。
-
-尺寸在分配RGBA像素缓冲区前检查。若超过4000像素,跳过导出并报告明确原因。编码先写入同目录临时文件,完成后检查实际字节数;超过50MB时删除临时文件,不留下超限最终文件。只有所有检查通过后,才原子移动/重命名为最终PNG。
-
-典型`327×139`地图的栅格主体约为`1308×556`像素,另加标题和边距。
-
-## 保存位置与命名
-
-输出根目录使用宿主进程当前工作目录:
-
-```text
-TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png
-```
-
-毫秒时间戳避免同一秒多次测试覆盖。目录只在图片开关打开且地图成功后创建。临时文件使用同目录、同文件名加`.tmp`后缀,以保证最终重命名不跨磁盘。
-
-## 日志行为
-
-引入TrapMap专用日志入口,其行为为:
-
-```text
-所有消息 -> DLog.Log(message, "TrapMapTest")
-终端开关开启 -> 额外Console.WriteLine("[TrapMapTest] " + message)
-```
-
-至少覆盖测试开始、输入参数、Detour位姿、车辆尺寸、地图边界/尺寸、轮胎层状态、图片保存成功/跳过/失败、最终统计和测试停止。图片错误不得因终端开关关闭而静默,仍必须进入`DLog`。
-
-## 组件边界
-
-图片导出放在独立文件`ClumsyPilot/TrapMapImageExporter.cs`,避免继续扩大已经较长的MovementTest文件。组件只消费不可变的导出请求数据:地图、车辆位姿、工作站、轮胎层元数据和目标文件路径;它不读取Detour、雷达或UI,也不修改栅格。
-
-`MovementTest.Trapmaptest.cs`负责开关、调用时机、日志和错误降级。导出发生在地图成功生成之后;导出失败不改变`TrapMapBuilder.Succeeded`或`GridMap`。
-
-实现使用内部纯C# RGBA光栅器绘制栅格、车辆、工作站和5×7位图文字,再由精确版本`StbImageWriteSharp` 1.16.7编码PNG。编码后立即在`IHDR`后插入`pHYs=11811/11811/unit1`,以保留300 DPI元数据。运行时不依赖平台绘图程序集或原生图形资产;除单个托管Stb编码程序集外,光栅、元数据和文件流程均只使用BCL。
-
-## 错误处理
-
-以下情况只导致图片导出失败,不导致建图失败:
-
-- 图片开关关闭。
-- 图片尺寸超过4000像素。
-- 输出目录创建失败。
-- RGBA缓冲区创建、绘制或PNG编码异常。
-- 临时文件超过50MB。
-- 临时文件重命名失败。
-
-异常路径必须尽力删除本次临时文件,不得删除已有的成功PNG。
-
-## 验证要求
-
-至少验证:
-
-1. 图片开关关闭时不创建文件和目录。
-2. 小型已知栅格导出的PNG由BCL测试解码器重新读取;所有chunk CRC有效,`IHDR`为RGBA8,`pHYs`表示300 DPI,像素尺寸符合4像素/格及布局规则。
-3. PNG中占用格、车辆和工作站采样位置颜色正确,Y轴没有上下颠倒。
-4. 超过4000像素的请求在RGBA缓冲区分配前失败。
-5. 最终路径使用毫秒时间戳且不覆盖旧文件。
-6. 成功文件严格小于或等于50MB,超限临时文件被删除。
-7. 终端开关开启时消息同时进入DLog和终端;关闭时仍进入DLog但不写终端。
-8. 图片失败时地图仍为成功状态。
-9. 现有源码契约、栅格行为、生命周期测试及`ClumsyPilot`编译继续通过。
-
-## 非目标
-
-- 不截取Clumsy/CycleGUI窗口。
-- 不保存紫色UI背景、小车3D模型、绿色两腿ROI或橙色检测猜测线。
-- 不改变栅格数据格式或地图边界计算。
-- 不接入新的点云来源。
-- 不修改`TrajPlanner`。
-- 不提交或暂存本次工作区改动。
diff --git a/docs/superpowers/specs/2026-07-22-trap-map-layered-construction-design.md b/docs/superpowers/specs/2026-07-22-trap-map-layered-construction-design.md
deleted file mode 100644
index 51c7675..0000000
--- a/docs/superpowers/specs/2026-07-22-trap-map-layered-construction-design.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# TrapMap 分层建图修订设计
-
-## 目标
-
-修正 `MovementTest.Trapmaptest.cs` 中“必须先检测到轮胎才能建立地图”的依赖倒置,并将随车固定窗口改为任务级世界坐标栅格。基础地图只依赖合法的车辆位姿、车辆尺寸、工作站世界坐标和地图参数;轮胎检测是可选障碍物输入,不再决定地图是否存在。
-
-本次仍是一次性地图测试,不扩展 `TrajPlanner`,也不引入尚不存在的通用障碍物点云接口。该地图覆盖当前车辆到目标工作站的任务范围,不宣称覆盖整个停车场的永久 SLAM 全局地图。
-
-## 地图范围
-
-目标工作站使用与 `DetourInterface.getCartLocation()` 完全相同的绝对世界坐标系,单位为 mm。第一版默认可调参数为:
-
-- `WorkstationX = 10000 mm`
-- `WorkstationY = 0 mm`
-- `MapMarginX = 3000 mm`
-- `MapMarginY = 3000 mm`
-
-读取车辆几何中心 `(CarX, CarY)` 后,地图边界按以下方式计算:
-
-```text
-XMin = min(CarX, WorkstationX) - MapMarginX
-XMax = max(CarX, WorkstationX) + MapMarginX
-YMin = min(CarY, WorkstationY) - MapMarginY
-YMax = max(CarY, WorkstationY) + MapMarginY
-```
-
-工作站坐标和 X/Y 冗余必须可配置,不能继续用一个随车半边长参数控制地图。边界采用世界坐标轴对齐矩形,保证车辆点、工作站点及四周冗余都在地图内。
-
-实现必须校验坐标、冗余、计算后的边界、行列数与总栅格数量,拒绝非有限值、负冗余、退化边界和可能造成异常内存分配的超大地图。
-
-## 分层模型
-
-地图按职责分为以下逻辑层:
-
-1. 基础层:按车辆—工作站包围矩形及 X/Y 冗余创建任务级世界坐标栅格,当前所有单元初始化为 `Free`。
-2. 自车层:按照车辆长度、宽度、朝向标记本车占用区域。
-3. 目标层:记录并可视化工作站位置;目标点不是障碍物,不写入占用栅格。
-4. 轮胎层:调用现有 `TwoLegDetect`,成功时将两个端点转换至世界坐标并膨胀为占用栅格。
-5. 点云障碍层:仅作为未来扩展边界,本次不实现、不虚构输入来源。
-
-当前 `Free` 的准确含义是“此栅格没有被本次已接入的占用来源标记”,并不表示传感器已经确认该区域不存在其他障碍物。代码注释必须明确这一限制,以便未来引入点云及 `Unknown` 状态。
-
-## 数据流与执行顺序
-
-1. 校验地图分辨率、工作站坐标、X/Y 冗余及车辆尺寸。
-2. 从 `DetourInterface.getCartLocation()` 读取车辆世界位姿。
-3. 根据车辆点和工作站点计算包围矩形,校验最终尺寸和栅格总量。
-4. 立即创建任务级世界坐标栅格地图。
-5. 标记自车占用层并记录工作站目标层。
-6. 尝试读取轮胎检测结果:
-   - 检测成功:转换并标记两个轮胎端点。
-   - 未检测到目标:轮胎层保持为空,地图仍然成功。
-   - 配置缺失、检测异常或坐标非法:轮胎层保持为空,地图仍然成功,但记录降级状态和具体原因。
-7. 可视化并发布地图,同时输出边界、工作站、各层状态和统计信息。
-
-地图的创建和发布不再等待轮胎输入。轮胎检测只影响轮胎层内容和传感器状态。
-
-## 状态与错误语义
-
-以下情况属于建图失败,不能发布地图:
-
-- 地图分辨率、范围或安全距离非法。
-- 工作站世界坐标或 X/Y 冗余非法。
-- 计算后的行列数或总栅格数量超过安全上限。
-- 车辆长度或宽度非法。
-- 车辆世界位姿不是有限值。
-- 基础地图创建、必要的自车标记或结果发布失败。
-
-以下情况属于轮胎层降级,不导致建图失败:
-
-- 未配置轮胎检测雷达。
-- `TwoLegDetect` 没有检测到目标。
-- 检测调用抛出异常。
-- 返回的端点或转换后的世界坐标非法。
-
-输出应区分:
-
-- `Succeeded`:基础地图是否成功生成。
-- `InputSource`:本次实际启用的障碍来源。
-- 轮胎层状态:已标记、未检测到或检测异常。
-- 轮胎层状态说明:未标记或异常的具体原因。
-
-“未检测到目标”与“检测器异常”都产生空轮胎层,但日志语义不能混同:前者表示本次没有轮胎投影,后者表示轮胎信息不可用。
-
-## 数据结构边界
-
-本次可以继续使用最终合并后的 `GridMapData.Cells`,无需提前实现完整的多层栅格容器。分层通过独立的填充步骤、来源状态和统计实现,避免为尚未接入的点云设计过度复杂的数据模型。
-
-未来点云接入时,应新增独立的点云转换/栅格化步骤,再合并到最终占用结果;不得把原始点云读取逻辑塞入轮胎检测函数。
-
-## 测试要求
-
-至少覆盖以下行为:
-
-1. 位姿、工作站和地图参数合法、轮胎检测无目标时,地图成功且轮胎障碍数为零。
-2. 轮胎检测配置缺失或调用异常时,地图仍成功,并记录降级原因。
-3. 轮胎检测成功时,两个端点被转换并投影为占用栅格。
-4. 边界严格由车辆点、工作站点及各轴冗余计算,并同时包含两个点。
-5. 工作站可视化但不增加占用栅格数量。
-6. 位姿、工作站、冗余、地图参数或总栅格数量非法时,地图失败且不发布半成品。
-7. 源码契约确认建图发生在轮胎检测之前,且不存在实车模式模拟障碍回退。
-8. 注释明确当前 `Free` 不等于传感器确认无障碍,并标明未来点云/`Unknown` 扩展方向。
-
-## 非目标
-
-- 不实现持续更新或历史地图融合。
-- 不读取或声称生成完整停车场 SLAM 静态地图;本次“全局”仅指车辆和工作站共享的世界坐标任务范围。
-- 不接入未经现有代码验证的点云 API。
-- 不修改或展开 `TrajPlanner`。
-- 不发送车辆运动或停止指令。
diff --git a/docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md b/docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md
deleted file mode 100644
index 905228b..0000000
--- a/docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# TrapMap Managed PNG Export Design
-
-## Goal
-
-Replace `System.Drawing.Common` in TrapMap image export so Clumsy can save the full grid PNG without depending on platform-specific drawing assemblies.
-
-## Chosen approach
-
-Use a small internal RGBA rasterizer for grid primitives and `StbImageWriteSharp` 1.16.7 for PNG encoding. Keep the public request/result API and the existing TrapMap call site unchanged.
-
-Alternatives rejected:
-
-- Copying `System.Drawing.Common.dll` beside the build output is unreliable because Clumsy performs its own dependency attachment and can load the incompatible `netstandard2.0` facade first.
-- `SkiaSharp` adds native Windows assets and more deployment points.
-- `ImageSharp` has a larger dependency/licensing surface and its current release does not target this project's `netstandard2.0` runtime.
-
-`StbImageWriteSharp` is selected because its package contains one managed `netstandard2.0` implementation and declares no dependencies. It has no native assets or alternate platform facades for Clumsy to select incorrectly.
-
-## Rasterization
-
-- Allocate an in-memory 32-bit RGBA pixel buffer after the existing 4000-pixel edge validation.
-- Preserve the current canvas dimensions, 4 pixels per cell, colors, Y inversion, vehicle/workstation geometry, header height, and 50 MiB final-file limit.
-- Draw filled rectangles, 1/2-pixel lines, circles, crosses, and vehicle polygons with deterministic integer raster operations.
-- Render header and workstation text with a small embedded ASCII bitmap font. Non-ASCII metadata is sanitized to a printable fallback for the PNG header only; original messages remain unchanged in DLog/Console.
-- Keep header lines non-overlapping and include bounds, resolution, rows/columns, occupied count/rate, obstacle count, tire status/message, and input source.
-
-## PNG encoding
-
-- Pass the RGBA pixel buffer to `StbImageWriteSharp.ImageWriter.WritePng`.
-- Insert a standard `pHYs` chunk immediately after `IHDR`, with X/Y both 11,811 pixels per metre and unit `1` (300 DPI).
-- Calculate the inserted chunk's CRC-32 and write its integers in big-endian order.
-- Validate the completed PNG before publication; encoding or metadata failures remain contained export failures.
-- Preserve the current collision-safe temporary-file reservation, actual encoded-size check, atomic move, and best-effort cleanup behavior.
-
-## Project and deployment changes
-
-- Remove the `System.Drawing.Common` package reference and `DeployFrameworkDrawingRuntime` target from `ClumsyPilot.csproj`.
-- Add `StbImageWriteSharp` version 1.16.7. No other new package is allowed.
-- The final build output must not require or deploy `System.Drawing.Common.dll` for TrapMap; it may deploy the single managed `StbImageWriteSharp.dll`.
-- Existing Clumsy references are not modified.
-
-## Verification
-
-- TDD first proves the current exporter/package still depends on `System.Drawing.Common`.
-- Reflection/source contracts assert the old package, build target, `using System.Drawing`, and drawing types are absent, and the exact Stb package is present.
-- Decode the generated PNG in the test with a test-only decoder or framework reader and verify dimensions, 300 DPI metadata, representative colors, Y inversion, header separation, unique concurrent filenames, and no temporary files.
-- Run source, build, grid, lifecycle, and image tests; require zero build errors and no new warnings.
-
-## Non-goals
-
-- No changes to grid construction, obstacle/tire inputs, vehicle motion, Painter visualization, export switches, output directory, or file-size/pixel limits.
-- No screenshot capture and no new general-purpose graphics framework.
diff --git a/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md b/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md
deleted file mode 100644
index ee0d19b..0000000
--- a/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md
+++ /dev/null
@@ -1,544 +0,0 @@
-# Hybrid A* 粗路径规划设计
-
-## 目标与范围
-
-在现有栅格地图能力之上,实现四舵轮 AMR 的 Hybrid A* 空间粗路径:支持前进、倒车、换向、静态/投影障碍绕行、车体碰撞检查、可配置的终点位置与航向容差,以及稠密路径与方向分段输出。
-
-本阶段**不包含**曲线平滑、B 样条、Bezier、局部 QP、SQP、Reeds-Shepp 精确终点连接、速度/加速度/时间参数化或底盘舵角控制。第一版采用汽车式恒曲率模型,不支持蟹行、纯横移和原地旋转。它们由后续独立模块处理。
-
-## 已确认的约束
-
-- 地图与外部人工障碍输入继续使用现有世界坐标单位:mm。
-- Hybrid A* 内部统一使用 m、rad、1/m;单位转换只能经过地图适配与 `Utils`,不能散落在搜索代码中。
-- 人工障碍物第一版支持以几何中心放置的轴对齐矩形和圆形。
-- `TwoLegProjectionInput` 是可选障碍物输入,不是规划地图是否可用的唯一条件;上层采集到的 `TwoLegDetect` 结果经 DTO 投影后与人工障碍物合并。
-- 环境占据地图只保存外部障碍物,绝不写入 AMR 自身足迹。AMR 尺寸、当前位姿和安全余量只用于粗路径的车体碰撞检测。
-- 障碍物不做安全膨胀;安全余量仅通过碰撞检查时的扩大车辆矩形应用,防止双重膨胀。
-- 第一版的终点条件为可配置的位置容差和车头航向容差。默认建议为 0.15 m 与 5°,而非精确连接到目标位姿。
-- `PrimitiveLength=0.50 m` 是单条原语的最大长度,不是终点只能出现的离散间隔。每条原语必须在内部积分点检查目标条件;该原语内首次满足条件时立即截断并建立终点候选节点。终点候选仍须进入 Open List,只有当它作为当前最优有效节点出队时,整个搜索才返回成功。
-- 碰撞检测必须保守:不得因航向离散、车辆中心的亚栅格偏移、距离场误差或原语离散采样而发布可能碰撞的路径。
-- `ObstacleDistanceField` 只能作为保守快速放行和代价估计,任何可能高估真实净空的近似都不得用于跳过精确车体碰撞检查。
-- 测试是交付的一部分:纯逻辑契约测试与参照 `Map` 的 `MovementTest` 集成/可视化入口都必须提供。
-- 每个文件只承担一个明确职责;对外调用通过模块门面类完成,不让调用者拼装搜索、地图和碰撞的内部对象。
-- 推荐调用方只使用 `CoarsePathPlanningService` 一次完成建图、粗路径搜索和可选调试发布;`PlanningMapFactory` 与 `HybridAStarPlanner` 是可独立测试、复用的下层模块门面。请求、结果、枚举、障碍物 DTO 和值对象仍然是可公开构造的数据契约。
-
-## 目录和命名空间
-
-```text
-ClumsyPilot/ParkrobTrajplanner/
-├── Initial_plan/                         ------ 已有路线与方案文档,不放运行时代码
-├── Utils/                                ------ 命名空间 MultiWheelC.TrajectoryPlanning.Utils
-│   ├── AngleMath.cs                      ------ 角度归一化、最短角差和航向离散索引
-│   ├── UnitConverter.cs                  ------ mm/m、deg/rad 和半径/曲率单位转换
-│   ├── CoordinateTransform.cs            ------ 车体坐标系与世界坐标系二维刚体变换
-│   ├── NumericGuard.cs                   ------ 有限值、正值和参数范围校验
-│   └── GridIndex.cs                      ------ 不可变行列索引值对象
-├── Map/                                  ------ 命名空间 MultiWheelC.TrajectoryPlanning.Mapping
-│   ├── Core/
-│   │   ├── EnvironmentGridMap.cs         ------ 只保存外部障碍物的 mm 单位占据栅格
-│   │   ├── MapBoundsMm.cs                ------ 有限、非退化的 mm 地图边界值对象
-│   │   ├── MapBuildRequest.cs            ------ 环境地图边界、分辨率和障碍物图层输入
-│   │   ├── EnvironmentMapBuildResult.cs  ------ 环境地图构建状态、来源摘要和失败原因
-│   │   └── EnvironmentMapBuilder.cs      ------ 校验并合并人工与投影障碍物图层
-│   ├── Obstacles/
-│   │   ├── IMapObstacle.cs               ------ 人工和投影障碍物的公共几何契约
-│   │   ├── AxisAlignedRectangleObstacle.cs ------ 世界轴对齐矩形障碍物 DTO
-│   │   ├── CircleObstacle.cs             ------ 世界坐标圆形障碍物 DTO
-│   │   └── MapObstacleRasterizer.cs      ------ 通过形状与格子相交测试写入占据栅格
-│   ├── Sources/
-│   │   ├── IMapObstacleSource.cs         ------ 纯快照障碍来源统一投影接口
-│   │   ├── ObstacleSourceStatus.cs       ------ Applied、Empty、Unavailable、Invalid 来源状态
-│   │   ├── ObstacleProjectionResult.cs   ------ 来源版本、状态、诊断和世界障碍物集合
-│   │   ├── ManualObstacleSource.cs       ------ 把人工圆和矩形作为世界障碍物输出
-│   │   ├── TwoLegProjectionInput.cs      ------ 已验证两腿端点、检测状态和检测时车辆位姿 DTO
-│   │   ├── TwoLegObstacleSource.cs       ------ 把 TwoLeg 快照接入统一障碍来源接口
-│   │   └── TwoLegObstacleProjector.cs    ------ 把车体系两腿端点投影为世界坐标圆障碍
-│   ├── Planning/
-│   │   ├── PlanningGridMap.cs            ------ 只读 m 单位占据图、距离场和规划可用状态
-│   │   ├── EuclideanDistanceTransform.cs ------ 线性时间生成栅格中心精确欧氏距离
-│   │   ├── ObstacleDistanceField.cs      ------ 生成不高估障碍净空的保守欧氏距离下界
-│   │   ├── PlanningMapCache.cs           ------ 线程安全的输入指纹与占据哈希快照缓存
-│   │   └── PlanningMapAdapter.cs         ------ 深拷贝占据数据并完成 mm 到 m 的边界适配
-│   ├── PlanningMapRequest.cs             ------ 地图门面的统一输入契约
-│   ├── PlanningMapBuildResult.cs         ------ 地图门面的统一输出契约
-│   ├── PlanningMapFactory.cs             ------ Map 模块唯一公共行为入口
-│   └── Test/
-│       ├── MovementTest.MapTest.cs       ------ Clumsy UI 地图构建与可视化测试入口
-│       └── Visualization/
-│           ├── PlanningMapImageExportRequest.cs ------ 规划快照、叠加层和输出选项 DTO
-│           ├── PlanningMapImageExportResult.cs ------ PNG 导出状态、路径、大小和诊断
-│           ├── PlanningMapImageExporter.cs ------ 校验请求、编排渲染并原子发布 PNG
-│           ├── PlanningMapImageRenderer.cs ------ 把地图、起终点、车体和路径绘制到 RGBA
-│           └── ValidatedPngWriter.cs     ------ Stb PNG 编码、结构和 CRC 完整性校验
-├── CoarsePath/                           ------ 命名空间 MultiWheelC.TrajectoryPlanning.CoarsePath
-│   ├── Contracts/
-│   │   ├── Pose2D.cs                     ------ m/rad 单位的不可变二维位姿
-│   │   ├── TravelDirection.cs            ------ Forward 与 Reverse 运动方向枚举
-│   │   ├── GoalDirectionConstraint.cs    ------ Any、Forward、Reverse 目标进入方向约束
-│   │   ├── VehicleParameters.cs          ------ 车体尺寸、安全余量和最大曲率参数
-│   │   ├── PlanningRequest.cs            ------ 地图、起终点、起始曲率和方向约束
-│   │   ├── HybridAStarConfiguration.cs   ------ 原语、离散、代价、限额和容差配置
-│   │   ├── PlanningResult.cs             ------ 状态、诊断、稠密路径和方向分段
-│   │   ├── PlanningStatus.cs             ------ 输入、碰撞、搜索和验证结果枚举
-│   │   ├── PlanningDiagnostics.cs        ------ 节点、堆、耗时、路径和终止统计
-│   │   ├── CoarsePathPoint.cs            ------ 位姿、弧长、方向、曲率和保守净空
-│   │   ├── CoarsePathPointSource.cs      ------ 起点、普通原语和终点截断来源枚举
-│   │   └── PathSegment.cs                ------ 前进/倒车分段及其包含式索引范围
-│   ├── Vehicle/
-│   │   ├── VehicleKinematics.cs          ------ 解析并校验车辆保守最大曲率
-│   │   ├── VehicleFootprint.cs           ------ 计算扩大车体矩形、包围盒和外接圆
-│   │   ├── OrientedRectangleCellIntersection.cs ------ 精确判断旋转车体矩形与栅格矩形相交
-│   │   └── FootprintCollisionChecker.cs  ------ 边界、距离场、精确和扫掠碰撞检查
-│   ├── Search/
-│   │   ├── MotionPrimitive.cs            ------ 单条恒曲率原语及其实际截断长度描述
-│   │   ├── MotionPrimitiveGenerator.cs   ------ 解析积分前进/倒车原语并保留内部点
-│   │   ├── BinaryMinHeap.cs              ------ netstandard2.0 兼容且确定性排序的 Open List
-│   │   ├── SearchCostCalculator.cs       ------ 统一计算长度、倒车、换向、曲率和净空代价
-│   │   ├── HybridAStarNode.cs            ------ 连续位姿、代价、父索引和原语描述
-│   │   ├── HybridAStarNodeKey.cs         ------ 位置格、航向格、方向和曲率离散键
-│   │   ├── GoalToleranceChecker.cs       ------ 位置、航向和目标进入方向容差判断
-│   │   ├── GridDijkstraHeuristic.cs      ------ 八邻域二维绕障距离启发
-│   │   └── HybridAStarSearch.cs          ------ 节点扩展、重开、限额和终点候选管理
-│   ├── Output/
-│   │   ├── PathBacktracker.cs            ------ 按父索引确定性重建原语内部点
-│   │   ├── CoarsePathAssembler.cs        ------ 生成弧长、换向点和包含式方向分段
-│   │   └── CoarsePathValidator.cs        ------ 复核数值、碰撞、曲率、终点和分段
-│   ├── HybridAStarPlanner.cs             ------ 只消费 PlanningGridMap 的纯搜索下层门面
-│   ├── Facade/
-│   │   ├── CoarsePathPlanningJob.cs      ------ 一次调用所需地图、起终点、车辆和调试选项
-│   │   ├── CoarsePathPlanningJobResult.cs ------ 同时返回地图构建结果和粗路径结果
-│   │   ├── PlanningDebugOptions.cs       ------ 地图、路径和碰撞调试发布开关
-│   │   ├── IPlanningDebugSink.cs         ------ 不影响规划状态的调试结果消费接口
-│   │   └── CoarsePathPlanningService.cs  ------ 建图、缓存、搜索和调试编排的一次调用入口
-│   └── Test/
-│       ├── CoarsePathScenarioFactory.cs  ------ 生成固定、可复现的地图和规划场景
-│       └── MovementTest.CoarsePathTest.cs ------ 后台运行规划并在 Clumsy UI 绘制结果
-│   └── README.md                         ------ 粗规划模块边界、公共调用入口和文档链接
-
-ClumsyPilot/tests/
-├── verify_planning_utils.ps1             ------ 单位、角度、坐标和数值守卫测试
-├── verify_planning_map_factory.ps1       ------ 地图输入、图层事务、快照和版本测试
-├── verify_planning_map_adapter.ps1       ------ 地图栅格化、图层、适配和距离场测试
-├── verify_planning_map_image.ps1         ------ 只读快照 PNG 渲染、限制和原子发布测试
-├── verify_coarse_path_collision.ps1      ------ 亚栅格、擦边和扫掠碰撞测试
-├── verify_coarse_path_search.ps1         ------ 原语截断、搜索、方向、限额和重开测试
-├── verify_coarse_path_integration.ps1    ------ Map 到最终路径验证的端到端测试
-└── benchmark_coarse_path.ps1             ------ 参考与压力场景性能资源验收
-```
-
-`Occupancygird_Map/Map_test` 是当前原型位置。实施时会把其中仍然需要的地图能力按以上职责迁移到 `Map`,避免继续向两个现有大文件叠加功能;图片导出可以保留为独立地图测试辅助,不成为规划器依赖。
-
-## Utils
-
-`Utils` 只放无状态、确定性的通用计算,不依赖 Clumsy UI、地图、传感器或搜索器。
-
-| 文件 | 唯一职责 |
-| --- | --- |
-| `AngleMath` | 角度归一化、最小有符号角差、度/弧度边界规则、航向离散索引。 |
-| `UnitConverter` | mm↔m、deg↔rad、曲率/半径转换;不包含业务配置。 |
-| `CoordinateTransform` | 车体系与世界系的二维刚体变换。 |
-| `NumericGuard` | 有限值、正值和范围校验的可复用方法。 |
-| `GridIndex` | 行列索引值对象与比较,不存储地图状态。 |
-
-## Map 模块
-
-### 对外调用门面
-
-`HybridAStarPlanner` 不直接实例化任何地图对象;`CoarsePathPlanningService` 只持有 `PlanningMapFactory`,不接触 `EnvironmentMapBuilder`、`MapObstacleRasterizer`、来源投影器或 `PlanningMapAdapter`。`PlanningMapFactory` 是 `Map` 模块唯一的公共创建入口:
-
-```csharp
-public sealed class PlanningMapFactory
-{
-    public PlanningMapBuildResult Create(PlanningMapRequest request);
-}
-```
-
-下层模块独立调用时只依赖其稳定输出:
-
-```csharp
-var mapResult = new PlanningMapFactory().Create(mapRequest);
-if (!mapResult.Succeeded)
-    return PlanningResult.FromMapFailure(mapResult);
-
-var result = new HybridAStarPlanner().Plan(new PlanningRequest
-{
-    Map = mapResult.Map,
-    Start = startPose,
-    Goal = goalPose,
-    Vehicle = vehicle,
-    Configuration = configuration,
-});
-```
-
-`PlanningMapRequest` 集中地图边界/分辨率、`IReadOnlyList` 和空旷地图声明;`PlanningMapBuildResult` 返回 `Succeeded`、失败原因、每个障碍物来源的摘要、缓存命中类型,以及成功时不可变的 `PlanningGridMap`。因此 A* 的 `PlanningRequest.Map` 始终是已完成校验、投影、栅格化、mm→m 适配和距离场生成的输入,规划器不需要了解地图构造细节。
-
-### 统一障碍物来源与投影
-
-所有人工、TwoLeg 和后续障碍物输入统一实现:
-
-```csharp
-public interface IMapObstacleSource
-{
-    string SourceId { get; }
-    long SourceVersion { get; }
-    bool IsRequired { get; }
-    ObstacleProjectionResult ProjectToWorld();
-}
-```
-
-`ProjectToWorld` 只能消费构造来源对象时已经取得的不可变快照,不得在内部读取传感器、定位、UI 或系统时间。它统一返回世界坐标 mm 几何体:
-
-```text
-ObstacleProjectionResult:
-    SourceId
-    SourceVersion
-    Status                  Applied/Empty/Unavailable/Invalid
-    Message
-    IReadOnlyList Obstacles
-```
-
-必需来源返回 `Unavailable/Invalid` 时地图构建失败;可选来源返回上述状态时记录诊断并继续处理其他来源。多个来源产生重叠障碍物是合法的,占据写入具有幂等语义。
-
-`ManualObstacleSource` 直接输出已经位于世界坐标系的圆和轴对齐矩形。`TwoLegProjectionInput` 是纯数据 DTO,明确包含检测状态、车体坐标系中的两个端点、端点半径、检测时刻的 AMR 世界位姿,以及 mm/deg 单位声明。`MovementTest` 或上层采集适配器负责调用现有 `TwoLegDetect`,随后把结果封装为快照;`TwoLegObstacleSource` 委托 `TwoLegObstacleProjector` 执行确定性车体到世界变换并输出零个或两个 `CircleObstacle`。
-
-新增障碍物来源只需投影为现有 `IMapObstacle` 几何体;如果需要新增多边形等几何类型,必须同时为 `MapObstacleRasterizer` 添加保守的形状-栅格相交实现和成功/边界/失败测试。任何来源都不得应用车辆安全余量。
-
-### 环境地图与图层
-
-`EnvironmentGridMap` 保存 mm 单位的地图边界、分辨率和仅含外部障碍物的占据单元。它不接受 `MarkVehicleFootprint` 一类接口。
-
-`EnvironmentMapBuilder` 是由 `PlanningMapFactory` 使用的内部环境图构造器:
-
-```csharp
-public sealed class EnvironmentMapBuilder
-{
-    public EnvironmentMapBuildResult Build(MapBuildRequest request);
-}
-```
-
-它依次校验地图参数、按 `SourceId` 确定性排序来源、收集 `ObstacleProjectionResult`、合并所有成功投影的 `IMapObstacle` 并调用唯一栅格化器。任何可选来源失败都不会删除其他来源已成功构建的占据内容。
-
-人工障碍物实现 `IMapObstacle`。`AxisAlignedRectangleObstacle` 使用 `(CenterXmm, CenterYmm, WidthMm, HeightMm)`;`CircleObstacle` 使用 `(CenterXmm, CenterYmm, RadiusMm)`。两者的尺寸必须为有限正数,矩形轴与世界 X/Y 轴对齐。`MapObstacleRasterizer` 是唯一直接写入环境栅格的类。
-
-`MapObstacleRasterizer` 是唯一直接写入 `EnvironmentGridMap` 的类型;各来源和投影器都不能取得地图写入接口。`TwoLegObstacleProjector` 不负责地图边界、栅格化、缓存或规划可用性判断。
-
-### 规划适配
-
-`PlanningMapAdapter` 是地图与规划器之间唯一的单位/数据边界:
-
-```csharp
-public sealed class PlanningMapAdapter
-{
-    public PlanningGridMap Adapt(EnvironmentGridMap map);
-}
-```
-
-适配时验证边界和分辨率、深拷贝占据数据、将长度从 mm 转为 m、将边界外固定解释为占据,并生成 `ObstacleDistanceField`。`PlanningGridMap` 是不可变的规划输入,包含 m 单位边界、分辨率、行列、占据数据、距离场、来源版本摘要、快照标识与 `PlanningReady/PlanningBlockReason`。
-
-距离场使用精确的二维欧氏距离变换计算栅格中心到最近占据栅格中心的距离,再减去一个完整栅格对角线 `sqrt(2) * ResolutionMeters` 并截断到零,得到当前自由栅格内任意点到任意占据栅格矩形的保守下界。查询不得进行会抬高结果的插值;查询点使用其所在栅格的保守值。显式空旷地图的障碍物距离可以是正无穷,但车体边界检查仍必须先执行,地图外始终按占据处理。
-
-距离场的用途受以下规则约束:
-
-- 当保守距离严格大于扩大车体外接圆半径时,碰撞检查器可以快速放行。
-- 当保守距离小于或等于外接圆半径时,必须执行精确的扩大车体矩形与占据栅格矩形相交检查。
-- `BodyClearance` 发布 `max(0, ConservativeCenterClearance - ExpandedFootprintCircumscribedRadius)`,作为车体净空的保守下界,不得声称为精确几何净空。
-- 多源距离场实现、空图语义、地图边界和最大栅格数都必须有自动化测试。
-
-`PlanningReady` 的规则:至少一个成功来源提供有效障碍语义即可使地图用于测试/规划;所有来源均为空时,只有已明确配置 `AllowExplicitEmptyMap=true` 才可规划。必需来源失败或未明确空旷语义时,以 `PlanningBlockReason` 阻止规划,而不是暗中把未观测区域当作空闲。
-
-### 现有地图优化与规划适配
-
-本阶段不在旧 `GridMapData` 外再包一层长期兼容适配器,而是把其中经过测试的几何规则迁移为纯逻辑、静态快照式地图管线。迁移目标是消除 UI、传感器、车辆自身足迹和规划查询之间的职责耦合,同时降低 Hybrid A* 高频占据查询与距离查询的开销。
-
-#### 现有职责拆分
-
-| 现有类型/函数 | 处理方式 | 新职责位置 |
-| --- | --- | --- |
-| `TrapMapBounds.TryCreate` | 保留有限值、退化边界、分辨率和最大栅格数校验;移除“车辆到工作站”业务假设 | `MapBoundsMm`、`MapBuildRequest` |
-| `GridMapData.WorldToGrid/GridToWorld` | 保留 X→列、Y→行和 XMax/YMax 排他规则;统一处理最后一个非完整栅格 | `EnvironmentGridMap`、`PlanningGridMap` |
-| `GridMapData.Cells byte[,]` | 改为私有行优先 `byte[]`,索引固定为 `row * Cols + col`;不暴露可写数组 | 两类 GridMap 的内部存储 |
-| `GridMapData.MarkObstacle/MarkObstacles` | 移除默认安全距离;保留候选包围盒裁剪和形状-格矩形相交 | `MapObstacleRasterizer` |
-| `GridMapData.MarkVehicleFootprint` | 从地图模块删除,不提供兼容开关 | `FootprintCollisionChecker` 查询时处理 |
-| `GridMapData.Copy` | 不再暴露可变副本;适配时只进行一次占据缓冲区深拷贝 | `PlanningMapAdapter` |
-| `TrapMapLayerComposer.Compose` | 泛化为“可选来源失败不破坏其他成功来源”的事务语义 | `IMapObstacleSource`、`EnvironmentMapBuilder` |
-| `TrapMapBuilder.Get` | 拆除 `MovementDefinition`、定位读取、TwoLeg 调用、Toast、Painter 和协程依赖 | 上层采集适配器 + `CoarsePathPlanningService` |
-| `TrapMapImageExporter.cs` | 保留经过验证的纯托管 RGBA/PNG 能力,拆分请求、结果、渲染、发布和 PNG 校验职责;输入改为只读规划快照 | `Map/Test/Visualization` |
-
-#### 统一地图输入与静态快照
-
-`PlanningMapRequest` 必须显式包含:
-
-```text
-MapBoundsMm Bounds
-float ResolutionMm
-IReadOnlyList ObstacleSources
-bool AllowExplicitEmptyMap
-```
-
-`Bounds` 使用 `[XMin, XMax) × [YMin, YMax)`;`ResolutionMm` 必须是 20~200 mm 的有限正数。来源 `SourceId` 必须非空且在一次请求内唯一,`SourceVersion` 必须非负,并在对应来源快照内容变化时递增。
-
-`PlanningMapFactory.Create` 返回与来源输入隔离的不可变静态快照,不实现增量栅格更新或距离场局部修补。`PlanningGridMap` 保存各来源版本摘要、`InputFingerprint`、`OccupancyHash` 和通过 `Interlocked.Increment` 生成的进程内单调 `SnapshotId`。该计数器只标识快照,不保存地图内容。规划开始后只读取同一快照;上层即使收到新障碍物,也不得修改正在使用的占据缓冲区。
-
-#### 两级指纹与快照复用
-
-`PlanningMapCache` 是 `PlanningMapFactory` 的线程安全、容量为 4 的最近使用缓存;长期存在的 `CoarsePathPlanningService` 持有同一个工厂实例,因此多次规划可以复用快照。
-
-每次创建按以下顺序判断:
-
-1. 调用纯快照来源的 `ProjectToWorld`,按 `SourceId` 排序,并对边界、分辨率、空图策略、来源状态/版本和规范化世界几何体计算 `InputFingerprint`。
-2. 若缓存中存在相同 `InputFingerprint`,直接返回同一不可变 `PlanningGridMap`;不重新栅格化或生成距离场。
-3. 输入指纹不同时重新栅格化,并对最终连续占据 `byte[]` 计算 `OccupancyHash`。
-4. 若地图几何参数和 `OccupancyHash` 与缓存快照相同,复用占据缓冲区与距离场,只生成包含新来源摘要和新 `SnapshotId` 的轻量快照。
-5. `OccupancyHash` 不同时才重新执行距离场变换并缓存完整新快照。
-
-浮点几何按其 IEEE 位模式和固定字段顺序计算确定性指纹,不通过简单的“坐标除以分辨率取整”判断变化,避免圆或矩形在格边附近发生漏失效。缓存项同时保留规范化输入描述;`InputFingerprint` 命中后仍执行结构相等比较。`OccupancyHash` 命中后仍比较地图几何参数和连续占据缓冲区长度/内容,不能只依赖哈希值判等。
-
-起点、终点、车辆尺寸、安全余量、Hybrid A* 参数和可视化开关不属于地图指纹。TwoLeg 的检测状态从有效变为无检测/不可用/过期时,其来源结果必须改变;上层采集适配器负责根据检测有效期构造正确的 `TwoLegProjectionInput`,地图来源接口本身不读取系统时间。
-
-#### 存储、坐标和查询优化
-
-- 占据数据使用私有连续 `byte[]`,避免公开 `byte[,]` 带来的可变性和多维数组索引开销。
-- `IsOccupied(row,col)`、`IsOccupiedWorld(x,y)` 和保守距离查询保持无分配、常数复杂度;地图外直接返回占据或零净空。
-- 世界坐标到格索引使用 `floor((value - min) / resolution)`;`XMax`、`YMax` 排他。最后一个格子的几何上界必须裁剪到实际地图上界。
-- 构造阶段使用 `checked` 计算 `Rows * Cols`,继续采用 4,000,000 格绝对上限;任何溢出或超限在分配前返回失败结果。
-- 圆形与矩形栅格化只遍历其裁剪后的格索引包围盒,不扫描全图。与地图完全不相交的合法障碍物被忽略并记录在来源摘要中,而不是使地图构建失败。
-- `EnvironmentGridMap` 只有程序集内部的占据写入入口;`PlanningGridMap` 不提供任何写入入口,也不返回内部缓冲区引用。
-
-#### 距离场优化
-
-`EuclideanDistanceTransform` 使用两次一维平方距离变换完成精确二维栅格中心距离计算,时间复杂度为 `O(Rows × Cols)`,不得为每个自由格遍历全部障碍格。中间数组按行列最大长度复用,最终距离使用连续 `double[]` 保存。
-
-`ObstacleDistanceField` 在精确中心距离上执行前述保守修正并封装查询,不允许调用方直接取得未经修正的中心距离用于碰撞放行。显式空图不运行无意义的变换,直接构造正无穷障碍距离场;地图边界仍由规划地图和车体碰撞检查独立约束。
-
-#### 地图迁移顺序
-
-1. 先建立 `MapBoundsMm`、新占据存储和纯栅格化测试,不修改旧 UI 入口。
-2. 建立 `PlanningMapFactory`、静态快照、距离场和规划查询测试。
-3. 将人工障碍、TwoLeg DTO 与现有 Ghost/固定场景迁移为统一 `IMapObstacleSource`。
-4. 建立两级指纹缓存与 `CoarsePathPlanningService` 一次调用入口。
-5. 将旧 `TrapMapImageExporter.cs` 拆分为 `Map/Test/Visualization` 下的五个文件,并将 PNG 导出与 `MovementTest.MapTest` 改为只消费新快照。
-6. 新旧地图回归结果一致后,退役旧地图构建、车体写入和图层合成入口,并更新或删除对应旧反射测试。
-
-迁移期间不得让 Hybrid A* 同时支持新旧两种地图类型;规划器从第一天起只接受 `PlanningGridMap`。
-
-## CoarsePath 模块
-
-### 一次调用编排门面
-
-常规调用方只调用:
-
-```csharp
-public sealed class CoarsePathPlanningService
-{
-    public CoarsePathPlanningJobResult Plan(
-        CoarsePathPlanningJob job,
-        CancellationToken cancellationToken = default);
-}
-```
-
-`CoarsePathPlanningJob` 集中以下输入:
-
-```text
-PlanningMapRequest MapRequest
-Pose2D Start
-Pose2D Goal
-VehicleParameters Vehicle
-HybridAStarConfiguration Configuration
-double StartVehicleCurvature
-TravelDirection? StartDirection
-GoalDirectionConstraint GoalDirection
-PlanningDebugOptions Debug
-```
-
-推荐调用形式:
-
-```csharp
-var result = planningService.Plan(new CoarsePathPlanningJob
-{
-    MapRequest = new PlanningMapRequest
-    {
-        Bounds = bounds,
-        ResolutionMm = 50f,
-        ObstacleSources = new IMapObstacleSource[]
-        {
-            new ManualObstacleSource(manualObstacles),
-            new TwoLegObstacleSource(twoLegSnapshot),
-        },
-        AllowExplicitEmptyMap = true,
-    },
-    Start = startPose,
-    Goal = goalPose,
-    Vehicle = vehicle,
-    Configuration = configuration,
-    Debug = new PlanningDebugOptions
-    {
-        VisualizeMap = true,
-        VisualizePath = true,
-    },
-}, cancellationToken);
-```
-
-一次调用内部固定执行:
-
-```text
-PlanningMapFactory.Create(MapRequest)
-    → 地图失败则生成 FromMapFailure 结果
-    → HybridAStarPlanner.Plan(PlanningRequest, cancellationToken)
-    → IPlanningDebugSink 按 Debug 开关发布地图、路径和诊断
-```
-
-`CoarsePathPlanningJobResult` 同时保留 `PlanningMapBuildResult MapResult` 与 `PlanningResult PlanningResult`,使调用方能够取得实际使用的 `PlanningGridMap` 快照、缓存命中情况和粗路径状态。地图失败时不启动搜索;调试发布失败只写入调试诊断,不改变地图或路径规划状态。
-
-`PlanningDebugOptions` 只包含 `VisualizeMap`、`VisualizePath`、`VisualizeCollisionChecks` 等旁路开关。`IPlanningDebugSink` 由 Clumsy `MovementTest` 适配实现;核心服务默认使用空实现,因此无 UI 环境与自动化测试不加载 Painter。
-
-### 纯搜索下层门面
-
-需要复用已有地图快照或单独测试搜索时调用 `HybridAStarPlanner`:
-
-```csharp
-public sealed class HybridAStarPlanner
-{
-    public PlanningResult Plan(
-        PlanningRequest request,
-        CancellationToken cancellationToken = default);
-}
-```
-
-`PlanningRequest` 组合以下不可变输入:
-
-- `PlanningGridMap Map`
-- `Pose2D Start` 与 `Pose2D Goal`
-- `VehicleParameters Vehicle`
-- `HybridAStarConfiguration Configuration`
-- `double StartVehicleCurvature`,未提供时显式使用零曲率
-- `TravelDirection? StartDirection`,`null` 表示起步方向不受约束
-- `GoalDirectionConstraint GoalDirection`,取值为 `Any`、`Forward` 或 `Reverse`
-
-它不接受地图构建器、UI 对象或传感器对象。起点曲率必须在车辆最大曲率内,并离散到最近的合法曲率等级;该索引作为起始搜索状态的一部分。
-
-`VehicleParameters` 明确使用车辆几何中心为 `Pose2D` 参考点,并包含 `LengthMeters`、`WidthMeters`、`SafetyMarginMeters`、可选 `MaximumCurvaturePerMeter` 与可选 `MinimumTurningRadiusMeters`。最大曲率和最小转弯半径同时存在时使用更保守的限制;两者都未提供时请求无效。
-
-`HybridAStarConfiguration` 集中所有可调参数,包括最大节点数、超时、航向分辨率、原语最大长度、积分步长、碰撞采样步长、曲率等级、代价权重、是否允许倒车、位置容差和航向容差。默认值为:
-
-| 参数 | 默认值 |
-| --- | --- |
-| `PrimitiveLengthMeters` | 0.50 m,表示最大长度 |
-| `IntegrationStepMeters` | 0.05 m |
-| `MaximumCollisionCheckStepMeters` | 0.025 m,且运行时不得大于 `Map.ResolutionMeters / 2` |
-| `HeadingResolutionRadians` | 5° |
-| `CurvatureLevelCount` | 5 |
-| `GoalPositionToleranceMeters` | 0.15 m |
-| `GoalHeadingToleranceRadians` | 5° |
-| `MaximumExpandedNodes` | 200,000 |
-| `SearchTimeout` | 5 s |
-| `HeuristicWeight` | 1.0 |
-| `ReverseCostMultiplier` | 1.5 |
-| `GearSwitchPenaltyMeters` | 1.0 |
-| `CurvatureMagnitudeWeight` | 0.10 |
-| `CurvatureChangePenaltyMetersPerLevel` | 0.05 |
-| `ClearanceCostWeight` | 0.20 |
-| `ClearanceCostDistanceMeters` | 0.50 m |
-
-搜索代价全部以“等效米”为单位:
-
-```text
-primitiveCost =
-    lengthMeters
-  × directionMultiplier
-  × (1
-     + CurvatureMagnitudeWeight × abs(curvature / maximumCurvature)
-     + ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters))
-  + gearSwitchPenalty
-  + CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta)
-```
-
-其中前进的 `directionMultiplier=1`,倒车使用 `ReverseCostMultiplier`;没有换向时 `gearSwitchPenalty=0`。所有权重必须为有限非负值。默认 `HeuristicWeight=1.0`;若调用方调大该值,只承诺更快地寻找可行解,不承诺离散图上的最低代价。
-
-`PlanningResult` 始终返回明确 `PlanningStatus`、诊断信息、零或一条 `IReadOnlyList` 和 `IReadOnlyList`。成功结果中的点契约固定为:
-
-```text
-CoarsePathPoint:
-    X、Y                         m
-    Heading、UnwrappedHeading   rad
-    ArcLength                   m,非负且不递减
-    Direction                   Forward/Reverse
-    VehicleCurvature            1/m
-    BodyClearance               m,保守下界
-    IsGearSwitchPoint           bool
-    Source                      Start/MotionPrimitive/GoalTruncation
-```
-
-`PathSegment` 固定包含 `SegmentIndex`、`Direction`、`StartIndex`、`EndIndex`、`StartsAtGearSwitch` 与 `EndsAtGearSwitch`。`StartIndex` 与 `EndIndex` 都是包含端点的索引,所有分段按索引顺序完整覆盖整条路径。换向时保留两个坐标和航向相同、弧长相同但方向不同的相邻点:前一点结束旧分段,后一点开始新分段并设置 `IsGearSwitchPoint=true`。除这种换向对外,装配器删除相邻重复点。粗路径不包含时间、速度、加速度、舵轮角或轮速。
-
-### 车辆、碰撞和搜索
-
-- `VehicleKinematics` 根据车辆参数提供最大曲率;直接最大曲率和最小转弯半径同时存在时采用更保守的值。
-- `VehicleFootprint` 以连续位姿计算扩大车辆矩形的四角、轴对齐包围盒和外接圆。安全余量只在这里同时加到长度和宽度两侧,不写入地图。
-- `OrientedRectangleCellIntersection` 使用分离轴定理判断连续位姿下的扩大车辆矩形是否与占据栅格矩形相交,不使用仅按离散航向和整数格偏移的模板,因此车辆中心的亚栅格偏移不会漏检。
-- `FootprintCollisionChecker` 依次执行扩大车体边界检查、保守距离场快速放行和包围盒内占据栅格的精确相交检查;它不执行搜索,也不改变地图。
-- `MotionPrimitiveGenerator` 仅生成恒曲率前进/倒车原语,以不大于 0.05 m 的步长保留输出积分点,并使用直线/圆弧解析公式更新位姿,不使用累计误差更大的显式欧拉积分。
-- 相邻碰撞检查位姿的中心位移不得超过 `min(MaximumCollisionCheckStepMeters, Map.ResolutionMeters / 2)`。同时用 `0.5 × (中心位移 + 外接圆半径 × 航向变化绝对值)` 作为扫掠附加余量检查相邻区间端点,保守覆盖两个采样位姿之间的车体运动;该附加余量只用于区间碰撞验证,不写入输出车体尺寸。
-- `GoalToleranceChecker` 只判断位置、航向和最后一段方向约束;位置和航向阈值来自 `HybridAStarConfiguration`。
-- 每条原语按积分点顺序执行数值合法性、碰撞和目标检查。如果某个内部积分点满足目标条件,当前原语在该点截断并生成终点候选;候选加入 Open List,只有当它作为最佳有效节点出队时才成功终止搜索。
-- `GridDijkstraHeuristic` 从目标在占据图上生成八邻域二维绕障距离启发,禁止穿过两个对角相邻障碍物的夹角;它不处理车辆运动学。若目标在二维图上不可达,规划返回 `NoFeasiblePath`。
-- `SearchCostCalculator` 只实现本节定义的等效米代价公式,集中处理倒车、换向、曲率、曲率变化与保守净空代价,不管理节点或 Open List。
-- `BinaryMinHeap` 是兼容 `netstandard2.0` 的内部最小堆,不依赖较新运行时的 `PriorityQueue`。排序依次使用 `F`、`H`、较大的 `G` 和单调递增插入序号,保证相同输入的搜索顺序可复现。
-- `HybridAStarSearch` 使用 `Dictionary` 保存每个离散键当前最佳 `G`。发现更小 `G` 时允许重新打开节点;Open List 中的旧条目通过比较最佳 `G` 惰性丢弃。Closed Set 键为位置格、航向格、方向和曲率等级。
-- 搜索循环在扩展节点前检查取消、超时和节点上限。终点候选只有作为当前最佳有效节点出队时才返回成功。
-- `PathBacktracker` 只存父节点索引和原语描述,在成功后确定性地重新生成内部积分点,避免为所有搜索节点长期保存稠密点。`CoarsePathAssembler` 按既定换向规则累计弧长并构造 `PathSegment`。
-- `CoarsePathValidator` 使用相同的连续位姿、扫掠余量和碰撞规则复核最终稠密输出,同时检查有限数值、曲率上限、目标容差、方向约束、弧长单调性、换向对和分段索引完整覆盖。
-
-第一版允许前进、倒车和换向。换向只可发生在原语边界;相邻原语曲率等级最多变化一级。目标达到容差即成功,不尝试 Reeds-Shepp 精确连接。
-
-## 状态与失败处理
-
-`PlanningStatus` 至少区分:`Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed` 与 `InternalError`。
-
-所有输入错误在开始搜索前返回状态与可读原因;取消、超时和节点上限均返回空路径,不发布部分结果。除参数为空这类编程错误外,搜索或地图对象不能通过异常把部分路径发布给调用方。`PlanningDiagnostics` 记录扩展节点数、生成节点数、重新打开节点数、丢弃的陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因,供之后的性能优化使用。
-
-## 测试设计
-
-纯逻辑测试沿用当前 PowerShell 反射契约测试方式,确保在项目的 `netstandard2.0` 与 Clumsy 引用环境中验证真实程序集。
-
-| 测试文件/入口 | 覆盖内容 |
-| --- | --- |
-| `verify_planning_utils.ps1` | mm/m、deg/rad、角度环绕、车体/世界坐标变换和非法数值。 |
-| `verify_planning_map_factory.ps1` | 统一来源投影、必需/可选失败策略、来源确定性顺序、空图声明、输入指纹、占据哈希、完整/缓冲区缓存命中、来源版本摘要、静态快照隔离与并发访问。 |
-| `verify_planning_map_adapter.ps1` | 圆与轴对齐矩形的包围盒裁剪和格矩形相交、AMR 自身不占据环境图、连续行优先存储、坐标边界、非完整末格、越界保守占据、mm→m 深拷贝、距离场不高估与 `PlanningReady`。 |
-| `verify_planning_map_image.ps1` | 从 `PlanningGridMap` 渲染占据格、边界、起终点、车辆和路径叠加;覆盖关闭导出、非法尺寸、像素/文件上限、唯一命名、临时文件清理、PNG 结构与 CRC。 |
-| `verify_coarse_path_collision.ps1` | 车体中心位于栅格中心和亚栅格位置时的正交/45°/任意航向,边角擦碰、薄障碍、地图边界、距离场快速放行与原语区间扫掠碰撞。 |
-| `verify_coarse_path_search.ps1` | 无障碍前进、0.30 m 非整倍数终点截断、单障碍绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、换向对、±π 航向容差、起点已满足目标、无解、取消、超时、节点上限、节点重新打开与确定性顺序。 |
-| `verify_coarse_path_integration.ps1` | `CoarsePathPlanningService` 一次调用完成多来源建图、缓存复用、规划、回溯和最终验证;复核调试开关不改变地图指纹或规划结果。 |
-| `benchmark_coarse_path.ps1` | Release 构建下的参考场景耗时、扩展节点数、Open List 峰值与托管内存增量。 |
-| `MovementTest.MapTest` | 在 Clumsy UI 中显示人工与 TwoLeg 投影后的环境栅格;可选 PNG 导出只用于调试证据。 |
-| `MovementTest.CoarsePathTest` | 使用固定可复现实例调用 `CoarsePathPlanningService`,绘制地图、起终点、扩大车体检查点和粗路径;`TestStop` 取消规划并清理 Painter,不向底盘发送运动命令。 |
-
-每个新增公共契约均需有成功、边界和失败三类测试。测试场景由 `CoarsePathScenarioFactory` 统一生成,不在 `MovementTest` 中手写地图、原语或搜索细节。
-
-`MovementTest.CoarsePathTest` 在后台任务中调用同步的 `CoarsePathPlanningService.Plan`,持有专用 `CancellationTokenSource`;`TestStop` 先取消规划,再清理任务引用和 Painter。UI 入口不得在界面线程上执行最长 5 s 的搜索,也不得调用任何底盘运动接口。
-
-PowerShell 测试统一使用以下形式执行,绕过本机脚本执行策略差异,并在首个错误处停止:
-
-```powershell
-powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\