Log in
main 0 open branches 33 files Download ZIP Browse files
test/calc.test.js 115 lines · 4.0 KB · javascript
1
import { describe, expect, test } from 'bun:test';
2
import { cubicSolve, isClose, isComplex, moveDistT3, peakRateT3, pyRound, rateT3 } from '../src/calc.js';
3
import { rng } from './helpers.js';
4
5
const ACC = 2147483648;
6
7
/** Independent tick-by-tick model of the firmware's T3 accumulator loop. */
8
function tickT3(time, rate, accel, jerk, accum) {
9
  let r = rate - Math.trunc(accel / 2) + Math.trunc(jerk / 6);
10
  let a = accel;
11
  let acc = BigInt(accum);
12
  for (let t = 0; t < time; t++) {
13
    r += a;
14
    a += jerk;
15
    acc += BigInt(r);
16
  }
17
  const big = BigInt(ACC);
18
  let pos = acc / big;
19
  if (acc % big < 0n) pos -= 1n;
20
  return { steps: Number(pos), accum: Number(acc - pos * big), rate: r };
21
}
22
23
describe('pyRound', () => {
24
  test('rounds half to even like Python', () => {
25
    expect(pyRound(0.5)).toBe(0);
26
    expect(pyRound(1.5)).toBe(2);
27
    expect(pyRound(2.5)).toBe(2);
28
    expect(pyRound(-0.5) === 0).toBe(true); // Python: round(-0.5) == 0
29
    expect(pyRound(-1.5)).toBe(-2);
30
    expect(pyRound(-2.5)).toBe(-2);
31
    expect(pyRound(2.4999999)).toBe(2);
32
    expect(pyRound(-2.6)).toBe(-3);
33
    expect(pyRound(1234567.5)).toBe(1234568);
34
  });
35
});
36
37
describe('isClose', () => {
38
  test('matches math.isclose defaults', () => {
39
    expect(isClose(1, 1 + 1e-10)).toBe(true);
40
    expect(isClose(1, 1 + 1e-8)).toBe(false);
41
    expect(isClose(0, 1e-12)).toBe(false); // rel_tol alone never matches zero
42
    expect(isClose(0, 1e-12, 1e-9)).toBe(true);
43
  });
44
});
45
46
describe('moveDistT3', () => {
47
  test('known values', () => {
48
    expect(moveDistT3(0, 5, 5, 5, 0)).toEqual([0, 0]);
49
    // One step per tick for 10 ticks.
50
    expect(moveDistT3(10, ACC, 0, 0, 0)).toEqual([10, 0]);
51
    // Half a step per tick for 3 ticks: 1 step, remainder half.
52
    expect(moveDistT3(3, ACC / 2, 0, 0, 0)).toEqual([1, ACC / 2]);
53
    // Negative rate from cleared accumulator starts at 2^31 - 1.
54
    expect(moveDistT3(1, -1, 0, 0)).toEqual([0, 2147483646]);
55
  });
56
57
  test('matches a tick-by-tick firmware model on random moves', () => {
58
    const rand = rng(1234);
59
    for (let i = 0; i < 400; i++) {
60
      const time = 1 + Math.floor(rand() * 3000);
61
      const jerk = Math.floor((rand() - 0.5) * 4000);
62
      const accel = Math.floor((rand() - 0.5) * 2e6);
63
      // Keep |rate| below 2^31 over the whole move, as the planner must.
64
      const rate = Math.floor((rand() - 0.5) * 8e8);
65
      const accum = Math.floor(rand() * ACC);
66
      const sim = tickT3(time, rate, accel, jerk, accum);
67
      if (Math.abs(sim.rate) >= ACC) continue;
68
      expect(moveDistT3(time, rate, accel, jerk, accum)).toEqual([sim.steps, sim.accum]);
69
      expect(rateT3(time, rate, accel, jerk)).toBe(sim.rate);
70
    }
71
  });
72
});
73
74
describe('peakRateT3', () => {
75
  test('equals the largest |rate| seen tick by tick', () => {
76
    const rand = rng(77);
77
    for (let i = 0; i < 300; i++) {
78
      const time = 1 + Math.floor(rand() * 2000);
79
      const jerk = Math.floor((rand() - 0.5) * 4000);
80
      const accel = Math.floor((rand() - 0.5) * 2e6);
81
      const rate = Math.floor((rand() - 0.5) * 8e8);
82
      let r = rate - Math.trunc(accel / 2) + Math.trunc(jerk / 6);
83
      let a = accel;
84
      let peak = 0;
85
      for (let t = 0; t < time; t++) {
86
        r += a;
87
        a += jerk;
88
        peak = Math.max(peak, Math.abs(r));
89
      }
90
      expect(peakRateT3(time, rate, accel, jerk)).toBe(peak);
91
    }
92
  });
93
});
94
95
describe('cubicSolve', () => {
96
  test('real root of the S-curve cubic j t^3 + 2 v t - d = 0', () => {
97
    const rand = rng(7);
98
    for (let i = 0; i < 200; i++) {
99
      const j = 100 + rand() * 50000;
100
      const v = rand() * 10;
101
      const d = 1e-4 + rand() * 5;
102
      const roots = cubicSolve(j, 0, 2 * v, -d).filter((r) => !isComplex(r));
103
      expect(roots.length).toBeGreaterThan(0);
104
      const t = roots[0];
105
      expect(Math.abs(j * t ** 3 + 2 * v * t - d)).toBeLessThan(1e-9 * Math.max(1, d));
106
    }
107
  });
108
109
  test('three real roots', () => {
110
    const roots = cubicSolve(1, -6, 11, -6).sort((a, b) => a - b); // (x-1)(x-2)(x-3)
111
    expect(roots[0]).toBeCloseTo(1, 9);
112
    expect(roots[1]).toBeCloseTo(2, 9);
113
    expect(roots[2]).toBeCloseTo(3, 9);
114
  });
115
});