Files
FlyShotHost/tests/Flyshot.Core.Tests/OfflinePlanTests.cs
yunxiao.zhu a6579f1e5b feat(*): 添加 ConfigRoot 运行时配置目录隔离
* 新增 ControllerClientCompatOptions.ConfigRoot 及解析方法
* 兼容层默认从运行目录 Config 加载模型、轨迹和配置
* 移除隐式父工作区根目录推断,旧路径仅在显式配置时生效
* Host 项目编译时将 Config 目录复制到输出目录
* 请求响应日志中间件忽略 /api/status/snapshot 高频轮询
* 补充 ConfigRoot 和日志过滤相关单元测试
2026-04-29 18:27:03 +08:00

156 lines
6.7 KiB
C#
Raw Permalink 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.Kinematics;
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", "FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot", "EOL9_EAU_90", false, 1.0)]
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot", "EOL9_EAU_90", true, 0.9)]
[InlineData("Rvbust/EOL10_EAU_0/EOL10_EAU_0.json", "FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot", "EOL10_EAU_0", false, 1.0)]
public void GenerateTrajectory_MatchesPythonDemo(
string configPath,
string robotModelPath,
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, robotModelPath);
var baseProfile = new RobotModelLoader().LoadProfile(resolvedRobotModelPath, loadedConfig.Robot.AccLimitScale, loadedConfig.Robot.JerkLimitScale);
var kinematicsModel = new RobotModelLoader().LoadKinematicsModel(resolvedRobotModelPath);
// 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.");
}
}