Calibration
Trigger Teslasuit skeleton calibration, score its quality, export the
reference frame. Typically used as a gate inside on_start().
Source: fes_framework/calibration.py
API reference: api_reference.md → CalibrationAPI
What it is#
CalibrationAPI is a small wrapper around the Teslasuit SDK's
calibration routine. It exposes three operations:
calibrate()— trigger the SDK's blocking calibration call and capture the resulting reference frame.check_quality()— assess the calibration via three heuristic checks (sensor reporting, left-right symmetry, plausible IMU ranges) and return a structured score.export(path)— write the reference frame to a timestamped CSV file for offline analysis.
The framework instantiates one CalibrationAPI per engine and exposes
it as engine.calibration.
Why it exists#
Calibration is essential for accurate motion capture — every joint angle is measured relative to the I-pose reference frame the SDK captures during calibration. Without it, joint angles drift, and any strategy keying on them produces incorrect stimulation.
But raw SDK calibration is just "call this function while the user stands still." The framework adds three things:
- A structured result (
CalibrationResult) that records success and the reference frame, so subsequent code can decide what to do. - A quality assessment (
CalibrationQuality) that catches the common failure modes — dead sensors, wrong I-pose, gross IMU miscalibration — before the user starts a session. - Export to CSV in a single call, for offline reproducibility.
The result is a pattern (calibrate → check quality → gate the run) that's easy to apply consistently across applications.
When to use it#
Almost every application should run calibration before its first
control cycle. The standard pattern is to subclass
ClosedLoopEngine, override on_start(), and run the
calibrate/check_quality/stop sequence:
class CalibratedEngine(ClosedLoopEngine):
def on_start(self) -> None:
input("Stand in I-pose. Press ENTER to calibrate...")
result = self.calibration.calibrate()
if not result.success:
print(f"FAIL: {result.message}")
self.stop()
return
quality = self.calibration.check_quality()
print(f"Quality: {quality.overall_score:.0%}")
if not quality.is_acceptable:
print("Quality too low. Reposition suit and retry.")
self.stop()Skip calibration only for applications that don't depend on accurate
joint angles — pure haptic-feedback apps, foot-contact-only apps,
runtime parameter exploration. Even then, the engine logs a warning
when run() is called without calibration.
Quality checks#
check_quality() runs three checks and averages their scores:
1. Sensor reporting (~33% weight)#
Counts how many of the 20 body sensors are returning data after
calibration. A sensor that's missing or returning all-zero
acceleration is flagged. Score = reporting / 20.
Common cause of failure: a suit segment is loose or unplugged.
quality.missing_sensors lists which ones.
2. Left-right symmetry (~33% weight)#
For each bilateral pair (LeftUpperLeg/RightUpperLeg,
LeftLowerLeg/RightLowerLeg, etc.) the cosine similarity between
the two acceleration vectors is computed. In I-pose, gravity should
be the only acceleration; bilateral limbs should be near-identical.
Score = mean cosine similarity across pairs (clamped to [0, 1]).
Common cause of failure: the user wasn't actually in I-pose (arms crossed, feet apart, leaning).
3. Plausible IMU ranges (~33% weight)#
For a small set of representative bones (Hips, Chest,
LeftUpperArm, RightUpperArm), confirm the accelerometer magnitude
is plausibly close to 1 G (~9.81 m/s²). Acceptable range:
8.8–11.8 m/s². Score = passed / total.
Common cause of failure: gross sensor miscalibration.
Overall#
overall_score = mean(sensor_score, symmetry_score, range_score),
rounded to 3 decimals. is_acceptable = overall_score >= 0.7.
The 0.7 threshold is CalibrationAPI.QUALITY_THRESHOLD. You can
tune it for your application — lower for casual demos, higher for
clinical precision work.
Result structures#
@dataclass
class CalibrationResult:
success: bool # True if SDK calibration succeeded
timestamp: float # time.time() at completion
reference_frame: RawData | None # raw IMU snapshot in I-pose
message: str = "" # human-readable status
@dataclass
class CalibrationQuality:
overall_score: float # 0.0..1.0 (mean of three checks)
is_acceptable: bool # overall_score >= QUALITY_THRESHOLD
sensors_reporting: int # 0..20
sensors_expected: int # always 20
missing_sensors: list[str] # bone names of silent sensors
symmetry_score: float # 0.0..1.0
issues: list[str] # human-readable issue descriptionsWhen you display quality to a user, quality.issues is the
operator-friendly summary; the numeric scores are for logging.
Minimal example — calibration gate#
from fes_framework.engine import ClosedLoopEngine
class CalibratedEngine(ClosedLoopEngine):
def on_start(self) -> None:
result = self.calibration.calibrate()
if not result.success:
self.stop()
return
quality = self.calibration.check_quality()
print(f"Calibration: {quality.overall_score:.0%} "
f"({quality.sensors_reporting}/20 sensors)")
if not quality.is_acceptable:
print(f"Issues: {quality.issues}")
self.stop()
return
# OK — continue into the main loop
self.calibration.export(path="./data")For a richer pattern (re-calibration on poor quality), see
examples/atomic/calibration_gate.py.
Export#
engine.calibration.export(path="./data") writes a CSV with a
timestamped filename: data/calibration_<YYYY-MM-DD_HH-MM-SS>.csv.
The contents are the raw IMU snapshot taken immediately after
calibration — the same data used for quality checks.
Use this for:
- offline reproducibility (re-run analysis with the same reference),
- archival of session metadata,
- debugging post-hoc when a session went unexpectedly.
See also#
- API Reference → CalibrationAPI
- Implementation Guide → Step 4 — calibration gate walkthrough
- Safety — calibration as a precondition
- Example:
examples/atomic/calibration_gate.py - Source:
fes_framework/calibration.py - Detailed design:
context/realisation details/calibration_design.md
