// Rate conversion and pose algebra for canonical ActionCodec2 trajectories. // // A port of the runtime's ``physical/semantic.py`` resampling and pose helpers, // including the scipy pieces they call (natural CubicSpline, PchipInterpolator, // PPoly evaluation, RotationSpline). Operations follow the NumPy/scipy order // of evaluation, and the LAPACK solvers are the reference algorithms, so // results agree with the Python runtime to the last bits of float64; the // float32 outputs therefore match except where a value sits on a rounding // boundary. Batch entry points run rows in parallel without the GIL. #include #include #include #include #include #include #include #include #include #include "parallel.hpp" namespace py = pybind11; namespace { using Vec = std::vector; struct Quat { double x, y, z, w; }; // ---- runtime ``_so3`` quaternion math (xyzw), NumPy evaluation order ---- Quat so3_normalize(const Quat& q) { const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); if (norm < 1e-12) throw std::invalid_argument("a pose contains a zero quaternion"); return {q.x / norm, q.y / norm, q.z / norm, q.w / norm}; } Quat so3_multiply(const Quat& a, const Quat& b) { const double cx = a.y * b.z - a.z * b.y; const double cy = a.z * b.x - a.x * b.z; const double cz = a.x * b.y - a.y * b.x; return so3_normalize({a.w * b.x + b.w * a.x + cx, a.w * b.y + b.w * a.y + cy, a.w * b.z + b.w * a.z + cz, a.w * b.w - (a.x * b.x + a.y * b.y + a.z * b.z)}); } Quat so3_from_rotvec(double x, double y, double z) { const double angle = std::sqrt(x * x + y * y + z * z); const double scale = angle >= 1e-12 ? std::sin(0.5 * angle) / angle : 0.5; return so3_normalize({x * scale, y * scale, z * scale, std::cos(0.5 * angle)}); } void so3_to_rotvec(const Quat& raw, double* out) { Quat q = so3_normalize(raw); if (q.w < 0.0) q = {-q.x, -q.y, -q.z, -q.w}; const double vector_norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z); const double angle = 2.0 * std::atan2(vector_norm, std::min(std::max(q.w, 0.0), 1.0)); const double scale = vector_norm >= 1e-12 ? angle / vector_norm : 2.0; out[0] = q.x * scale; out[1] = q.y * scale; out[2] = q.z * scale; } // ---- scipy ``Rotation`` operations used by RotationSpline ---- Quat sp_from_quat(const Quat& q) { const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); if (!(norm > 0.0)) throw std::invalid_argument("found zero norm quaternions in `quat`"); return {q.x / norm, q.y / norm, q.z / norm, q.w / norm}; } Quat sp_inv(const Quat& q) { return {-q.x, -q.y, -q.z, q.w}; } Quat sp_compose(const Quat& p, const Quat& q) { const double cx = p.y * q.z - p.z * q.y; const double cy = p.z * q.x - p.x * q.z; const double cz = p.x * q.y - p.y * q.x; const Quat r{p.w * q.x + q.w * p.x + cx, p.w * q.y + q.w * p.y + cy, p.w * q.z + q.w * p.z + cz, p.w * q.w - (p.x * q.x + p.y * q.y + p.z * q.z)}; return sp_from_quat(r); } void sp_as_rotvec(Quat q, double* out) { if (q.w < 0.0) q = {-q.x, -q.y, -q.z, -q.w}; const double angle = 2.0 * std::atan2(std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z), q.w); double scale; if (angle <= 1e-3) { const double angle2 = angle * angle; scale = 2.0 + angle2 / 12.0 + 7.0 * angle2 * angle2 / 2880.0; } else { scale = angle / std::sin(angle / 2.0); } out[0] = scale * q.x; out[1] = scale * q.y; out[2] = scale * q.z; } Quat sp_from_rotvec(const double* v) { const double angle = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); double scale; if (angle <= 1e-3) { const double angle2 = angle * angle; scale = 0.5 - angle2 / 48.0 + angle2 * angle2 / 3840.0; } else { scale = std::sin(angle / 2.0) / angle; } return {scale * v[0], scale * v[1], scale * v[2], std::cos(angle / 2.0)}; } // ---- piecewise cubics (scipy PPoly with CubicHermiteSpline coefficients) ---- // PPoly interval: x[i] <= t < x[i+1], extrapolating with the end intervals. int64_t ppoly_interval(const Vec& x, double t) { const int64_t n = static_cast(x.size()); if (t >= x[n - 1]) return n - 2; if (t < x[0]) return 0; return static_cast(std::upper_bound(x.begin(), x.end(), t) - x.begin()) - 1; } // scipy ``evaluate_poly1`` for a cubic: sum of c[k-1-p] * s**p, powers built by // repeated multiplication. double ppoly_value(const double* c, double s) { double result = 0.0, z = 1.0; result = result + c[3] * z; z *= s; result = result + c[2] * z; z *= s; result = result + c[1] * z; z *= s; result = result + c[0] * z; return result; } // Hermite coefficients per interval and column: c[(k*4 + p)*cols + a] with // p = 0..3 the descending powers, as CubicHermiteSpline stacks them. Vec hermite_coefficients(const Vec& x, const Vec& y, const Vec& slopes, int64_t cols) { const int64_t n = static_cast(x.size()); Vec c(static_cast((n - 1) * 4 * cols)); for (int64_t k = 0; k < n - 1; ++k) { const double dx = x[k + 1] - x[k]; for (int64_t a = 0; a < cols; ++a) { const double y0 = y[k * cols + a], y1 = y[(k + 1) * cols + a]; const double d0 = slopes[k * cols + a], d1 = slopes[(k + 1) * cols + a]; const double slope = (y1 - y0) / dx; const double t = (d0 + d1 - 2.0 * slope) / dx; c[(k * 4 + 0) * cols + a] = t / dx; c[(k * 4 + 1) * cols + a] = (slope - d0) / dx - t; c[(k * 4 + 2) * cols + a] = d0; c[(k * 4 + 3) * cols + a] = y0; } } return c; } void ppoly_evaluate(const Vec& x, const Vec& c, int64_t cols, const Vec& times, int64_t out_cols, int64_t col_offset, double* out) { for (size_t i = 0; i < times.size(); ++i) { const int64_t k = ppoly_interval(x, times[i]); const double s = times[i] - x[k]; for (int64_t a = 0; a < cols; ++a) { const double coeff[4] = {c[(k * 4 + 0) * cols + a], c[(k * 4 + 1) * cols + a], c[(k * 4 + 2) * cols + a], c[(k * 4 + 3) * cols + a]}; out[i * out_cols + col_offset + a] = ppoly_value(coeff, s); } } } // LAPACK dgtsv (reference algorithm) for several right-hand sides b[n][cols]. void gtsv(Vec dl, Vec d, Vec du, Vec& b, int64_t cols) { const int64_t n = static_cast(d.size()); auto pivot = [&](int64_t i, bool last) { if (std::fabs(d[i]) >= std::fabs(dl[i])) { if (d[i] == 0.0) throw std::invalid_argument("singular matrix"); const double fact = dl[i] / d[i]; d[i + 1] = d[i + 1] - fact * du[i]; for (int64_t j = 0; j < cols; ++j) b[(i + 1) * cols + j] = b[(i + 1) * cols + j] - fact * b[i * cols + j]; if (!last) dl[i] = 0.0; } else { const double fact = d[i] / dl[i]; d[i] = dl[i]; const double temp = d[i + 1]; d[i + 1] = du[i] - fact * temp; if (!last) { dl[i] = du[i + 1]; du[i + 1] = -fact * dl[i]; } du[i] = temp; for (int64_t j = 0; j < cols; ++j) { const double swap = b[i * cols + j]; b[i * cols + j] = b[(i + 1) * cols + j]; b[(i + 1) * cols + j] = swap - fact * b[(i + 1) * cols + j]; } } }; for (int64_t i = 0; i + 2 < n; ++i) pivot(i, false); if (n > 1) pivot(n - 2, true); if (d[n - 1] == 0.0) throw std::invalid_argument("singular matrix"); for (int64_t j = 0; j < cols; ++j) { b[(n - 1) * cols + j] = b[(n - 1) * cols + j] / d[n - 1]; if (n > 1) b[(n - 2) * cols + j] = (b[(n - 2) * cols + j] - du[n - 2] * b[(n - 1) * cols + j]) / d[n - 2]; for (int64_t i = n - 3; i >= 0; --i) { b[i * cols + j] = (b[i * cols + j] - du[i] * b[(i + 1) * cols + j] - dl[i] * b[(i + 2) * cols + j]) / d[i]; } } } // scipy CubicSpline(bc_type="natural") slopes at the knots. Vec natural_cubic_slopes(const Vec& x, const Vec& y, int64_t cols) { const int64_t n = static_cast(x.size()); Vec dx(n - 1), slope((n - 1) * cols); for (int64_t k = 0; k < n - 1; ++k) { dx[k] = x[k + 1] - x[k]; for (int64_t a = 0; a < cols; ++a) slope[k * cols + a] = (y[(k + 1) * cols + a] - y[k * cols + a]) / dx[k]; } // Banded A (3 x n): A[0] superdiagonal, A[1] diagonal, A[2] subdiagonal. Vec upper(n, 0.0), diagonal(n, 0.0), lower(n, 0.0), b(n * cols); for (int64_t k = 1; k < n - 1; ++k) diagonal[k] = 2.0 * (dx[k - 1] + dx[k]); for (int64_t k = 2; k < n; ++k) upper[k] = dx[k - 2]; for (int64_t k = 0; k + 2 < n; ++k) lower[k] = dx[k + 1]; for (int64_t k = 1; k < n - 1; ++k) { for (int64_t a = 0; a < cols; ++a) b[k * cols + a] = 3.0 * (dx[k] * slope[(k - 1) * cols + a] + dx[k - 1] * slope[k * cols + a]); } // Natural ends: second derivative zero (bc value 0.0). diagonal[0] = 2.0 * dx[0]; upper[1] = dx[0]; diagonal[n - 1] = 2.0 * dx[n - 2]; lower[n - 2] = dx[n - 2]; for (int64_t a = 0; a < cols; ++a) { b[a] = -0.5 * 0.0 * (dx[0] * dx[0]) + 3.0 * (y[cols + a] - y[a]); b[(n - 1) * cols + a] = 0.5 * 0.0 * (dx[n - 2] * dx[n - 2]) + 3.0 * (y[(n - 1) * cols + a] - y[(n - 2) * cols + a]); } Vec dl(lower.begin(), lower.end() - 1), du(upper.begin() + 1, upper.end()); gtsv(dl, diagonal, du, b, cols); return b; } double sign(double v) { return v > 0.0 ? 1.0 : (v < 0.0 ? -1.0 : (v == 0.0 ? 0.0 : v)); } double pchip_edge(double h0, double h1, double m0, double m1) { double d = ((2.0 * h0 + h1) * m0 - h0 * m1) / (h0 + h1); const bool mask = sign(d) != sign(m0); const bool mask2 = (sign(m0) != sign(m1)) && (std::fabs(d) > 3.0 * std::fabs(m0)); if (mask) d = 0.0; else if (mask2) d = 3.0 * m0; return d; } // scipy PchipInterpolator derivatives at the knots. Vec pchip_slopes(const Vec& x, const Vec& y, int64_t cols) { const int64_t n = static_cast(x.size()); Vec h(n - 1), m((n - 1) * cols), dk(n * cols, 0.0); for (int64_t k = 0; k < n - 1; ++k) { h[k] = x[k + 1] - x[k]; for (int64_t a = 0; a < cols; ++a) m[k * cols + a] = (y[(k + 1) * cols + a] - y[k * cols + a]) / h[k]; } if (n == 2) { for (int64_t a = 0; a < cols; ++a) dk[a] = dk[cols + a] = m[a]; return dk; } for (int64_t k = 1; k < n - 1; ++k) { const double w1 = 2.0 * h[k] + h[k - 1]; const double w2 = h[k] + 2.0 * h[k - 1]; for (int64_t a = 0; a < cols; ++a) { const double left = m[(k - 1) * cols + a], right = m[k * cols + a]; const bool condition = sign(right) != sign(left) || right == 0.0 || left == 0.0; if (condition) { dk[k * cols + a] = 0.0; } else { const double whmean = (w1 / left + w2 / right) / (w1 + w2); dk[k * cols + a] = 1.0 / whmean; } } } for (int64_t a = 0; a < cols; ++a) { dk[a] = pchip_edge(h[0], h[1], m[a], m[cols + a]); dk[(n - 1) * cols + a] = pchip_edge(h[n - 2], h[n - 3], m[(n - 2) * cols + a], m[(n - 3) * cols + a]); } return dk; } // ---- scipy RotationSpline ---- using Mat3 = std::array; Mat3 skew(const double* v) { return {0.0, -v[2], v[1], v[2], 0.0, -v[0], -v[1], v[0], 0.0}; } Mat3 matmul(const Mat3& a, const Mat3& b) { Mat3 r{}; for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) r[i * 3 + j] = a[i * 3 + 0] * b[0 * 3 + j] + a[i * 3 + 1] * b[1 * 3 + j] + a[i * 3 + 2] * b[2 * 3 + j]; return r; } void matvec(const Mat3& a, const double* v, double* out) { for (int i = 0; i < 3; ++i) out[i] = a[i * 3 + 0] * v[0] + a[i * 3 + 1] * v[1] + a[i * 3 + 2] * v[2]; } double norm3(const double* v) { return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); } void cross(const double* a, const double* b, double* out) { out[0] = a[1] * b[2] - a[2] * b[1]; out[1] = a[2] * b[0] - a[0] * b[2]; out[2] = a[0] * b[1] - a[1] * b[0]; } Mat3 rate_to_rotvec_dot(const double* rotvec) { const double nm = norm3(rotvec); const double k = nm > 1e-4 ? (1.0 - 0.5 * nm / std::tan(0.5 * nm)) / (nm * nm) : 1.0 / 12.0 + 1.0 / 720.0 * (nm * nm); const Mat3 s = skew(rotvec), ss = matmul(s, s); Mat3 r{}; for (int i = 0; i < 9; ++i) r[i] = ((i % 4 == 0 ? 1.0 : 0.0) + 0.5 * s[i]) + k * ss[i]; return r; } Mat3 rotvec_dot_to_rate(const double* rotvec) { const double nm = norm3(rotvec); double k1, k2; if (nm > 1e-4) { k1 = (1.0 - std::cos(nm)) / (nm * nm); k2 = (nm - std::sin(nm)) / std::pow(nm, 3.0); } else { k1 = 0.5 - (nm * nm) / 24.0; k2 = 1.0 / 6.0 - (nm * nm) / 120.0; } const Mat3 s = skew(rotvec), ss = matmul(s, s); Mat3 r{}; for (int i = 0; i < 9; ++i) r[i] = ((i % 4 == 0 ? 1.0 : 0.0) - k1 * s[i]) + k2 * ss[i]; return r; } void nonlinear_term(const double* rv, const double* rvd, double* out) { const double nm = norm3(rv); const double dp = rv[0] * rvd[0] + rv[1] * rvd[1] + rv[2] * rvd[2]; double cp[3], ccp[3], dccp[3]; cross(rv, rvd, cp); cross(rv, cp, ccp); cross(rvd, cp, dccp); double k1, k2, k3; if (nm > 1e-4) { const double sn = std::sin(nm), cs = std::cos(nm); k1 = (-nm * sn - 2.0 * (cs - 1.0)) / std::pow(nm, 4.0); k2 = (-2.0 * nm + 3.0 * sn - nm * cs) / std::pow(nm, 5.0); k3 = (nm - sn) / std::pow(nm, 3.0); } else { k1 = 1.0 / 12.0 - (nm * nm) / 180.0; k2 = -1.0 / 60.0 + (nm * nm) / 12604.0; k3 = 1.0 / 6.0 - (nm * nm) / 120.0; } for (int i = 0; i < 3; ++i) out[i] = dp * (k1 * cp[i] + k2 * ccp[i]) + k3 * dccp[i]; } // LAPACK dgbsv (reference dgbtf2 + dgbtrs) for one right-hand side. ``ab`` // holds A in band storage with ``kl`` extra rows for fill-in: ldab = // 2*kl+ku+1 and ab[(kl+ku+i-j) + ldab*j] = A(i, j), all zero-based. void gbsv(int64_t n, int64_t kl, int64_t ku, Vec& ab, Vec& b) { const int64_t ldab = 2 * kl + ku + 1, kv = ku + kl; auto at = [&](int64_t row, int64_t col) -> double& { return ab[row + ldab * col]; }; std::vector pivots(n); int64_t ju = 0; for (int64_t j = 0; j < n; ++j) { const int64_t km = std::min(kl, n - 1 - j); int64_t jp = 0; double best = std::fabs(at(kv, j)); for (int64_t i = 1; i <= km; ++i) { if (std::fabs(at(kv + i, j)) > best) { best = std::fabs(at(kv + i, j)); jp = i; } } pivots[j] = jp + j; if (at(kv + jp, j) == 0.0) throw std::invalid_argument("singular matrix"); ju = std::max(ju, std::min(j + ku + jp, n - 1)); if (jp != 0) { for (int64_t c = 0; c <= ju - j; ++c) std::swap(at(kv + jp - c, j + c), at(kv - c, j + c)); } if (km > 0) { const double reciprocal = 1.0 / at(kv, j); for (int64_t i = 1; i <= km; ++i) at(kv + i, j) = reciprocal * at(kv + i, j); for (int64_t c = 1; c <= ju - j; ++c) { const double y = at(kv - c, j + c); if (y != 0.0) { const double temp = -1.0 * y; for (int64_t i = 1; i <= km; ++i) at(kv - c + i, j + c) = at(kv - c + i, j + c) + at(kv + i, j) * temp; } } } } // Forward: L, with the row interchanges. for (int64_t j = 0; j + 1 < n; ++j) { const int64_t lm = std::min(kl, n - 1 - j); if (pivots[j] != j) std::swap(b[pivots[j]], b[j]); if (b[j] != 0.0) { const double temp = -1.0 * b[j]; for (int64_t i = 1; i <= lm; ++i) b[j + i] = b[j + i] + at(kv + i, j) * temp; } } // Backward: U, a band of width kl+ku above the diagonal (dtbsv). const int64_t k = kl + ku; for (int64_t j = n - 1; j >= 0; --j) { if (b[j] != 0.0) { b[j] = b[j] / at(k, j); const double temp = b[j]; for (int64_t i = j - 1; i >= std::max(0, j - k); --i) b[i] = b[i] - temp * at(k + i - j, j); } } } struct RotationSpline { Vec times; std::vector rotations; Vec coefficients; // ppoly layout, 3 columns RotationSpline(const Vec& knots, const std::vector& raw) : times(knots) { const int64_t n = static_cast(knots.size()); rotations.reserve(n); for (const Quat& q : raw) rotations.push_back(sp_from_quat(q)); const int64_t m = n - 1; Vec dt(m), rotvecs(m * 3), rates(m * 3), rotvecs_dot; for (int64_t k = 0; k < m; ++k) { dt[k] = times[k + 1] - times[k]; sp_as_rotvec(sp_compose(sp_inv(rotations[k]), rotations[k + 1]), &rotvecs[k * 3]); for (int a = 0; a < 3; ++a) rates[k * 3 + a] = rotvecs[k * 3 + a] / dt[k]; } if (n == 2) { rotvecs_dot = rates; } else { solve_rates(dt, rates, rotvecs, rotvecs_dot); } coefficients.assign(m * 4 * 3, 0.0); for (int64_t k = 0; k < m; ++k) { for (int a = 0; a < 3; ++a) { const double rv = rotvecs[k * 3 + a], ar = rates[k * 3 + a], rd = rotvecs_dot[k * 3 + a]; coefficients[(k * 4 + 0) * 3 + a] = (-2.0 * rv + dt[k] * ar + dt[k] * rd) / std::pow(dt[k], 3.0); coefficients[(k * 4 + 1) * 3 + a] = (3.0 * rv - 2.0 * dt[k] * ar - dt[k] * rd) / (dt[k] * dt[k]); coefficients[(k * 4 + 2) * 3 + a] = ar; coefficients[(k * 4 + 3) * 3 + a] = 0.0; } } } // scipy RotationSpline._solve_for_angular_rates; ``rates`` comes back as the // knot angular rates, ``rotvecs_dot`` as the segment rotation-vector rates. static void solve_rates(const Vec& dt, Vec& rates, const Vec& rotvecs, Vec& rotvecs_dot) { const int64_t m = static_cast(dt.size()); const double first[3] = {rates[0], rates[1], rates[2]}; std::vector a(m), a_inv(m); for (int64_t k = 0; k < m; ++k) { a[k] = rate_to_rotvec_dot(&rotvecs[k * 3]); a_inv[k] = rotvec_dot_to_rate(&rotvecs[k * 3]); } // Block-tridiagonal system of m-1 unknown 3-vectors in (5, 5) band form. const int64_t size = 3 * (m - 1), kl = 5, ku = 5, ldab = 2 * kl + ku + 1; Vec band(ldab * size, 0.0); auto set = [&](int64_t i, int64_t j, double value) { band[(kl + ku + i - j) + ldab * j] = value; }; for (int64_t k = 0; k < m - 2; ++k) { for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { set(3 * (k + 1) + r, 3 * k + c, 2.0 * a_inv[k + 1][r * 3 + c] / dt[k + 1]); set(3 * k + r, 3 * (k + 1) + c, 2.0 * a[k + 1][r * 3 + c] / dt[k + 1]); } } } for (int64_t k = 0; k < m - 1; ++k) { const double diagonal = 4.0 * (1.0 / dt[k] + 1.0 / dt[k + 1]); for (int r = 0; r < 3; ++r) set(3 * k + r, 3 * k + r, diagonal); } Vec b0(size); for (int64_t k = 0; k < m - 1; ++k) { for (int r = 0; r < 3; ++r) b0[3 * k + r] = 6.0 * (rotvecs[k * 3 + r] * std::pow(dt[k], -2.0) + rotvecs[(k + 1) * 3 + r] * std::pow(dt[k + 1], -2.0)); } double product[3]; matvec(a_inv[0], first, product); for (int r = 0; r < 3; ++r) b0[r] -= 2.0 / dt[0] * product[r]; matvec(a[m - 1], &rates[(m - 1) * 3], product); for (int r = 0; r < 3; ++r) b0[size - 3 + r] -= 2.0 / dt[m - 1] * product[r]; rotvecs_dot.assign(m * 3, 0.0); for (int iteration = 0; iteration < 10; ++iteration) { for (int64_t k = 0; k < m; ++k) matvec(a[k], &rates[k * 3], &rotvecs_dot[k * 3]); Vec b(size); for (int64_t k = 0; k < m - 1; ++k) { double delta_beta[3]; nonlinear_term(&rotvecs[k * 3], &rotvecs_dot[k * 3], delta_beta); for (int r = 0; r < 3; ++r) b[3 * k + r] = b0[3 * k + r] - delta_beta[r]; } Vec work = band; gbsv(size, kl, ku, work, b); bool converged = true; for (int64_t i = 0; i < size; ++i) { const double delta = std::fabs(b[i] - rates[i]); rates[i] = b[i]; if (!(delta < 1e-9 * (1.0 + std::fabs(b[i])))) converged = false; } if (converged) break; } for (int64_t k = 0; k < m; ++k) matvec(a[k], &rates[k * 3], &rotvecs_dot[k * 3]); // Knot rates: the fixed first rate, then the solved interior ones. Vec shifted(m * 3); for (int r = 0; r < 3; ++r) shifted[r] = first[r]; for (int64_t i = 0; i < 3 * (m - 1); ++i) shifted[3 + i] = rates[i]; rates = shifted; } Quat operator()(double t) const { const int64_t m = static_cast(times.size()) - 1; const int64_t k = ppoly_interval(times, t); const double s = t - times[k]; double rotvec[3]; for (int a = 0; a < 3; ++a) { const double coeff[4] = {coefficients[(k * 4 + 0) * 3 + a], coefficients[(k * 4 + 1) * 3 + a], coefficients[(k * 4 + 2) * 3 + a], coefficients[(k * 4 + 3) * 3 + a]}; rotvec[a] = ppoly_value(coeff, s); } int64_t index = static_cast(std::upper_bound(times.begin(), times.end(), t) - times.begin()) - 1; index = std::min(std::max(index, 0), m - 1); return sp_compose(rotations[index], sp_from_rotvec(rotvec)); } }; Quat slerp(const Quat& left, const Quat& right, double weight) { const Quat first = so3_normalize(left); Quat second = so3_normalize(right); double dot = first.x * second.x + first.y * second.y + first.z * second.z + first.w * second.w; if (dot < 0.0) second = {-second.x, -second.y, -second.z, -second.w}; dot = std::min(std::max(std::fabs(dot), 0.0), 1.0); Quat out; if (dot > 0.9995) { out = {first.x + weight * (second.x - first.x), first.y + weight * (second.y - first.y), first.z + weight * (second.z - first.z), first.w + weight * (second.w - first.w)}; } else { const double angle = std::acos(dot), denominator = std::sin(angle); const double a = std::sin((1.0 - weight) * angle) / denominator, b = std::sin(weight * angle) / denominator; out = {a * first.x + b * second.x, a * first.y + b * second.y, a * first.z + b * second.z, a * first.w + b * second.w}; } return so3_normalize(out); } // ---- runtime resample_uniform ---- enum Kind { kLinear = 0, kHold = 1, kPose = 2 }; enum Method { kLinearMethod = 0, kPchip = 1, kCubic = 2 }; int64_t resampled_horizon(int64_t length, double source_hz, double target_hz) { return std::max(1, static_cast(std::floor(length * target_hz / source_hz + 0.5))); } int64_t output_width(int kind, int64_t width) { return kind == kPose ? 7 : width; } // One row: float32 [T, D] in, float32 [count, D'] out. void resample_row(const float* input, int64_t steps, int64_t width, double source_hz, double target_hz, int kind, int64_t count, bool identity, int method, float* output) { Vec values(steps * width); for (int64_t i = 0; i < steps * width; ++i) values[i] = input[i]; const int64_t out_width = output_width(kind, width); if (identity) { for (int64_t i = 0; i < steps * width; ++i) output[i] = static_cast(values[i]); return; } Vec target_time(count), source_time(steps), interpolation_time(count); for (int64_t i = 0; i < count; ++i) target_time[i] = static_cast(i) / target_hz; for (int64_t i = 0; i < steps; ++i) source_time[i] = static_cast(i) / source_hz; std::vector left(count), right(count); Vec weight(count); for (int64_t i = 0; i < count; ++i) { interpolation_time[i] = std::min(target_time[i], source_time[steps - 1]); const double position = target_time[i] * source_hz; left[i] = std::min(static_cast(std::floor(position)), steps - 1); right[i] = std::min(left[i] + 1, steps - 1); weight[i] = position - static_cast(left[i]); } Vec result(count * out_width); if (kind == kHold) { for (int64_t i = 0; i < count; ++i) for (int64_t a = 0; a < width; ++a) result[i * out_width + a] = values[left[i] * width + a]; } else { const bool pose = kind == kPose; const int64_t cols = pose ? 3 : width; Vec continuous(steps * cols); for (int64_t t = 0; t < steps; ++t) for (int64_t a = 0; a < cols; ++a) continuous[t * cols + a] = values[t * width + a]; if (steps == 1) { for (int64_t i = 0; i < count; ++i) for (int64_t a = 0; a < cols; ++a) result[i * out_width + a] = continuous[a]; } else if (method == kPchip) { const Vec c = hermite_coefficients(source_time, continuous, pchip_slopes(source_time, continuous, cols), cols); ppoly_evaluate(source_time, c, cols, interpolation_time, out_width, 0, result.data()); } else if (method == kCubic) { const Vec c = hermite_coefficients(source_time, continuous, natural_cubic_slopes(source_time, continuous, cols), cols); ppoly_evaluate(source_time, c, cols, interpolation_time, out_width, 0, result.data()); // Guard: fall back to PCHIP on axes that materially leave their envelope. std::vector fallback; for (int64_t a = 0; a < cols; ++a) { double lower = continuous[a], upper = continuous[a]; for (int64_t t = 1; t < steps; ++t) { lower = std::min(lower, continuous[t * cols + a]); upper = std::max(upper, continuous[t * cols + a]); } const double tolerance = 1e-9 + 0.05 * (upper - lower); for (int64_t i = 0; i < count; ++i) { const double v = result[i * out_width + a]; if (v < lower - tolerance || v > upper + tolerance) { fallback.push_back(a); break; } } } if (!fallback.empty()) { const int64_t f = static_cast(fallback.size()); Vec subset(steps * f); for (int64_t t = 0; t < steps; ++t) for (int64_t j = 0; j < f; ++j) subset[t * f + j] = continuous[t * cols + fallback[j]]; const Vec c = hermite_coefficients(source_time, subset, pchip_slopes(source_time, subset, f), f); Vec sampled(count * f); ppoly_evaluate(source_time, c, f, interpolation_time, f, 0, sampled.data()); for (int64_t i = 0; i < count; ++i) for (int64_t j = 0; j < f; ++j) result[i * out_width + fallback[j]] = sampled[i * f + j]; } } else { for (int64_t i = 0; i < count; ++i) for (int64_t a = 0; a < cols; ++a) { const double lo = values[left[i] * width + a], hi = values[right[i] * width + a]; result[i * out_width + a] = lo + weight[i] * (hi - lo); } } if (pose) { std::vector quaternions(steps); for (int64_t t = 0; t < steps; ++t) quaternions[t] = {values[t * width + 3], values[t * width + 4], values[t * width + 5], values[t * width + 6]}; auto store = [&](int64_t i, const Quat& q) { result[i * out_width + 3] = q.x; result[i * out_width + 4] = q.y; result[i * out_width + 5] = q.z; result[i * out_width + 6] = q.w; }; if (steps == 1) { for (int64_t i = 0; i < count; ++i) store(i, quaternions[0]); } else if (method == kLinearMethod) { for (int64_t i = 0; i < count; ++i) store(i, slerp(quaternions[left[i]], quaternions[right[i]], weight[i])); } else { const RotationSpline spline(source_time, quaternions); for (int64_t i = 0; i < count; ++i) store(i, spline(interpolation_time[i])); } } } for (int64_t i = 0; i < count * out_width; ++i) output[i] = static_cast(result[i]); } int parse_kind(const std::string& kind) { if (kind == "linear") return kLinear; if (kind == "hold") return kHold; if (kind == "pose_xyzw") return kPose; throw std::invalid_argument("unsupported resampling kind"); } int parse_method(const std::string& method) { if (method == "linear") return kLinearMethod; if (method == "pchip") return kPchip; if (method == "cubic") return kCubic; throw std::invalid_argument("method must be one of ['cubic', 'linear', 'pchip']"); } using F32 = py::array_t; void check_finite(const F32& values) { const float* data = values.data(); for (py::ssize_t i = 0; i < values.size(); ++i) if (!std::isfinite(data[i])) throw std::invalid_argument("values must be a non-empty finite [T, D] array"); } F32 resample_uniform_batch(const F32& values, double source_hz, double target_hz, const std::string& kind_name, int64_t target_length, const std::string& method_name, int threads) { if (values.ndim() != 3 || values.shape(0) < 1 || values.shape(1) < 1) throw std::invalid_argument("values must have non-empty shape [B, T, D]"); if (!(std::isfinite(source_hz) && source_hz > 0.0)) throw std::invalid_argument("source_hz must be positive and finite"); if (!(std::isfinite(target_hz) && target_hz > 0.0)) throw std::invalid_argument("target_hz must be positive and finite"); check_finite(values); const int kind = parse_kind(kind_name), method = parse_method(method_name); const int64_t batch = values.shape(0), steps = values.shape(1), width = values.shape(2); if (kind == kPose && width != 7) throw std::invalid_argument("pose_xyzw values must have shape [T, 7]"); const int64_t standard = resampled_horizon(steps, source_hz, target_hz); const bool identity = source_hz == target_hz && (target_length < 0 || target_length == standard); const int64_t count = identity ? steps : (target_length < 0 ? standard : target_length); if (count < 1) throw std::invalid_argument("target_length must be a positive integer"); const int64_t out_width = output_width(kind, width); F32 output({batch, count, out_width}); const float* in = values.data(); float* out = output.mutable_data(); { py::gil_scoped_release release; ac2::parallel_for(batch, threads, [&](int64_t row) { resample_row(in + row * steps * width, steps, width, source_hz, target_hz, kind, count, identity, method, out + row * count * out_width); }); } return output; } // Elementwise pose maps over [B, T, D] float32 arrays. template F32 map_rows(const F32& values, int64_t in_width, int64_t out_width, int threads, Row&& row) { if (values.ndim() != 3 || values.shape(2) != in_width || values.shape(1) < 1) throw std::invalid_argument("expected shape [B, T, " + std::to_string(in_width) + "]"); check_finite(values); const int64_t batch = values.shape(0), steps = values.shape(1); F32 output({batch, steps, out_width}); const float* in = values.data(); float* out = output.mutable_data(); { py::gil_scoped_release release; ac2::parallel_for(batch, threads, [&](int64_t b) { row(in + b * steps * in_width, steps, out + b * steps * out_width); }); } return output; } // runtime integrate_eef_deltas: [T, 6] body increments -> [T, 7] absolute poses. F32 integrate_eef_deltas_batch(const F32& deltas, int threads) { return map_rows(deltas, 6, 7, threads, [](const float* in, int64_t steps, float* out) { double position[3] = {0.0, 0.0, 0.0}; Quat current{0.0, 0.0, 0.0, 1.0}; for (int64_t t = 0; t < steps; ++t) { for (int a = 0; a < 3; ++a) { position[a] = position[a] + static_cast(in[t * 6 + a]); out[t * 7 + a] = static_cast(position[a]); } const Quat increment = so3_from_rotvec(in[t * 6 + 3], in[t * 6 + 4], in[t * 6 + 5]); current = so3_multiply(current, increment); out[t * 7 + 3] = static_cast(current.x); out[t * 7 + 4] = static_cast(current.y); out[t * 7 + 5] = static_cast(current.z); out[t * 7 + 6] = static_cast(current.w); } }); } // runtime eef_pose_from_components(..., "rotation_vector"). F32 eef_pose_from_rotvec_batch(const F32& poses, int threads) { return map_rows(poses, 6, 7, threads, [](const float* in, int64_t steps, float* out) { for (int64_t t = 0; t < steps; ++t) { for (int a = 0; a < 3; ++a) out[t * 7 + a] = in[t * 6 + a]; const Quat q = so3_from_rotvec(in[t * 6 + 3], in[t * 6 + 4], in[t * 6 + 5]); out[t * 7 + 3] = static_cast(q.x); out[t * 7 + 4] = static_cast(q.y); out[t * 7 + 5] = static_cast(q.z); out[t * 7 + 6] = static_cast(q.w); } }); } // runtime eef_pose_absolute: [T, 7] xyz + xyzw -> [T, 6] xyz + rotation vector. F32 eef_pose_absolute_batch(const F32& poses, int threads) { return map_rows(poses, 7, 6, threads, [](const float* in, int64_t steps, float* out) { for (int64_t t = 0; t < steps; ++t) { for (int a = 0; a < 3; ++a) out[t * 6 + a] = in[t * 7 + a]; double rotvec[3]; so3_to_rotvec({in[t * 7 + 3], in[t * 7 + 4], in[t * 7 + 5], in[t * 7 + 6]}, rotvec); for (int a = 0; a < 3; ++a) out[t * 6 + 3 + a] = static_cast(rotvec[a]); } }); } // runtime eef_pose_deltas: local [T, 7] path -> [T, 6] body increments. F32 eef_pose_deltas_batch(const F32& poses, int threads) { return map_rows(poses, 7, 6, threads, [](const float* in, int64_t steps, float* out) { Quat previous{0.0, 0.0, 0.0, 1.0}; for (int64_t t = 0; t < steps; ++t) { for (int a = 0; a < 3; ++a) { const double before = t == 0 ? 0.0 : static_cast(in[(t - 1) * 7 + a]); out[t * 6 + a] = static_cast(static_cast(in[t * 7 + a]) - before); } const Quat q = so3_normalize({in[t * 7 + 3], in[t * 7 + 4], in[t * 7 + 5], in[t * 7 + 6]}); double rotvec[3]; so3_to_rotvec(so3_multiply({-previous.x, -previous.y, -previous.z, previous.w}, q), rotvec); for (int a = 0; a < 3; ++a) out[t * 6 + 3 + a] = static_cast(rotvec[a]); previous = q; } }); } // Re-express per-step rotation increments between world-frame (left) and // body-frame (right) multiplication, as scipy ``Rotation`` composes them. F32 rotvec_left_to_right_batch(const F32& increments, int threads) { return map_rows(increments, 3, 3, threads, [](const float* in, int64_t steps, float* out) { Quat current{0.0, 0.0, 0.0, 1.0}; for (int64_t t = 0; t < steps; ++t) { const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]}; const Quat following = sp_compose(sp_from_rotvec(v), current); double rotvec[3]; sp_as_rotvec(sp_compose(sp_inv(current), following), rotvec); for (int a = 0; a < 3; ++a) out[t * 3 + a] = static_cast(rotvec[a]); current = following; } }); } F32 rotvec_right_to_left_batch(const F32& increments, int threads) { return map_rows(increments, 3, 3, threads, [](const float* in, int64_t steps, float* out) { Quat current{0.0, 0.0, 0.0, 1.0}; for (int64_t t = 0; t < steps; ++t) { const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]}; const Quat following = sp_compose(current, sp_from_rotvec(v)); double rotvec[3]; sp_as_rotvec(sp_compose(following, sp_inv(current)), rotvec); for (int a = 0; a < 3; ++a) out[t * 3 + a] = static_cast(rotvec[a]); current = following; } }); } } // namespace void register_resample(py::module_& module) { module.def("resample_uniform_batch", &resample_uniform_batch, py::arg("values"), py::arg("source_hz"), py::arg("target_hz"), py::arg("kind"), py::arg("target_length") = -1, py::arg("method") = "linear", py::arg("threads") = 0, "runtime resample_uniform over each [T, D] row of a [B, T, D] batch."); module.def("integrate_eef_deltas_batch", &integrate_eef_deltas_batch, py::arg("deltas"), py::arg("threads") = 0); module.def("eef_pose_from_rotvec_batch", &eef_pose_from_rotvec_batch, py::arg("poses"), py::arg("threads") = 0); module.def("eef_pose_absolute_batch", &eef_pose_absolute_batch, py::arg("poses"), py::arg("threads") = 0); module.def("eef_pose_deltas_batch", &eef_pose_deltas_batch, py::arg("poses"), py::arg("threads") = 0); module.def("rotvec_left_to_right_batch", &rotvec_left_to_right_batch, py::arg("increments"), py::arg("threads") = 0); module.def("rotvec_right_to_left_batch", &rotvec_right_to_left_batch, py::arg("increments"), py::arg("threads") = 0); }