tests/test_finger_transfer.cs
using System.Numerics;
using HumanoidMocapX;
using HumanoidMocapX.Core.Motion;
using HumanoidMocapX.Inference;

static class FingerTransferTests
{
    internal static void Run()
    {
        var curled = Curled().ToDocument(); var observations = ObservationFixture.Observe(curled);
        var unchanged = Curled().ToDocument();
        MotionTests.Near(1, FingerTransfer.Apply(unchanged, observations));
        for (var f = 0; f < curled.Frames.Count; f++)
            for (var b = 0; b < curled.Bones.Count; b++)
                MotionTests.Near(1, Math.Abs(Quaternion.Dot(MotionDocument.Q(curled.Frames[f].Rotations[b]), MotionDocument.Q(unchanged.Frames[f].Rotations[b]))), 1e-5);

        var flat = MotionTests.Fixture().ToDocument();
        MotionTests.Near(1, FingerTransfer.Apply(flat, observations));
        var actual = ObservationFixture.Observe(flat);
        foreach (var start in new[] { SamObservations.LeftHand, SamObservations.RightHand })
            for (var i = 0; i < SamObservations.HandPoints * 3; i++) MotionTests.Near(observations[start + i], actual[start + i], 1e-4);

        var distrusted = ObservationFixture.Observe(curled);
        for (var f = 0; f < curled.Frames.Count; f++)
            for (var i = SamObservations.ImageKeypoints + SamKeypoints.Count * 2; i < SamObservations.Elements; i++)
                distrusted[f * SamObservations.Elements + i] += 500;
        var kept = MotionTests.Fixture().ToDocument(); var original = MotionTests.Fixture().ToDocument();
        MotionTests.Near(0, FingerTransfer.Apply(kept, distrusted));
        for (var b = 0; b < kept.Bones.Count; b++)
            MotionTests.Near(1, Quaternion.Dot(MotionDocument.Q(original.Frames[0].Rotations[b]), MotionDocument.Q(kept.Frames[0].Rotations[b])), 1e-6);
        MotionTests.Throws<ArgumentException>(() => FingerTransfer.Apply(kept, observations.AsSpan(1)));

        var stiff = Stiff().ToDocument(); var coupled = MotionTests.Fixture().ToDocument();
        FingerTransfer.Apply(coupled, ObservationFixture.Observe(stiff));
        foreach (var side in new[] { "L", "R" })
            foreach (var finger in new[] { "Index", "Middle", "Ring", "Pinky" })
            {
                var (observed, straight) = Bends(stiff, finger, side);
                var (middle, tip) = Bends(coupled, finger, side);
                MotionTests.Check(observed > .4 && straight < .01, $"Stiff fixture bends {observed} and {straight}");
                MotionTests.Near(observed, middle, .01);
                MotionTests.Near(observed * 2 / 3, tip, .01);
            }
        Console.WriteLine("PASS: finger transfer keeps matching fingers, reaches observed finger directions, bends straight tips with their middle joints, and ignores hand passes that disagree with the body");
    }

    /// <summary>Middle and tip joint bends in radians of one finger in the first frame.</summary>
    static (double Middle, double Tip) Bends(MotionDocument motion, string finger, string side)
    {
        var position = ObservationFixture.Positions(motion, motion.Frames[0], out _, out var beyond);
        int prox = ObservationFixture.Bone(motion, finger + "Prox" + side), mid = ObservationFixture.Bone(motion, finger + "Mid" + side), dist = ObservationFixture.Bone(motion, finger + "Dist" + side);
        Vector3 a = Vector3.Normalize(position[mid] - position[prox]), b = Vector3.Normalize(position[dist] - position[mid]), c = Vector3.Normalize(beyond[dist] - position[dist]);
        return (Math.Acos(Math.Clamp(Vector3.Dot(a, b), -1, 1)), Math.Acos(Math.Clamp(Vector3.Dot(b, c), -1, 1)));
    }

    /// <summary>The motion fixture with bent middle finger joints and straight tips, as SAM reports relaxed hands.</summary>
    static FlowMotion Stiff()
    {
        var fixture = MotionTests.Fixture();
        foreach (var pose in fixture.Poses)
            for (var joint = 22; joint < 52; joint++)
                pose[joint * 3 + 2] = (joint - 22) % 3 == 1 ? .6f : 0;
        return fixture;
    }

    /// <summary>The motion fixture with every finger joint bent by its own angle.</summary>
    static FlowMotion Curled()
    {
        var fixture = MotionTests.Fixture();
        foreach (var pose in fixture.Poses)
            for (var joint = 22; joint < 52; joint++)
            {
                pose[joint * 3 + 1] = .2f - (joint - 22) * .01f;
                pose[joint * 3 + 2] = .3f + (joint - 22) % 3 * .25f;
            }
        return fixture;
    }
}