Paper · 2024-11-07 · cs.RO, cs.AI, cs.LG
Field Assessment of Force Torque Sensors for Planetary Rover Navigation
Field test of six force-torque sensors, one above each wheel of the MaRTA planetary rover, on four terrain types. A neural network on sensor statistics reaches 96.17% test accuracy (IMU only: 79.35%). Estimating drawbar pull from the sensors by lever-length filtering is not reliable enough without ground truth. Author-reported; MIT code.
Support vector machineMulti-layer perceptron (MLP)Feature scalingDropoutterrain-classificationforce-torque-sensingplanetary-roverproprioceptiondrawbar-pull
arXivPDFCodeMarkdown15 min read
AI-compiled by claude-haiku-5-5 on 2026-10-25, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
A rover that drives over another planet has to know what kind of ground it is on. Most rovers sense this with inertial sensors (IMUs), which feel vibration and motion. This paper asks whether a force-torque sensor, a device that measures push and twist forces, mounted above each wheel does a better job. The authors drove a six-wheeled test rover over loose soil, compressed sand, pebbles and rock at a semi-desert site in Spain that stands in for Mars and the Moon. They recorded the sensors and trained two simple classifiers, a support vector machine and a small neural network, to name the ground type. The force-torque sensors did well, especially with the neural network.
The paper then tries to read how hard each wheel pulls from the same sensors. The signal is too noisy to read directly, so the authors propose a filter and conclude that it is not yet dependable. For most rovers they recommend cheaper, conventional sensors.
TL;DR
- Field test on ESA's MaRTA rover, a six-wheeled testbed with one six-axis force-torque sensor (FTS) above each wheel, on four labelled terrain types §1 §3 fig. 3.
- Terrain classification on sensor statistics, author-reported test accuracy: neural network with FTS data 96.17%, with IMU only 79.35%; SVM with FTS 95.58%, with IMU only 85.84% §3.1 §3.2 tab. 2 tab. 3 tab. 5 tab. 6.
- Drawbar pull: the paper keeps only samples whose lever length (torque divided by force) falls in a plausible geometric band. It reports forces of roughly 5 to 20 N in those windows, but has no ground truth to check them §4 fig. 14. The conclusion is that FTS data cannot yet be read as drawbar pull §5.
- The MIT-licensed code reproduces the feature pipeline and both classifiers. Several details differ from the paper, notably the window length, the dropout placement, the sign of the lever ratio and the test split (see Paper vs code).
Problem
Planetary rovers cannot be repaired once they are on the surface, so they plan conservatively from their sensors §1. Wheel slip and drawbar pull (the pulling force a wheel can deliver to the ground) matter for mobility, and the usual sensing for them is visual odometry, motor current or IMUs §2. Force-torque sensors are well established in manipulators and legged robots, but the paper says that no earlier work used them alone for terrain classification on wheeled rovers, and that their use for drawbar pull on rovers is little explored §2. The paper's question is whether the sensors help with navigation tasks such as terrain classification and drawbar pull, using real data from unstructured terrain with slopes, turns and varying speed §1 §3.
Method
The test rover is MaRTA, a half-scale model of the ExoMars rover with a triple bogie. Each of its six leg assemblies carries one six-axis FTS between the wheel and the steering joint, above the wheel rather than at the hub. An IMU sits on the chassis base plate §1 §3 fig. 3 fig. 10. The data come from a field test in July 2023 at the Bardenas Reales semi-desert in Spain §1.
- Labels. Four terrain classes were chosen by visual inspection of the camera view: loose soil, compressed sand, pebbles and rock §3 fig. 2. The code lists 15 labelled traverse segments, 4 compressed, 3 pebbles, 2 loose and 6 rock baseprod_helpers.py.
- Windows. Sensor data are cut into fixed windows, and the paper uses 1-second windows. The FTS and IMU streams are not synchronised, so each window pools the data from all sensors §3.
- Statistics. Each window yields five statistics (minimum, maximum, mean, median, standard deviation) per channel §3. Each FTS contributes its three forces and three torques, plus one derived channel, the lever ratio Ty / Fx (eq. 1) described in step 6. That is 7 channels × 5 = 35 values per FTS. The IMU contributes its three accelerations × 5 = 15 values. Six FTS plus the IMU give 225 inputs §3. The paper says the derived value is force over the torque about z, but eq. 1 and the code use the torque about y (see Paper vs code).
- Classifiers. A support vector classifier is tuned by grid search over C, gamma and the kernel §3.1. A feed-forward network with one hidden layer of 64 units is trained with dropout §3.2, and fig. 8 shows two hidden layers of 64 units.
- Evaluation. The data are split 75/25 into training and test sets. The paper compares IMU only, FTS only and both §3.1 §3.2 tab. 2 to tab. 7. Per-class results are given as row-normalised confusion matrices.
- Drawbar pull. The paper relates the torque about y at the sensor to the force in x through a lever length L (eq. 1). L lies between 0.1 m, the sensor-to-axle distance, and 0.175 m, the sensor-to-ground distance at the contact point §4 fig. 10. The paper assumes a rigid linkage, a rigid wheel, straight travel on flat ground, no sinkage and no grousers §4. It keeps samples whose lever length lies within a tolerance band around 10 to 17.5 cm §4.
\tau_y = F_x \cdot L eq. 1
# Feature rows for one labelled traverse [§3]
# Input: six FTS streams, one IMU stream, window length w [s]
# Output: rows of [label, 210 FTS values, 15 IMU values]
def make_rows(traverse, fts_streams, imu, w):
# Right-side sensors: negate Force_X and Force_Y only
# [code: preprocessing/export_classification_stats.py#L192-L195]
for stream in fts_streams:
if stream.name[1] == "R":
stream.force_x = -stream.force_x
stream.force_y = -stream.force_y
# Lever ratio, signed, no absolute value [eq. 1]
# [code: preprocessing/export_classification_stats.py#L197-L198]
for stream in fts_streams:
stream.lever = stream.torque_y / stream.force_x
rows = []
# Consecutive, non-overlapping windows of length w [§3]
# [code: preprocessing/export_classification_stats.py#L201-L203]
for start in window_starts(traverse, w):
row = [traverse.label]
for stream in fts_streams:
# 6 channels + lever, 5 statistics each = 35 values [§3]
# [code: preprocessing/export_classification_stats.py#L225-L269]
row += five_stats_per_channel(stream, start, start + w)
# 3 acceleration axes x 5 statistics = 15 values [§3]
# [code: preprocessing/export_classification_stats.py#L278-L297]
row += five_stats_per_channel(imu, start, start + w)
rows.append(row)
# Drop rows with NaN, e.g. windows without samples
# [code: preprocessing/export_classification_stats.py#L313-L315]
return drop_nan(rows)# Random split of the rows [§3.1]
# [code: ml/dataset.py#L60-L80]
def split(rows, test_fraction=0.25, seed=42):
# Shuffle all rows with a fixed seed [code: ml/dataset.py#L60-L63]
rows = shuffle(rows, seed)
# Test share of the rows [code: ml/dataset.py#L67]
n_test = int(len(rows) * test_fraction)
# Train is all but the last n rows; test takes the last n+1 rows,
# so one row appears in both [code: ml/dataset.py#L77-L80]
return rows[:-n_test], rows[-n_test - 1:]# SVM: grid search over C, gamma and kernel [§3.1]
def fit_svm(x_train, y_train):
# Parameter grid; "degree" is in the code only
# [§3.1] [code: ml/svm.py#L191-L196]
grid = {
"C": [0.1, 1, 10, 100],
"gamma": [1, 0.1, 0.01, 0.001],
"kernel": ["linear", "rbf", "poly", "sigmoid"],
"degree": [2, 3],
}
# Five-fold cross-validation, refit on the training split
# [code: ml/svm.py#L202]
search = grid_search_cv(SVC, grid, folds=5)
search.fit(x_train, y_train)
return search.best_estimator_# Neural network with a log-softmax output [§3.2]
# [code: ml/model.py#L36-L70]
def forward(inputs):
# Input dropout, rate 0.1 [code: ml/model.py#L45]
x = dropout(inputs, rate=0.1)
# Fully connected layers with ReLU; sizes chosen by cross-validation
# [code: ml/train.py#L510-L518]
for layer in hidden_layers:
x = relu(layer(x))
# Dropout rate 0.2 after the last hidden layer [code: ml/model.py#L66]
x = dropout(x, rate=0.2)
# Four terrain classes [code: ml/model.py#L42]
# [code: ml/model.py#L68]
return log_softmax(output_layer(x))# Training loop [§3.2] [code: ml/train.py#L478-L585]
def train(model, batches, epochs=50, lr=0.001):
# Adam; not named in the paper [code: ml/train.py#L556]
optimizer = adam(model.parameters(), lr=lr)
for epoch in range(epochs):
for inputs, labels in batches:
# Batch size 32 [§3.2]
optimizer.zero_grad()
loss = cross_entropy(model(inputs), labels)
loss.backward()
# Clip the gradient norm at 1 [code: ml/train.py#L292]
clip_grad_norm(model.parameters(), max_norm=1)
optimizer.step()# Lever-length filter for drawbar-pull intervals [§4]
def keep_stable(stream, low=0.100, high=0.175, tol=0.01):
# Lever length, absolute value [eq. 1]
# [code: preprocessing/find_usable.py#L79]
lever = abs(stream.torque_y / stream.force_x)
# Tolerance widens the band, e.g. 0.09 to 0.185 m for tol 0.01
# [code: preprocessing/find_usable.py#L138-L142]
return (low - tol < lever) & (lever < high + tol)Hyperparameters
| Name | Value | Where | Notes |
|---|---|---|---|
| Window length | 1 s | §3 | Code default is 0.5 s, non-overlapping export_classification_stats.py |
| Statistics per channel | min, max, mean, median, std | §3 | export_classification_stats.py |
| Inputs per FTS | 35 (6 channels + lever, × 5 statistics) | §3 | Lever = Ty / Fx; paper says torque about z in text §3 |
| Inputs per IMU | 15 (3 accelerations × 5) | §3 | Accelerations only; no orientation in features export_classification_stats.py |
| Total inputs | 225 (6 × 35 + 15) | §3 | |
| Test split | 25% | §3.1 tab. 2 | Shuffled, seed 42 dataset.py |
| SVM C | {0.1, 1, 10, 100} | §3.1 | Best C = 0.1 for IMU, FTS and both tab. 2 tab. 3 tab. 4 |
| SVM gamma | {1, 0.1, 0.01, 0.001} | §3.1 | Best: 0.1 (IMU), 1 (FTS), 1 (both) |
| SVM kernel | linear, RBF, polynomial, sigmoid | §3.1 | Best: polynomial (IMU), linear (FTS and both) |
| SVM degree | not stated | svm.py | Grid {2, 3} in code only |
| SVM folds | not stated | svm.py | 5-fold in code |
| SVM feature scaling | not stated | svm.py svm.py | Off unless --scale_features is passed |
| NN hidden layers | one layer of 64 (text) or two of 64 (figure) | §3.2 fig. 8 | Paper conflicts with itself. Code chooses among 7 layer sizes by CV train.py |
| NN input dropout | 0.1 | §3.2 model.py | |
| NN hidden dropout | 0.2 | §3.2 model.py | Paper: between hidden layers. Code: one layer before the output model.py |
| NN batch size | 32 | §3.2 | Test set in one batch train.py |
| NN epochs | 50 | §3.2 | train.py |
| NN optimizer | not stated | train.py | Adam |
| NN learning rate | not stated | train.py | Default 0.001. CV hardcodes 0.001 train.py |
| NN gradient clip | not stated | train.py | Norm 1 |
| NN weight init | not stated | model.py | Xavier uniform weights, bias 0.01 |
| NN loss | not stated | train.py | Cross-entropy on log-softmax outputs |
| NN feature scaling | not stated | dataset.py | No standardisation on the training path |
| Random seed | 42 | not stated | train.py |
| Lever band, paper | 10 to 17.5 cm | §4 | Geometric limits: 10 cm offset, 17.5 cm reach |
| Lever tolerance, code | ± 1 cm (9 to 18.5 cm) | tab. 9 | find_usable.py |
| Force and torque limits | 164.16 N, 28.728 Nm | not stated | Code only, from a 0.8 gearhead efficiency find_usable.py |
| Sensor geometry | 10 cm FTS to wheel axis; 17.5 cm FTS to ground | §4 fig. 10 | Wheel diameter 15 cm |
Results
Author-reported. Setup: BASEPROD field test, one six-wheeled rover, four terrain classes, 25% test split §3.1 §3.2 tab. 2. The paper gives one split per model and no confidence intervals. The number of repeated runs is not stated.
| Model | Input | Train accuracy [%] | Test accuracy [%] | Source |
|---|---|---|---|---|
| SVM, polynomial kernel | IMU | 89.51 | 85.84 | tab. 2 |
| SVM, linear kernel | FTS | 100.00 | 95.58 | tab. 3 |
| SVM, linear kernel | IMU + FTS | 100.00 | 95.87 | tab. 4 |
| NN | IMU | 84.02 | 79.35 | tab. 5 |
| NN | FTS | 98.92 | 96.17 | tab. 6 |
| NN | IMU + FTS | 99.31 | 96.76 | tab. 7 |
Per-class recall, the diagonal of each row-normalised confusion matrix [%]:
| Model | Input | Loose | Compressed | Pebbles | Rock | Source |
|---|---|---|---|---|---|---|
| SVM | IMU | 90.91 | 86.25 | 58.21 | 96.23 | tab. 2 |
| SVM | FTS | 100.00 | 97.50 | 85.07 | 98.11 | tab. 3 |
| SVM | IMU + FTS | 100.00 | 97.50 | 86.57 | 98.11 | tab. 4 |
| NN | IMU | 96.97 | 80.00 | 28.36 | 96.86 | tab. 5 |
| NN | FTS | 100.00 | 98.75 | 88.06 | 97.48 | tab. 6 |
| NN | IMU + FTS | 100.00 | 95.00 | 95.52 | 97.48 | tab. 7 |
The full matrices are in tab. 2 to tab. 7. The row percentages imply 33 loose, 80 compressed, 67 pebbles and 159 rock test samples, 339 in total. This is derived from the percentages, not stated in the paper, and matches the inference-set size in tab. 8.
The paper's conclusions on these numbers §3.3:
- FTS features beat IMU features for the neural network. The largest gain is on pebbles: the IMU-only network puts 67.16% of pebbles into rock, against 10.45% with FTS §3.2 tab. 5 tab. 6.
- For the SVM, FTS raises every class over IMU. Pebbles goes from 58.21% to 85.07% recall §3.1 tab. 2 tab. 3.
- Adding the IMU to FTS changes the overall test accuracy slightly. The tables give +0.29 points for the SVM and +0.59 for the NN tab. 3 tab. 4 tab. 6 tab. 7. The NN text says about 0.1 points §3.2, so the two sources disagree.
Timing, measured on an 11th-gen Intel i7-11800H laptop with an RTX 3050 Ti Mobile, averaged over five repeats, with inference on the whole 339-sample test set tab. 8:
| Model | Measure | Time [s] | Source |
|---|---|---|---|
| SVM | Full grid search | 24.25522 | tab. 8 |
| SVM | One fit, best parameters | 0.132472 | tab. 8 |
| SVM | Inference, CPU | 0.00762 | tab. 8 |
| NN | One training run, best parameters | 33.751 | tab. 8 |
| NN | Inference, CPU | 0.0014288 | tab. 8 |
The paper says it does not expect to run classification faster than 1 Hz, and that its rovers move slower than 1 m/s, so both models are fast enough for it §3.3.
Drawbar pull. The paper reports the share of FTS samples that remain after the lever filter, for each sensor and in total, at three tolerances. The first column is the tolerance around 10 to 17.5 cm. The caption's example (9 to 18.5 cm) matches the 1 cm row tab. 9:
| Tolerance [cm] | FL | FR | CL | CR | BL | BR | Total |
|---|---|---|---|---|---|---|---|
| 5 | 75.05 | 61.93 | 57.80 | 59.13 | 74.65 | 55.05 | 63.94 |
| 2 | 61.32 | 46.66 | 41.56 | 43.62 | 60.98 | 39.91 | 49.01 |
| 1 | 54.64 | 40.38 | 34.74 | 37.60 | 54.00 | 34.00 | 42.56 |
Values are the paper's, rounded to two decimals tab. 9. In the highlighted stable intervals the paper reports about 10 N of force in x for loose soil, about 20 N for compressed sand, and 10 to 20 N on rock §4 fig. 11 fig. 12 fig. 13. Short zoomed windows give 5 to 10 N for loose soil and about 15 N ± 5 N for compressed sand, with rock much more variable §4 fig. 14. These are not checked against measured drawbar pull, and the paper says ground truth is needed §4 §5.
Paper vs code
The official code is github.com/spaceuma/fts-assessment at commit
42d6378 on the default branch. The paper's Data availability statement links
it, and the copyright holder is the authors' lab LICENSE. The README says the published
numbers came from tag v0.2-revision, and the preprint from v0.1-preprint
README.md. I did not check out those tags, so the comparisons
below are against the default branch only.
- Lever ratio. The text says force in x over torque about z §3. Eq. 1 uses τ_y, and the code computes Torque_Y / Force_X eq. 1 [code: preprocessing/export_classification_stats.py#L198]. The code follows eq. 1.
- Window length and overlap. The paper uses 1-second windows described as sliding §3. The export defaults to 0.5 s [code: preprocessing/export_classification_stats.py#L63], and the windows are consecutive and non-overlapping [code: preprocessing/export_classification_stats.py#L201-L203]. The README run command does not pass a window size README.md.
- Sign of the right-side sensors. The paper says right-side sensors are rotated 180° about z fig. 10. The export negates Force_X and Force_Y only export_classification_stats.py. A rotation about z would also flip the torques about x and y. Because torque_y is not flipped, the signed lever feature has the opposite sign on right-side sensors. The paper does not say which convention it used.
- Lever filter for classification. The paper uses the lever filter for
drawbar pull §4. The export sets
cut_to_lever = False, so the classifier inputs are not filtered [code: preprocessing/export_classification_stats.py#L354]. The paper does not state whether the classifier used the filter. - Lever band. The paper's band is 10 to 17.5 cm §4. The code's default tolerance is 1 cm, so the band is 9 to 18.5 cm [code: preprocessing/find_usable.py#L138-L142]. The 5 cm and 2 cm rows of tab. 9 are not produced by the code on this branch. The code also takes the absolute value of the ratio find_usable.py, where eq. 1 is signed.
- Hidden-layer count. The text says one hidden layer of 64 units §3.2. The figure shows two fig. 8. The code's model default is [64, 64] [code: ml/model.py#L36], and the training script chooses among seven sizes by five-fold CV train.py. The paper does not state which size was selected.
- Dropout placement. The paper says 20% dropout between the hidden layers §3.2. The code applies one 20% dropout after the last hidden layer, before the output layer model.py model.py.
- SVM grid. The paper lists C, gamma and kernel §3.1. The code also grids polynomial degree over {2, 3} svm.py and uses 5-fold CV [code: ml/svm.py#L202]. The paper states neither.
- SVM multi-class scheme. The paper says one-vs-one and one-vs-all gave the
same accuracy and reports one-vs-all §3.1. The code passes
decision_function_shape="ovr"svm.py [code: ml/svm.py#L200]. Scikit-learn'sSVCtrains one-vs-one internally, and that flag reshapes onlydecision_function. The paper's one-vs-all label likely does not describe how the models were trained. - Test split. The paper says 25% of the samples are held out tab. 2. The code's test slice has one more row than the 25% count, and that row is also in training dataset.py.
- Feature scaling. The paper does not say whether features were scaled. The
neural network path does not scale dataset.py. The SVM script
scales only with
--scale_features, which the README command omits [code: ml/svm.py#L93-L97] README.md. - Sample count. The table gives 1,360 processed samples tab. 1. The text of the NN section says 1,361 §3.2.
Pitfalls
- Overlapping test row. The training slice ends one row before the test slice begins, so one shuffled row is in both sets dataset.py. The effect is probably small, but it is a leak.
- Random split over windows. The split is random over rows, not over traverses dataset.py. Windows from the same traverse, a few seconds apart, can land in train and test. The paper does not state the split unit. Accuracy on an unseen traverse may be lower than the reported numbers.
- Header row dropped. The export writes integer column names in the first
line training_data.csv.
pd.read_csv(..., skiprows=1)skips that line dataset.py, so the first data row becomes the column names and is lost. I read this from the code and pandas behaviour, but did not run it. - Unbounded lever feature. The lever ratio divides by Force_X with no guard export_classification_stats.py. Values near zero make it very large. The paper notes the filter does not remove all standing-still samples fig. 15. An infinite value would fail the check at [code: preprocessing/export_classification_stats.py#L341-L342], so the export stops rather than silently continuing.
- Unscaled neural-network inputs. The network sees raw statistics [code: ml/dataset.py#L96], including the lever statistics, whose scale varies with Force_X. Standardising first is an easy change, but the paper does not say whether it was done.
- Unscaled SVM by default. The README command leaves scaling off [code: README.md#L91]. The gamma grid goes down to 0.001, so results depend on scale.
- Model selection. Each fold's accuracy is taken from the last epoch [code: ml/train.py#L404-L424]. Fold checkpoints are saved train.py, but the final model is retrained from scratch with the chosen sizes [code: ml/train.py#L553-L556], so the fold checkpoints are not used.
- Learning rate. Cross-validation hardcodes 0.001 [code:
ml/train.py#L400] and ignores
--learning_rate. Only the final model uses it train.py. - Redundant log-softmax. The model returns log-probabilities, and the loss applies cross-entropy, which takes another log-softmax model.py train.py. Log-probabilities already sum to one, so the second pass changes nothing.
- Dataset needed. The traverse data are not in the repository. They are read
from
BASEPROD_TRAVERSE_PATH, defaulting to/mnt/baseprod/rover_sensorsbaseprod_helpers.py README.md. The dataset is at the DOI in the README README.md. The test folder has only a small CSV fixture. - Two labelled-traverse lists. The export imports
labelled_traverses[code: preprocessing/export_classification_stats.py#L49]. The module also haslabelled_traverses_arxiv, with different start and end times [code: preprocessing/baseprod_helpers.py#L98-L114]. The paper does not say which list produced its results. - Drawbar pull is unvalidated. The paper has no drawbar-pull ground truth. The force and torque limits used in the filter are not in the paper [code: preprocessing/find_usable.py#L145-L146].
- Sensor location. For wheel-interaction measurements the paper recommends mounting the sensors in the drive hub and correlating them with motor current. For slip detection it suggests current sensors, or FTS in the wheel hub §5.
Lineage
- Builds on: nothing with an arXiv id. Related work cited in §2 includes axle-mounted FTS terrain classification §2 and IMU-based terrain classification §2, without cards here.
- Compared against: the paper's own IMU-only, FTS-only and combined models §3.1 §3.2. No external baseline.
- Superseded by: not known.
Source and license
Adapted from “Field Assessment of Force Torque Sensors for Planetary Rover Navigation” by Levin Gerdes, Carlos Pérez del Pulgar, Raúl Castilla Arquillo, Martin Azkarate (arXiv:2411.04700), used under CC BY 4.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/spaceuma/fts-assessment at commit 42d6378da9f9 (MIT). Any quoted code is shown under that license.