feat: 初始化飞拍替换方案仓库骨架

* 建立 .NET 8 解决方案及分层项目结构
* 添加 Flyshot.Core.Domain 领域模型(机器人、轨迹、运动学)
* 添加 Flyshot.Core.Planning 规划层(ICSP、CubicSpline、采样器)
* 添加 Flyshot.Core.Triggering 触发时序与 IO 时间轴
* 添加 Flyshot.Core.Config 配置兼容与 .robot 解析
* 添加 Flyshot.Server.Host 最小宿主及 /healthz 端点
* 补充单元测试与集成测试项目
* 添加 CLAUDE.md、AGENTS.md、README.md 项目规范
This commit is contained in:
2026-04-23 17:35:37 +08:00
commit 4eeaa3fef3
47 changed files with 5140 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
using Flyshot.Core.Domain;
namespace Flyshot.Core.Planning;
/// <summary>
/// 基于 CubicSpline + 逐段时间缩放迭代的 ICSP 规划器。
///
/// 算法核心逻辑(与逆向文档一致):
/// 1. 初始段时长取关节空间欧氏距离;
/// 2. 每轮用当前时长构造 CubicSpline解析求每段 1/2/3 阶导峰值;
/// 3. 按速度一次方、加速度平方根、jerk 立方根缩放时长;
/// 4. 以 sum(|scale_i - 1|) 为收敛指标,保存历史最优结果;
/// 5. 最终用最优时长构造 CubicSpline 并输出时间轴。
/// </summary>
public sealed class ICspPlanner
{
/// <summary>
/// 默认收敛阈值,对应原实现中的 1e-3。
/// </summary>
public const double DefaultThreshold = 1e-3;
/// <summary>
/// 默认最大迭代轮数,对应原实现中的 1000。
/// </summary>
public const int DefaultMaxIterations = 1000;
/// <summary>
/// 执行 ICSP 规划,返回包含完整时间轴和收敛信息的轨迹。
/// </summary>
public PlannedTrajectory Plan(TrajectoryRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var waypoints = request.Program.Waypoints;
if (waypoints.Count < 4)
{
throw new ArgumentException("ICSP 至少需要 4 个示教点。", nameof(request));
}
var qs = WaypointsToArray(waypoints);
var (velLimits, accLimits, jerkLimits) = ExtractLimits(request.Robot);
// 初始段时长直接取相邻示教点的关节空间欧氏距离。
var segmentDurations = ComputeInitialDurations(qs);
int nseg = segmentDurations.Length;
int dof = qs[0].Length;
double bestThreshold = double.PositiveInfinity;
double[]? bestDurations = null;
double[]? bestScales = null;
CubicSplineInterpolator? bestSpline = null;
int bestIterations = 0;
double[]? bestWaypointTimes = null;
for (int iteration = 0; iteration <= DefaultMaxIterations; iteration++)
{
var waypointTimes = CumulativeTimes(segmentDurations);
var spline = new CubicSplineInterpolator(waypointTimes, qs);
var (maxDq, maxDdq, maxDddq) = spline.SegmentMaxAbsDerivatives();
var scales = new double[nseg];
for (int seg = 0; seg < nseg; seg++)
{
double segScale = 0.0;
for (int d = 0; d < dof; d++)
{
double sv = Math.Abs(maxDq[seg, d] / velLimits[d]);
double sa = Math.Sqrt(Math.Abs(maxDdq[seg, d] / accLimits[d]));
double sj = Math.Cbrt(Math.Abs(maxDddq[seg, d] / jerkLimits[d]));
segScale = Math.Max(segScale, Math.Max(sv, Math.Max(sa, sj)));
}
scales[seg] = segScale;
}
double currentThreshold = 0.0;
for (int seg = 0; seg < nseg; seg++)
{
currentThreshold += Math.Abs(scales[seg] - 1.0);
}
if (currentThreshold < bestThreshold)
{
bestThreshold = currentThreshold;
bestDurations = (double[])segmentDurations.Clone();
bestScales = (double[])scales.Clone();
bestSpline = spline;
bestIterations = iteration + 1;
bestWaypointTimes = (double[])waypointTimes.Clone();
}
if (currentThreshold < DefaultThreshold)
{
break;
}
for (int seg = 0; seg < nseg; seg++)
{
segmentDurations[seg] *= scales[seg];
}
}
if (bestSpline is null || bestDurations is null || bestScales is null || bestWaypointTimes is null)
{
throw new InvalidOperationException("ICSP 规划未能产生有效结果。");
}
return new PlannedTrajectory(
robot: request.Robot,
originalProgram: request.Program,
plannedWaypoints: waypoints,
waypointTimes: bestWaypointTimes,
segmentDurations: bestDurations,
segmentScales: bestScales,
method: PlanningMethod.Icsp,
iterations: bestIterations,
threshold: bestThreshold);
}
/// <summary>
/// 把领域层路点列表转换成 double[][],方便数学运算。
/// </summary>
private static double[][] WaypointsToArray(IReadOnlyList<JointWaypoint> waypoints)
{
var result = new double[waypoints.Count][];
for (int i = 0; i < waypoints.Count; i++)
{
result[i] = waypoints[i].Positions.ToArray();
}
return result;
}
/// <summary>
/// 从机器人配置中提取速度/加速度/jerk 限值数组。
/// </summary>
private static (double[] vel, double[] acc, double[] jerk) ExtractLimits(RobotProfile robot)
{
int dof = robot.DegreesOfFreedom;
var vel = new double[dof];
var acc = new double[dof];
var jerk = new double[dof];
for (int d = 0; d < dof; d++)
{
vel[d] = robot.JointLimits[d].VelocityLimit;
acc[d] = robot.JointLimits[d].AccelerationLimit;
jerk[d] = robot.JointLimits[d].JerkLimit;
}
return (vel, acc, jerk);
}
/// <summary>
/// 计算初始段时长:取相邻路点在关节空间的欧氏距离。
/// </summary>
private static double[] ComputeInitialDurations(double[][] qs)
{
int n = qs.Length;
var durations = new double[n - 1];
for (int i = 0; i < n - 1; i++)
{
double sumSq = 0.0;
for (int d = 0; d < qs[i].Length; d++)
{
double diff = qs[i + 1][d] - qs[i][d];
sumSq += diff * diff;
}
durations[i] = Math.Sqrt(sumSq);
}
return durations;
}
/// <summary>
/// 由段时长累加得到绝对时间节点(首项为 0
/// </summary>
private static double[] CumulativeTimes(double[] segmentDurations)
{
int nseg = segmentDurations.Length;
var times = new double[nseg + 1];
times[0] = 0.0;
for (int i = 0; i < nseg; i++)
{
times[i + 1] = times[i] + segmentDurations[i];
}
return times;
}
}