Files
FlyShotHost/tests/Flyshot.Core.Tests/OfflinePlanTests.cs
yunxiao.zhu c6829d214a feat(*): 添加 J519 实发重采样与 JSON 机型模型
* 新增 J519 实发采样器,按 8ms 周期生成 timing/jerk 诊断行并完成 rad->deg 转换
* 兼容层产物导出补充 speedRatio,规划编排补齐 smoothStartStopTiming 与日志透传
* 配置与机型加载切换到运行目录 JSON 模型,并补齐 7L 展开模型与相关单元测试
2026-05-07 17:08:32 +08:00

158 lines
6.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Flyshot.Core.Planning.Export;
using Flyshot.Core.Planning.Sampling;
using Flyshot.Core.Triggering;
using Xunit.Abstractions;
namespace Flyshot.Core.Tests;
/// <summary>
/// 离线轨迹生成入口,与 Python demo 的 CLI 对齐。
///
/// 为什么放在测试项目里?
/// ---
/// 测试项目已经引用了所有需要的模块Config、Domain、Planning、Triggering
/// 不需要额外创建 Console 项目。通过 dotnet test --filter 可以直接调用,
/// 也能利用 xUnit 的断言做结果验证。
/// </summary>
public sealed class OfflinePlanTests
{
/// <summary>
/// 使用预设参数生成离线轨迹,输出到 analysis/output/dotnet/。
/// </summary>
[Theory]
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "EOL9_EAU_90", false, 1.0)]
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "EOL9_EAU_90", true, 0.9)]
[InlineData("Rvbust/EOL10_EAU_0/EOL10_EAU_0.json", "EOL10_EAU_0", false, 1.0)]
public void GenerateTrajectory_MatchesPythonDemo(
string configPath,
string trajName,
bool useSelfAdapt,
double speedRatio)
{
var workspaceRoot = GetWorkspaceRoot();
var resolvedConfigPath = Path.Combine(workspaceRoot, configPath);
var outputDir = Path.Combine(workspaceRoot, "analysis", "output", "dotnet", $"{trajName}_sr{speedRatio:F2}_{(useSelfAdapt ? "adapt" : "icsp")}");
Directory.CreateDirectory(outputDir);
// 1. 加载配置和模型。
var loadedConfig = new RobotConfigLoader().Load(resolvedConfigPath, repoRoot: workspaceRoot);
var program = loadedConfig.Programs[trajName];
var resolvedRobotModelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "LR_Mate_200iD_7L.json");
var loadedModel = new RobotModelLoader().LoadProfileAndKinematics(
resolvedRobotModelPath,
loadedConfig.Robot.AccLimitScale,
loadedConfig.Robot.JerkLimitScale);
var baseProfile = loadedModel.Profile;
var kinematicsModel = loadedModel.KinematicsModel;
// 2. 应用 speed_ratio 缩放。
var scaledProfile = ScaleRobotProfile(baseProfile, speedRatio);
// 3. 规划轨迹。
var request = new TrajectoryRequest(
robot: scaledProfile,
program: program,
method: useSelfAdapt ? PlanningMethod.SelfAdaptIcsp : PlanningMethod.Icsp);
PlannedTrajectory trajectory;
if (useSelfAdapt)
{
trajectory = new SelfAdaptIcspPlanner().Plan(request, loadedConfig.Robot.AdaptIcspTryNum);
}
else
{
trajectory = new ICspPlanner().Plan(request);
}
// 4. 生成触发时间轴。
var timeline = new ShotTimelineBuilder(new WaypointTimestampResolver())
.Build(trajectory, holdCycles: loadedConfig.Robot.IoKeepCycles, samplePeriod: TimeSpan.FromMilliseconds(16));
// 5. 稠密采样。
double samplePeriod = 0.016;
var jointDense = TrajectorySampler.SampleJointTrajectory(trajectory, samplePeriod);
var cartDense = TrajectorySampler.SampleCartesianTrajectory(trajectory, kinematicsModel, samplePeriod);
// 6. 导出文件。
TrajectoryExporter.WriteJointDenseTrajectory(Path.Combine(outputDir, "JointDetialTraj.demo.txt"), jointDense);
TrajectoryExporter.WriteCartesianDenseTrajectory(Path.Combine(outputDir, "CartDetialTraj.demo.txt"), cartDense);
TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.demo.json"), timeline.ShotEvents);
// 7. 打印统计信息到测试输出。
_testOutputHelper.WriteLine($"traj_name={trajName}");
_testOutputHelper.WriteLine($"speed_ratio={speedRatio:F6}");
_testOutputHelper.WriteLine($"duration={trajectory.WaypointTimes[^1]:F6}");
_testOutputHelper.WriteLine($"joint_dense_rows={jointDense.Count}");
_testOutputHelper.WriteLine($"cart_dense_rows={cartDense.Count}");
_testOutputHelper.WriteLine($"shot_events={timeline.ShotEvents.Count}");
_testOutputHelper.WriteLine($"output_dir={outputDir}");
// 最小验证:输出文件存在且非空。
Assert.True(File.Exists(Path.Combine(outputDir, "JointDetialTraj.demo.txt")));
Assert.True(File.Exists(Path.Combine(outputDir, "CartDetialTraj.demo.txt")));
Assert.True(File.Exists(Path.Combine(outputDir, "ShotEvents.demo.json")));
Assert.True(jointDense.Count > 0);
Assert.True(cartDense.Count > 0);
}
private readonly ITestOutputHelper _testOutputHelper;
public OfflinePlanTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
/// <summary>
/// 按 speed_ratio 缩放机器人关节限制。
///
/// 缩放律:
/// - 速度按一次方speedRatio^1
/// - 加速度按平方speedRatio^2
/// - jerk 按立方speedRatio^3
///
/// 为什么加速度是平方?
/// ---
/// 如果把时间轴整体缩放 1/speedRatio 倍,速度按 speedRatio 缩放,加速度按 speedRatio^2 缩放。
/// 所以降低 speed_ratio 意味着降低速度、更大幅度地降低加速度和 jerk轨迹更平滑。
/// </summary>
private static RobotProfile ScaleRobotProfile(RobotProfile source, double speedRatio)
{
return new RobotProfile(
name: source.Name,
modelPath: source.ModelPath,
degreesOfFreedom: source.DegreesOfFreedom,
jointLimits: source.JointLimits
.Select(limit => new JointLimit(
limit.JointName,
limit.VelocityLimit * speedRatio,
limit.AccelerationLimit * speedRatio * speedRatio,
limit.JerkLimit * speedRatio * speedRatio * speedRatio))
.ToArray(),
jointCouplings: source.JointCouplings,
servoPeriod: source.ServoPeriod,
triggerPeriod: source.TriggerPeriod);
}
/// <summary>
/// 定位父工作区根目录。
/// </summary>
private static string GetWorkspaceRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
{
return Path.GetFullPath(Path.Combine(current.FullName, ".."));
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
}
}