171 lines
7.5 KiB
JavaScript
171 lines
7.5 KiB
JavaScript
/**
|
||
* Unit-Test für scripts/4_yAxis_rotation_reconstruction.py
|
||
*
|
||
* Das Python-Skript wird via child_process aufgerufen.
|
||
* stdout enthält das JSON-Ergebnis (letzte Zeile – davor ggf. Leerzeilen).
|
||
* stderr enthält menschenlesbare Zusammenfassung (wird ignoriert).
|
||
*
|
||
* Test-Daten: test/y-axis-finder-examples/ (drei Timestamps → Arm1-Bogen)
|
||
*
|
||
* Erwartete Werte stammen aus verifiziertem Lauf (2026-06-12):
|
||
* Marker 197, 218, 219 → rotieren (12 Punkte, 3 × 4 Ecken)
|
||
* Marker 201, 204 → gefiltert (Bewegung < 10 mm)
|
||
* axisDir ≈ [0.9995, 0.029, -0.015] (fast reine X-Achse des Roboters)
|
||
*/
|
||
|
||
const { execFileSync } = require('child_process');
|
||
const path = require('path');
|
||
|
||
const SCRIPT = path.join(__dirname, '..', 'scripts', '4_yAxis_rotation_reconstruction.py');
|
||
const EXAMPLES = path.join(__dirname, 'y-axis-finder-examples');
|
||
const POS_A = path.join(EXAMPLES, '20260612_190019');
|
||
const POS_B = path.join(EXAMPLES, '20260612_190104');
|
||
const POS_C = path.join(EXAMPLES, '20260612_190241');
|
||
|
||
// ── Hilfsfunktion ──────────────────────────────────────────────────────────────
|
||
const { spawnSync } = require('child_process');
|
||
|
||
/**
|
||
* Ruft das Python-Skript auf und gibt das geparste JSON zurück.
|
||
* Verwendet spawnSync (statt execFileSync) damit ein Fehler-Exit-Code (ok:false)
|
||
* nicht sofort eine Exception auslöst – wir parsen das stdout trotzdem.
|
||
* stdout kann mehrere Zeilen haben – JSON ist die letzte nicht-leere Zeile.
|
||
*/
|
||
function runScript(posA, posB, posC, extraArgs = []) {
|
||
const proc = spawnSync('python3', [SCRIPT, posA, posB, posC, ...extraArgs], {
|
||
encoding: 'utf-8',
|
||
// stderr läuft zum Terminal durch → menschenlesbarer Summary sichtbar
|
||
});
|
||
if (proc.error) throw proc.error; // z. B. python3 nicht gefunden
|
||
|
||
const stdout = proc.stdout || '';
|
||
const lastJsonLine = stdout
|
||
.trim()
|
||
.split('\n')
|
||
.filter(l => l.trim().startsWith('{'))
|
||
.at(-1);
|
||
|
||
if (!lastJsonLine) {
|
||
throw new Error(
|
||
`Kein JSON in stdout.\nstdout: ${stdout}\nstderr: ${proc.stderr}`
|
||
);
|
||
}
|
||
return JSON.parse(lastJsonLine);
|
||
}
|
||
|
||
// ── Haupt-Test-Suite ──────────────────────────────────────────────────────────
|
||
describe('4_yAxis_rotation_reconstruction.py – Arm1-Testdaten (190019 / 190104 / 190241)', () => {
|
||
|
||
/** Einmalig ausführen – dauert ca. 1 s */
|
||
let r;
|
||
beforeAll(() => {
|
||
r = runScript(POS_A, POS_B, POS_C);
|
||
});
|
||
|
||
// ── Grundstatus ───────────────────────────────────────────────────────────
|
||
test('Berechnung erfolgreich (ok: true)', () => {
|
||
expect(r.ok).toBe(true);
|
||
});
|
||
|
||
// ── Marker-Anzahlen ───────────────────────────────────────────────────────
|
||
test('5 gemeinsame fremd-Marker erkannt', () => {
|
||
expect(r.numMarkersCommon).toBe(5);
|
||
expect(r.commonMarkerIds).toEqual([197, 201, 204, 218, 219]);
|
||
});
|
||
|
||
test('3 Marker tatsächlich genutzt (197, 218, 219)', () => {
|
||
expect(r.numMarkersUsed).toBe(3);
|
||
expect(r.usedMarkerIds).toEqual([197, 218, 219]);
|
||
});
|
||
|
||
test('12 Punkte (3 Marker × 4 Ecken)', () => {
|
||
expect(r.numPoints).toBe(12);
|
||
});
|
||
|
||
// ── Filterung ─────────────────────────────────────────────────────────────
|
||
test('2 Marker wegen zu geringer Bewegung gefiltert (201, 204)', () => {
|
||
expect(r.skipped).toHaveLength(2);
|
||
|
||
const skippedIds = r.skipped.map(s => s.marker_id).sort((a, b) => a - b);
|
||
expect(skippedIds).toEqual([201, 204]);
|
||
|
||
// Beide mit korrektem Grund
|
||
r.skipped.forEach(s => {
|
||
expect(s.reason).toMatch(/Bewegung zu gering/);
|
||
expect(s.max_movement_mm).toBeLessThan(10.0); // unter dem Threshold
|
||
});
|
||
});
|
||
|
||
// ── Residuen ──────────────────────────────────────────────────────────────
|
||
test('Abstandsresiduen: Mittelwert < 10 mm', () => {
|
||
expect(r.residual_dist_mean_mm).toBeLessThan(10);
|
||
});
|
||
|
||
test('Abstandsresiduen: Maximum < 15 mm', () => {
|
||
expect(r.residual_dist_max_mm).toBeLessThan(15);
|
||
});
|
||
|
||
test('Winkelresiduen: Mittelwert < 5°', () => {
|
||
expect(r.residual_angle_mean_deg).toBeLessThan(5);
|
||
});
|
||
|
||
test('Winkelresiduen: Maximum < 7°', () => {
|
||
expect(r.residual_angle_max_deg).toBeLessThan(7);
|
||
});
|
||
|
||
// ── Achsenrichtung ────────────────────────────────────────────────────────
|
||
test('axisDir ist ein Einheitsvektor (|axisDir| ≈ 1)', () => {
|
||
const [ax, ay, az] = r.axisDir;
|
||
const len = Math.sqrt(ax * ax + ay * ay + az * az);
|
||
expect(len).toBeCloseTo(1.0, 4); // ≤ 0.00005 Abweichung
|
||
});
|
||
|
||
test('Achse liegt fast auf X-Achse des Roboters (axisDir[0] > 0.99)', () => {
|
||
// Physikalisch: Arm1 schwingt auf/ab → Drehachse = X-Richtung
|
||
expect(r.axisDir[0]).toBeGreaterThan(0.99);
|
||
});
|
||
|
||
test('axisDir hat 3 Komponenten', () => {
|
||
expect(r.axisDir).toHaveLength(3);
|
||
});
|
||
|
||
test('axisPoint_mm hat 3 Komponenten', () => {
|
||
expect(r.axisPoint_mm).toHaveLength(3);
|
||
});
|
||
|
||
// ── Marker-Ergebnis-Struktur ──────────────────────────────────────────────
|
||
test('markerResults enthält genau 3 Einträge (verwendete Marker)', () => {
|
||
expect(r.markerResults).toHaveLength(3);
|
||
});
|
||
|
||
test('jeder Marker-Eintrag hat n_points_used === 4', () => {
|
||
r.markerResults.forEach(mr => {
|
||
expect(mr.n_points_used).toBe(4);
|
||
expect(mr.circumcenter_mean_mm).toHaveLength(3);
|
||
});
|
||
});
|
||
});
|
||
|
||
// ── Parametertest: benutzerdefinierter min-movement-Threshold ─────────────────
|
||
describe('4_yAxis_rotation_reconstruction.py – min-movement-Parameter', () => {
|
||
|
||
test('mit --min-movement 0 werden alle 5 Marker genutzt (kein Filter)', () => {
|
||
const r = runScript(POS_A, POS_B, POS_C, ['--min-movement', '0']);
|
||
// Alle 5 gemeinsamen Marker werden versucht; Ergebnis ist schlechter
|
||
expect(r.ok).toBe(true);
|
||
expect(r.numMarkersUsed).toBe(5);
|
||
expect(r.numMarkersCommon).toBe(5);
|
||
// skipped enthält nur noch ggf. degenerierte Punkte (keine Bewegungs-Skips)
|
||
const movementSkips = r.skipped.filter(s => s.reason && s.reason.includes('Bewegung zu gering'));
|
||
expect(movementSkips).toHaveLength(0);
|
||
});
|
||
|
||
test('mit --min-movement 1000 werden alle Marker gefiltert → ok: false', () => {
|
||
// Die rotierenden Marker (197, 218, 219) bewegen sich >100 mm,
|
||
// erst ab 1000 mm werden auch sie herausgefiltert.
|
||
const r = runScript(POS_A, POS_B, POS_C, ['--min-movement', '1000']);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.error).toBeDefined();
|
||
});
|
||
});
|