Truth, measured and believed
The three epistemic categories a snapshot keeps apart, and the two differences between them that are worth measuring.
Why the schema separates them
A simulator can tell you exactly where a robot is. A robot cannot know that about itself. If both numbers live in the same struct under similar names, a control loop that accidentally reads the exact one works beautifully in simulation and fails on hardware, and nothing in the code looks wrong.
The message schema prevents that by construction, and the SDK's State mirrors it:
truth, measurement and belief sit in separate blocks, and no field appears in two of
them. Choosing a block is therefore a deliberate act. If your controller reads
kin.lin_pos, you have decided to use ground truth, and that decision is visible in
the source.
| Category | Blocks | Available on a real robot | What it is |
|---|---|---|---|
| truth | kin, wrench, env | no | simulator-exact values, the physics engine's own numbers |
| measured | sensors | yes | the noisy, robot-observable view of the same instant |
| believed | estimate | yes | what the robot's own filter has concluded |
| neither | actuator | yes | command in, realised motion out |
actuator is listed as none of the three on purpose. It is not a measurement of the
world and not a belief about it: it is the echo of what you commanded beside what the
device did. It has its own page, Actuators.
The two differences are the experiment
Because the blocks are published from the same instant, characterising a sensor or an estimator is a subtraction between two fields of one snapshot. Nothing has to be inferred, and no separate ground-truth log has to be aligned in time.
The SDK source states two of these outright:
| Difference | What it is |
|---|---|
estimate.kin - kin | the estimator error |
sensors.barometer.pressure - env.air_pressure | the barometer's error |
The same construction extends to every other device. sensors.gyroscope.angular_velocity - kin.ang_vel
is the gyro's noise realisation on that sample, because both are body-frame angular
rates in rad/s. sensors.gnss.geo_point against env.geo_point is the receiver's
position error.
Two cautions apply to every such diff. Both sides must be in the same frame, which is
not automatic when a device overrides the robot's axis_convention (see
Sensors). And the measured side must be fresh: a sensor slower than
the state stream republishes its previous reading, so a diff taken every sample
measures the same noise realisation several times over. Compare timestamps first, as
Timestamps and sequence numbers describes.
Reading the believed block
estimate has the same Kinematics shape as the truth block, plus its own clock, its
own frame, and a validity flag.
From examples/rust/src/bin/ex10_sensors_tour.rs:
#![allow(unused)] fn main() { // -- believed ------------------------------------------------------- let e = &s.estimate; println!( "BELIEVED estimate {} frame={:?}", stamp(e.valid, e.timestamp), e.coord_frame_id ); println!( " lin_pos {} m (estimate.kin - kin IS the error)", v3(e.kin.lin_pos) ); }
The same in C++ (examples/cpp/ex10_sensors_tour.cpp)
// -- believed -----------------------------------------------------
std::printf("BELIEVED estimate ");
stamp(r.estimate.valid, r.estimate.timestamp);
// The fixed C char arrays are NUL-terminated; the precision bounds
// the read even if a future field ever fills the buffer exactly.
std::printf(" frame=\"%.*s\"\n",
static_cast<int>(sizeof r.estimate.coord_frame_id - 1),
r.estimate.coord_frame_id);
v3("lin_pos", r.estimate.kin.lin_pos, "m", "(estimate.kin - kin IS the error)");
v3("lin_vel", r.estimate.kin.lin_vel, "m/s", "");
The same in Python (examples/python/ex10_sensors_tour.py)
# -- believed -------------------------------------------------------
e = s.estimate
print(
f"BELIEVED estimate {stamp(e.valid, e.timestamp)} "
f"frame={e.coord_frame_id!r}"
)
print(f" lin_pos {v3(e.kin.lin_pos)} m (estimate.kin - kin IS the error)")
print(f" lin_vel {v3(e.kin.lin_vel)} m/s")
C++ reaches the block through s.raw, the copied C struct, where the other two have named
fields on the snapshot: r.estimate.kin.lin_pos against s.estimate.kin.lin_pos. The
fields, the units and the subtraction that gives the estimator error are identical.
Today that always prints an invalid stamp, zeroed vectors and an empty frame id:
BELIEVED estimate [INVALID t=0.000] frame=""
lin_pos ( +0.000, +0.000, +0.000) m (estimate.kin - kin IS the error)
It is not a decode failure and not a dropped block: a missing nested table decodes to its
Default, which is all zeros, false and empty strings. The simulator runs no estimator
and omits the field on purpose, for the same reason this page opens with. An estimate that
silently mirrors truth would make estimate.kin - kin a tautology, so the block is absent
rather than filled from the truth block.
Publishing the belief yourself
The believed block is therefore yours to supply, and it travels on its own topic rather
than in the snapshot. publish_estimate puts a swarmbotix.states.EstimateState on
vrobots/{sys_id}/z/estimate, where the fixed wing reads it under FW_EST_OBSERVER.
Read sensors off z/state, run your filter, publish the result, and the subtraction
against kin measures a real estimator error. Publishing
estimates is that loop end to end.
Gotcha.
estimate.validis false until the filter converges, and an unconverged estimate that silently mirrors truth is the classic trap. A filter initialised from the simulator's own state reads as a perfect estimator for as long as nothing disturbs it, and your error metric reads zero because you are subtracting a number from itself. Gate every use ofestimateonestimate.valid, and treat an error of exactly zero as evidence of a bug rather than of quality.
Which block should your code read
| You are writing | Read | Because |
|---|---|---|
| a controller you intend to port to hardware | sensors, or estimate when valid | these are the only blocks a physical robot has |
| an estimator or filter under test | sensors in, kin only to score it | reading kin inside the filter invalidates the test |
| a sensor characterisation | both sides of one of the diffs above | the pair is published from the same instant |
| a plotting or debugging tool | anything | there is no port to fail |
Note. Nothing in the SDK stops a controller reading
kin. Early on that is often the right choice, because it separates a controller bug from a sensing problem. Make it a decision you can find later, not a default.
Next: Kinematics
See also: Publishing estimates, Frames, axes and units, A tour of the whole snapshot, Sensor noise