Quaternion Interpolation — Robotics/Orientation 3D
Robotics/Orientation_3D/Quaternion_Interpolation · 3 input / 1 output port(s) at insert · exports to Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text
Description#
The block's own DESCRIPTION_HTML, rendered verbatim — the same text the config dialog's info panel and the library navigator show. Fix a wrong sentence in the block's .cpp (R-D9), never here.
Quaternion Interpolation
Robotics / Orientation 3D
Interpolates between two orientations by a fraction that arrives on a port, by SLERP, LERP or NLERP. All three reduce to two scalars:
y = A·p + B·q
Writing d for the angle cosine between the two normalized quaternions and θ = arccos(d):
- SLERP – constant angular rate along the great circle: A = sin((1−f)θ) / (sinθ·|p|), B = ±sin(fθ) / (sinθ·|q|).
- LERP – the straight chord, not renormalized: A = (1−f)/|p|, B = ±f/|q|. The result is a quaternion of length less than one everywhere except the endpoints.
- NLERP – the same chord, brought back to the unit sphere. Its divisor is a scalar too: L = √((1−f)² + 2(1−f)f·d + f²).
Four things happen before any interpolation, and each one changes the answer:
- Both inputs are normalized. At f = 0 the output is p/|p|, not p.
- The shortest path is taken. If the two normalized quaternions point away from each other, the second is negated – the same rotation, the short way round – so at f = 1 the output is then −q/|q|.
- f is clamped to [0, 1]. Outside that range the output is the endpoint, not an extrapolation.
- LERP is not renormalized; NLERP is. At f = 0.5 NLERP and SLERP agree exactly.
Ports
- p – the starting orientation, a [4,1] column [w x y z]T, scalar-first. It need not be unit; the block normalizes it.
- q – the ending orientation, also [4,1] and also normalized by the block.
- f – the interpolation fraction, a [1,1] scalar, clamped to [0, 1].
- q(f) – the interpolated quaternion, [4,1]. Its size is fixed, not inherited. It is unit for SLERP and NLERP and shorter than unit for LERP.
Parameters
- Interpolation Method – which of the three runs. Changing it
changes which arithmetic runs, not just its constants:
- SLERP – the default. Constant angular rate, one arccosine and two sines per sample.
- LERP – the cheapest: no trigonometry at all, and the output dips inside the unit sphere between the endpoints.
- NLERP – LERP brought back to unit length. Unit like SLERP, but its angular rate is not constant – it lingers near the endpoints.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text. Nothing is exposed as a tunable parameter: the method is structural, so only the selected one is emitted, and a core exported for LERP carries no trigonometry at all.
The near-parallel case is handled without a branch: the half-angle is floored at 1e−9 rather than tested, because the floored SLERP weights and the LERP weights agree to about 1e−18 there and LERP between two coincident unit quaternions is already the right answer. The clamp of d and that floor are both written as 0.5(a + b ± |a − b|) – ten languages spell an inline conditional ten ways, and VHDL has none at all.
The three HDL targets are simulation-only real arithmetic: two square roots, an arccosine, two sines and three divisions are not a fixed-point datapath. In VHDL the normalized p and the two mixing scalars are staged through Q16.16 as well, because a VHDL process body has no real variable to hold them; the other two hardware targets declare their own and stay in double precision throughout.
Simulink bridge
Both directions, onto aerolibutil/Quaternion Interpolation
in the Aerospace Blockset. Interpolation Method maps to
method and the three values are spelled identically on both sides,
so the round trip is lossless.
The Simulink block also carries an action parameter that decides
what it does when an input is not already normalized – and its default is
Error. This block normalizes silently and has no counterpart for that, so
an export pins action to None: the exported model then
behaves as this one does. An import of a model whose block was set to
Warning or Error loses that setting, which is worth
knowing because it turns a stopped simulation into a running one.
The block defines no SampleTime parameter, so the rate
stays on the ICore side and a block configured with an explicit positive rate
reports that the rate did not cross.
Notes
- Algebraic and stateless: the output depends only on the current inputs, so the block cannot break an algebraic loop.
- A zero-length input has no direction, so p = 0 or q = 0 divides by zero and the output is not a number. There is nothing sensible to answer instead – an orientation of length zero is not an orientation.
- Deliberately no state space. The output divides by the inputs' norms, so it is not linear in them and model reduction is right to refuse the block.
- To interpolate along a path of more than two orientations, chain the block – each segment is its own pair – rather than expecting one block to carry a spline.
Code facts#
| Fact | Value |
|---|---|
| registered type | Robotics/Orientation_3D/Quaternion_Interpolation |
| family | Robotics/Orientation_3D |
| solver environment class | ICoreBlock_0_Robotics_1_Orientation_3D_2_Quaternion_Interpolation |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Robotics/Orientation_3D/Quaternion_Interpolation/ICoreBlock_0_Robotics_1_Orientation_3D_2_Quaternion_Interpolation.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Robotics/Orientation_3D/Quaternion_Interpolation/ICoreBlock_0_Robotics_1_Orientation_3D_2_Quaternion_Interpolation.h |
| default size on canvas | 120 × 96 px |
| ports at insert | 3 in, 1 out |
| code generators implemented | Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text |
Ports#
| # | Direction | Signal type | Description label |
|---|---|---|---|
| 1 | in | ICoreDouble | p |
| 2 | in | ICoreDouble | q |
| 3 | in | ICoreDouble | f |
| 4 | out | ICoreDouble | q(f) |
Ports the constructor creates. A block whose port list changes with its configuration adds or removes ports at load time; the count above is the one a freshly inserted block has.
Configuration variables#
| Config variable | Default | Simulink parameter |
|---|---|---|
Interpolation Method | SLERP%~%LERP%~%NLERP~~SLERP | method |
Every block also carries Sampling Time (s) from ICoreBlockSolverEnvironment: zero or less inherits the solver's rate, a positive value runs the block at that period.
Simulink bridge#
| support | Support::Both |
| Simulink path | aerolibutil/Quaternion Interpolation |
| port-count rule | PortsParam::None |
SampleTime parameter | no — the counterpart defines none; the rate stays on the ICore side |
| always set | action = None |
| ICore config | Simulink parameter | Value translation |
|---|---|---|
Interpolation Method | method | SLERP → SLERP, LERP → LERP, NLERP → NLERP |
Caveat (shown to the user): the three methods are spelled identically on both sides. ⚠ The Simulink block's "action" parameter decides what it does when an input is not already normalized and DEFAULTS TO Error; this block normalizes silently and has no counterpart, so an export pins action to None and the exported model behaves as this one does. An IMPORT loses a Warning or Error setting, which turns a stopped simulation into a running one. Note the block defines no SampleTime parameter: an ICore rate set explicitly stays on this side and is reported
Catalog contract: src/ICoreBlocks/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The checker has a blind spot here — it could not resolve something (a grouped port bullet, a computed config name), which is reported and never counted as a pass. A reader has to settle it:
B0no sample under docs/generated/samples/ — nothing to cross-check (P8.1)
The verdict above is
tools/docs/check_block_descriptions.py(P7.1), which compares LISTS. It cannot read a sentence: "stateless" on a block with a state, an initial-value semantic the recursion does not implement, a "not synthesizable" caveat the HDL banner contradicts. That is the agent audit (P7.3) on BLOCK_DESCRIPTION_AUDIT.md, and this tool's green is not a substitute for one.
File banner (developer view)#
The top comment of the block's .cpp — the maths, the realization and the export strategy, addressed to whoever changes it. It must not contradict the description above (P7.5).
Quaternion Interpolation -- SLERP, LERP or NLERP between two orientations Three inputs (p, q as [4,1] scalar-first quaternions; f as a [1,1] fraction) and one [4,1] output. Algebraic and stateless.
ALL THREE METHODS ARE ONE SHAPE: y = A*p + B*q, A and B two scalars built from np = |p|, nq = |q|, the dot product, and f. Writing them that way is what keeps the block one implementation across ten backends instead of three, and what keeps the emitted code small -- the norms fold into the scalars, so no target ever builds an intermediate quaternion and then normalizes it.
sg = sign of the dot product (the SHORTEST-PATH flip) d = |dot| / (np*nq), clamped to 1 f clamped to [0, 1]
LERP A = (1-f)/np B = sg*f/nq NLERP A = (1-f)/(np*L) B = sg*f/(nq*L), L = sqrt((1-f)^2 + 2(1-f)f d + f^2) SLERP A = sin((1-f)th)/(s*np) B = sg*sin(f*th)/(s*nq), th = acos(d), s = sin(th)
NLERP's divisor is a scalar because |(1-f)p^ + f q^| = sqrt((1-f)^2 + 2(1-f)f d + f^2) when both are unit -- so even the renormalization costs one square root rather than a vector.
⚠ FOUR PREPARATION STEPS, ALL MEASURED against aerolibutil/Quaternion Interpolation in R2026a on 2026-09-10, and each one silently changes the answer if it is left out:
- BOTH INPUTS ARE NORMALIZED FIRST. At f = 0 the block answers p/|p|, not p.
- THE SHORTEST PATH IS TAKEN. Measured with a pair whose normalized dot product is
-0.2239: the answer at f = 1 is MINUS q/|q|, component for component.
- f IS CLAMPED to [0, 1]. Measured at f = -0.3 and f = 1.4: the answers are exactly the
f = 0 and f = 1 answers, not extrapolations.
- LERP IS NOT RENORMALIZED and NLERP is. At f = 0.5 NLERP and SLERP then coincide to the
last bit, which is a useful check on any implementation of either.
⚠ THE NEAR-PARALLEL CASE IS HANDLED WITHOUT A BRANCH, by flooring the half-angle at 1e-9 rather than testing sin(th) against a tolerance. As th -> 0 the SLERP weights tend to (1-f) and f, which is LERP, and LERP between two coincident unit quaternions is already the right answer and already unit -- so the floored form agrees with a branch to about 1e-18 and every backend carries one code path instead of two. Both the clamp of d and the floor of th are written as min/max through 0.5*(a+b -/+ |a-b|), for the same reason: ten languages spell an inline conditional ten ways, and VHDL has none at all.
MEASURED END TO END against the Simulink block: seven quaternion pairs (including
Sample results#
No sample run is committed for this block. Samples come from the headless harness (DOCS_PLAN.md P8.1) into docs/generated/samples/; until one exists this block's behaviour is witnessed by the parity and export-verification suites, not by a plot here.