Files
appRobotHoming/test/yAxisRotation.test.js
2026-06-13 00:00:18 +02:00

171 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});
});