arbeiten am callibration

This commit is contained in:
chk
2026-06-13 00:00:18 +02:00
parent e0e4212a90
commit 1762a771cf
51 changed files with 30343 additions and 5 deletions

170
test/yAxisRotation.test.js Normal file
View File

@@ -0,0 +1,170 @@
/**
* 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();
});
});