Fused Operations¶
Several common expression shapes — sin next to cos, exp(v) * cos(u),
sqrt(x) next to 1/sqrt(x) — share almost all of their recurrence work.
The fused surface computes such pairs in a single coupled recurrence pass
instead of two (or three) separate kernel calls plus a Cauchy product.
Every function on this page works for dense TE, named NE, and mixed-order
MTE expansions alike, and lives in namespace tax (reachable from
<tax/tax.hpp>). They are runtime-only (each seeds its recurrence with a
libm call at the constant term), not constexpr. Pair-returning functions
order the std::pair as spelled in the name: sinCos → {sin, cos},
sqrtInvSqrt → {sqrt, 1/sqrt}, expSinCos → {exp·sin, exp·cos}.
The fused surface at a glance¶
| Function | Computes | Typical speedup vs the composed spelling |
|---|---|---|
sinCos(x) |
{sin(x), cos(x)} |
~2x vs sin(x) + cos(x) |
sinhCosh(x) |
{sinh(x), cosh(x)} |
~1.8x vs sinh(x) + cosh(x) |
sqrtInvSqrt(x) |
{sqrt(x), 1/sqrt(x)} |
~1.3–1.5x univariate; parity at large multivariate sizes |
expSin(v, u) |
exp(v) * sin(u) |
~1.4–1.8x vs exp(v) * sin(u) |
expCos(v, u) |
exp(v) * cos(u) |
~1.4–1.8x vs exp(v) * cos(u) |
expSinCos(v, u) |
{exp(v)*sin(u), exp(v)*cos(u)} |
both results for the price of one fused pass |
halfPow<K>(x) |
x^(K/2) |
one pow recurrence instead of sqrt + power chain |
invSqrtPow<K>(x) |
x^(-K/2) |
likewise; invSqrtPow<3>(r2) is the 1/r³ gravity kernel |
pow<N>(x) |
x^N (compile-time integer) |
integer Cauchy chain, constexpr; ~1.2x vs real pow |
pow<Num,Den>(x) |
x^(Num/Den) |
reduces to the cheapest kernel; ~1.3x when it lands on the integer chain |
norm<P, Q>(v) |
‖v‖_P^Q (Q defaults to 1) |
P-norm of a vector as one series; raises once to Q/P (~1.6x vs re-raising on 1/r³) |
The speedups are measured on the library's own benchmarks; the exp·trig
fusion replaces three recurrences plus a Cauchy product (exp, the
coupled sin/cos, and the multiply) with one coupled pass.
Pair-returning functions and structured bindings¶
sinCos, sinhCosh, sqrtInvSqrt, and expSinCos return a
std::pair of expansions; structured bindings are the natural spelling:
#include <tax/tax.hpp>
auto x = tax::TE<8>::variable(0.7);
auto [s, c] = tax::sinCos(x); // {sin(x), cos(x)} in one pass
auto [sh, ch] = tax::sinhCosh(x); // {sinh(x), cosh(x)} — one shared exp pair
auto r2 = x * x + 1.0;
auto [r, ir] = tax::sqrtInvSqrt(r2); // {sqrt(r2), 1/sqrt(r2)}, requires r2.value() > 0
sin/cos were already computed by one coupled recurrence internally —
calling both tax::sin(x) and tax::cos(x) simply ran that recurrence
twice and discarded the companion each time. sinCos hands you both
results of the single pass, hence the ~2x.
sqrtInvSqrt only pays when BOTH outputs are consumed
The inverse square root costs one extra forward substitution on top of
the sqrt pass — cheap, but not free. If you need only one of the two,
call tax::sqrt(x) or invSqrtPow<K>(x) instead: computing the unused
companion is a measured net loss. The classic profitable case is a
radius where both r and 1/r-powers appear in the same formula.
Fused exp·trig¶
expSin, expCos, and expSinCos compute exp(v) times a trig function
of a different argument u — the damped-oscillation shape
\(e^{v}\sin u\) / \(e^{v}\cos u\) — in one coupled recurrence:
auto t = tax::TE<10>::variable(0.0);
auto v = -0.5 * t; // decay exponent
auto u = 3.0 * t; // phase
tax::TE<10> d = tax::expCos(v, u); // exp(v)*cos(u), one pass
auto [qs, qc] = tax::expSinCos(v, u); // both quadratures at once
Use expSin/expCos when a single output is needed, expSinCos when both
are — the coupled pass computes both internally either way.
Half-integer powers: halfPow<K> and invSqrtPow<K>¶
halfPow<K>(x) computes \(x^{K/2}\) for a compile-time integer K, picking
the cheapest correct path at compile time:
- Even
Kdispatches to the integer-power chain (binary exponentiation) — valid for negative constant terms too, and requiresx.value() != 0only whenKis negative. - Odd
Kruns the single real-exponentpowrecurrence — requiresx.value() > 0.
invSqrtPow<K>(x) is the spelling for \(x^{-K/2} = 1/\sqrt{x}^{\,K}\) with
K >= 1 (a static_assert enforces it); it requires x.value() > 0.
// Point-mass gravity: a = -mu * r / |r|^3, with r2 = x² + y² + z².
auto ir3 = tax::invSqrtPow<3>(r2); // r2^(-3/2) — one recurrence pass
auto ax = -mu * x * ir3;
auto v3 = tax::halfPow<3>(r2); // r2^(3/2) = |r|³
auto s = tax::halfPow<-4>(r2); // r2^(-2), integer chain: r2.value() < 0 is fine
One seriesPow pass is the fastest single-output spelling — it beats the
fused sqrtInvSqrt pair plus a power chain whenever only one output is
consumed. A caller that needs sqrt(x) alongside x^(-K/2) should
combine sqrtInvSqrt with pow instead.
Compile-time powers: pow<N> and pow<Num, Den>¶
halfPow/invSqrtPow are the Den == 2 face of a general compile-time power:
pow<N>(x)— integer power with the exponent in the type. Same integer Cauchy chain aspow(x, N),constexpr, no libm. Prefer it whenever the exponent is a constant.pow<Num, Den>(x)— the rational power \(x^{\text{Num}/\text{Den}}\). The exponent is reduced bygcdat compile time and bound to the cheapest kernel: the integer chain whenDen | Num(sopow<6,3>becomespow<2>, ~1.3× faster than a real-exponentpow), the sqrt/invsqrt chain when the reduced denominator is 2 (pow<3,2>==halfPow<3>,pow<-3,2>==invSqrtPow<3>), otherwise a single real-exponent recurrence (pow<2,5>== \(x^{2/5}\)).
For a genuine fractional exponent there is nothing faster than that single
seriesPow pass — it out-measures both a dedicated root (cbrt) and
sqrt-then-integer-power, because \(x^{c}\) obeys the same degree-by-degree
recurrence whatever c is; only the scalar constant term needs a libm seed.
Vector norms: norm<P, Q>¶
tax::norm<P, Q>(v) is the \(Q\)-th power of the \(P\)-norm of a vector of
expansions, as a single series. Q defaults to 1 (the plain \(P\)-norm) and
P to the Euclidean 2-norm:
The input is an Eigen column vector of expansions (as returned by
tax::la::variables) or any range of them (e.g. the std::array from
tax::variables); the elements may be dense TE, named NE, or mixed MTE.
Eigen::Vector3d r0{3.0, 4.0, 12.0};
auto r = tax::la::variables<tax::TE<8, 3>>(r0); // vector of coordinate variables
auto len = tax::norm(r); // sqrt(x²+y²+z²) — value 13 at r0 (P=2, Q=1)
auto n3 = tax::norm<3>(r); // (Σ vᵢ³)^(1/3) — the 3-norm
auto ir3 = tax::norm<2, -3>(r); // 1/|r|³ — the gravity kernel
The single norm<P, Q> is the fused spelling: it raises the accumulated
power-sum once to Q/P (via pow<Q, P>), rather than taking the root and
re-raising — one recurrence pass instead of two. On the 1/|r|³ gravity
kernel that is 1.64× faster than pow(norm(r), -3), and for P == 2 it
binds straight to invSqrtPow, so norm<2,-3> is bit-identical to
invSqrtPow<3>(x² + y² + z²).
What norm<P> assumes
It computes \((\sum_i v_i^{P})^{1/P}\), which is the true \(P\)-norm when the
summands are non-negative — even P, or a vector with positive components
at the expansion point. It requires \(\sum_i v_i^{P} > 0\) at the expansion
point (abs is not smooth, so the odd-P signed power-sum is used as-is).
Named and mixed-order overloads¶
The whole fused surface exists for NamedTaylorExpansion and
MixedTaylorExpansion as well. Single-operand forms (sinCos, sinhCosh,
sqrtInvSqrt, halfPow<K>, invSqrtPow<K>) preserve the operand's axis
set; the two-operand forms (expSin, expCos, expSinCos) compose in the
union of the operands' axis sets, exactly like operator* and atan2:
auto t = tax::variable<"t", 8>(0.0); // axes {t}
auto w = tax::variable<"w", 8>(3.0); // axes {w}
auto [qs, qc] = tax::expSinCos(-0.5 * t, w * t); // both over axes {t, w}
auto g = tax::invSqrtPow<3>(r2); // axis set of r2, unchanged
Mixed-order operands additionally follow the usual max-order promotion on shared axes — see Named & Mixed-Order Expansions.
When not to fuse¶
- Only one output consumed → use the single-output function
(
sin,sqrt,halfPow). The single-output kernels deliberately do not write a discarded companion. - Large multivariate shapes with
sqrtInvSqrt→ the win shrinks to parity as the Cauchy-product work dominates; it never becomes a loss when both outputs are used, but don't expect the univariate speedup.
Next: the exact signatures and return types are tabulated in the Core API Reference; the coupled recurrences behind these kernels are derived in Internals / Recurrence Relations.