import { describe, it, expect } from 'vitest'; import { Octree, type OctreeBody } from './octree'; const acc = () => ({ fx: 0, fy: 0, fz: 0 }); describe('Octree', () => { it('accumulates nothing for an empty tree', () => { const out = acc(); new Octree([]).accumulate({ x: 0, y: 0, z: 0 }, 100, 0.8, out); expect(out).toEqual({ fx: 0, fy: 0, fz: 0 }); }); it('exerts no force on a body from itself', () => { const body: OctreeBody = { x: 5, y: 5, z: 5 }; const out = acc(); new Octree([body]).accumulate(body, 100, 0.8, out); expect(out).toEqual({ fx: 0, fy: 0, fz: 0 }); }); it('pushes a body away from another under negative strength', () => { const a: OctreeBody = { x: 0, y: 0, z: 0 }; const b: OctreeBody = { x: 10, y: 0, z: 0 }; const out = acc(); new Octree([a, b]).accumulate(a, -100, 0.8, out); // b is to the right, so a is pushed left. expect(out.fx).toBeLessThan(0); expect(Math.abs(out.fy)).toBeLessThan(1e-9); }); it('falls off with the square of the distance', () => { const near = acc(); const far = acc(); const probe: OctreeBody = { x: 0, y: 0, z: 0 }; new Octree([probe, { x: 10, y: 0, z: 0 }]).accumulate(probe, -100, 0.8, near); new Octree([probe, { x: 20, y: 0, z: 0 }]).accumulate(probe, -100, 0.8, far); expect(Math.abs(near.fx) / Math.abs(far.fx)).toBeCloseTo(4, 1); }); it('scales with the mass of the far group', () => { const one = acc(); const three = acc(); const probe: OctreeBody = { x: 0, y: 0, z: 0 }; new Octree([probe, { x: 50, y: 0, z: 0 }]).accumulate(probe, -100, 0.8, one); new Octree([ probe, { x: 50, y: 0, z: 0 }, { x: 50.1, y: 0, z: 0 }, { x: 49.9, y: 0, z: 0 }, ]).accumulate(probe, -100, 0.8, three); expect(Math.abs(three.fx)).toBeGreaterThan(Math.abs(one.fx) * 2.5); }); it('approximates a distant cluster close to the exact result', () => { // The whole point of Barnes-Hut: theta 0 is exact, 0.8 is nearly so. const probe: OctreeBody = { x: 0, y: 0, z: 0 }; const cluster: OctreeBody[] = []; for (let i = 0; i < 60; i += 1) { cluster.push({ x: 400 + (i % 5), y: (i % 7) - 3, z: (i % 3) - 1 }); } const exact = acc(); const approx = acc(); const tree = new Octree([probe, ...cluster]); tree.accumulate(probe, -100, 0, exact); tree.accumulate(probe, -100, 0.8, approx); expect(approx.fx).toBeCloseTo(exact.fx, 2); }); it('survives perfectly coincident bodies instead of returning NaN', () => { const probe: OctreeBody = { x: 1, y: 1, z: 1 }; const out = acc(); new Octree([probe, { x: 1, y: 1, z: 1 }, { x: 1, y: 1, z: 1 }]).accumulate( probe, -100, 0.8, out ); expect(Number.isFinite(out.fx)).toBe(true); expect(Number.isFinite(out.fy)).toBe(true); expect(Number.isFinite(out.fz)).toBe(true); }); it('handles a large body count without blowing the stack', () => { const bodies: OctreeBody[] = []; for (let i = 0; i < 5000; i += 1) { bodies.push({ x: (i * 37) % 900, y: (i * 53) % 900, z: (i * 71) % 900 }); } const tree = new Octree(bodies); const out = acc(); expect(() => tree.accumulate(bodies[0], -100, 0.8, out)).not.toThrow(); expect(Number.isFinite(out.fx)).toBe(true); }); });