MuscleMap
Anatomical name → SDK channel ID. The indirection that lets you write
self.ems_output.quadriceps_left = ... instead of computing bone
indices and channel slices.
Source: fes_framework/muscle_map.py
Config: fes_framework/config/muscle_map_4R.json
API reference: api_reference.md → MuscleMap
Quadriceps
rightanteriorWrite self.ems_output.quadriceps_right — the Stimulator fires 2 of 5 channels on this zone via the MuscleMap.
# Quadriceps (right) — write the slot; the Stimulator resolves
# channels via MuscleMap. You never touch SDK channel IDs.
self.ems_output.quadriceps_right = EMSParamData(
IsMuted=False,
Amplitude=40, # % of calibrated max
PulseWidth=120, # microseconds
Period=20.0, # ms (= 50 Hz)
)What it is#
MuscleMap is a runtime lookup table that resolves anatomical muscle
names ("quadriceps_left", "biceps_right", …) to the integer channel
IDs the Teslasuit SDK uses to deliver stimulation. It loads a JSON
config at construction time, asks the SDK's mapper subsystem for the
current bone layout, and turns each muscle's bone_index + channel_slice spec into a concrete list of channel IDs.
A MuscleMap instance is held by SuitHandler.muscle_map and exposed
to your strategy as self.muscles.
Why it exists#
The Teslasuit SDK exposes the suit as a geometric structure — bones,
nodes, channels. To stimulate the left quadriceps you have to know that
it lives on bone index 1 (the left upper leg), and that the relevant
channels on that bone are slice [0:3] for hardware version 4.x. That
mapping changes between hardware versions (Teslasuit 4.x and XR5).
This is the wrong API for clinical and research developers. They think
in muscle groups, not bone-index-channel-slice tuples. The MuscleMap
formalises the translation as one-time configuration, with the
following consequences:
- Application code is portable across hardware versions — swap a JSON file, no code changes.
- The translation is consulted at connection time, then cached. There is no per-cycle lookup overhead.
- Adding a new muscle is a config change, not a code change.
See: Design principle 3.
When to use it#
Implicitly: every EmsData write goes through the MuscleMap. The
Stimulator looks up the channel IDs for quadriceps_left and routes
the stimulation pulse there. You don't write this code.
Explicitly: when you want to stimulate by region or side without naming each muscle:
def process(self) -> None:
for muscle in self.muscles.by_side("left"):
setattr(self.ems_output, muscle.name, EMSParamData(
IsMuted=False, Amplitude=30, PulseWidth=120, Period=20.0,
))Or when you need the underlying channel IDs (rare — usually only when implementing a custom haptic library):
channels = self.muscles.get_channels("quadriceps_left") # → [14, 15, 16]Default muscle inventory (4.x config)#
The shipping muscle_map_4R.json (covering the Teslasuit 4.x family)
defines 20 muscles:
Lower body (10)#
| Field name | Anatomy |
|---|---|
quadriceps_left/_right | Anterior thigh |
hamstring_left/_right | Posterior thigh |
gastrocnemius_left/_right | Calf |
tibialis_anterior_left/_right | Shin |
gluteus_left/_right | Buttock |
Upper body (10)#
| Field name | Anatomy |
|---|---|
deltoid_left/_right | Shoulder |
biceps_left/_right | Anterior upper arm |
triceps_left/_right | Posterior upper arm |
wrist_flexors_left/_right | Anterior forearm |
wrist_extensors_left/_right | Posterior forearm |
These are the only muscle field names that exist on EmsData. To add
a new muscle, you'd edit muscle_map_4R.json and extend the
EmsData dataclass. The two must stay in sync.
Query API#
The MuscleMap supports a minimal set of queries:
self.muscles["quadriceps_left"] # → MuscleInfo
"quadriceps_left" in self.muscles # → True
list(self.muscles) # → [MuscleInfo, MuscleInfo, ...]
len(self.muscles) # → 20
self.muscles.get_channels("quadriceps_left") # → [14, 15, 16]
self.muscles.list_muscles() # → ["quadriceps_left", "quadriceps_right", ...]
self.muscles.by_side("left") # → [MuscleInfo, MuscleInfo, ...]
self.muscles.by_region("upper_leg") # → [MuscleInfo, MuscleInfo, ...]Each MuscleInfo carries the metadata you might need:
@dataclass
class MuscleInfo:
name: str # "quadriceps_left"
channel_ids: list # [14, 15, 16]
side: str # "left" or "right"
body_region: str # "upper_leg", "lower_leg", "hip", ...
default_period_ms: int # from config
default_amplitude_pct: int # from config
default_pulse_width_us: int # from configThe default_* fields are loaded from the config's
stimulation_defaults section per muscle. You can use them as
sensible starting parameters when initialising a strategy.
Minimal example — region-based control#
from fes_framework.control.strategy_base import ControlStrategyBase
from fes_framework.data.types import EMSParamData
class WarmupStrategy(ControlStrategyBase):
"""Stimulate every muscle in the upper leg at low amplitude."""
def process(self) -> None:
for muscle in self.muscles.by_region("upper_leg"):
setattr(self.ems_output, muscle.name, EMSParamData(
IsMuted=False,
Amplitude=muscle.default_amplitude_pct,
PulseWidth=muscle.default_pulse_width_us,
Period=muscle.default_period_ms,
))Notice: no hardcoded muscle names, no hardcoded amplitude. The strategy will continue to work if you later add a new upper-leg muscle to the config.
Hardware versions#
The framework supports Teslasuit 4.x and XR5. It ships with
muscle_map_4R.json (covering the 4.x family). To run on XR5 (or any
other supported variant):
- Author or obtain a JSON config for that version
(e.g.
muscle_map_XR5.json). - Pass it explicitly:
suit_handler = SuitHandler(muscle_map_config="path/to/muscle_map_XR5.json") engine = ClosedLoopEngine(control_strategy=MyStrategy(), suit_handler=suit_handler)
The framework detects the hardware version from the config's
hardware_version field; you can read it as
self.muscles.hardware_version.
Config file shape (abbreviated)#
{
"hardware_version": "4R",
"muscles": {
"quadriceps_left": {
"side": "left",
"body_region": "upper_leg",
"channels": [
{ "bone_index": 1, "channel_slice": [0, 3] }
],
"stimulation_defaults": {
"period_ms": 20,
"amplitude_pct": 0,
"pulse_width_us": 120
}
},
// ...
}
}channel_slice accepts:
[i]— single channel ID[start, end]— Python-style slice[start, null]— slice fromstartto end of bone[i, j, k, ...](length > 2) — explicit list of channel IDs
See fes_framework/config/muscle_map_4R.json for the full file.
See also#
- API Reference → MuscleMap
- Stimulator — the consumer of the MuscleMap
- Design principle 3
- Implementation Guide → Step 3 — semantic muscle output
- Example:
examples/atomic/semantic_muscle_control.py - Source:
fes_framework/muscle_map.py
