SyncValsverifier → artifact → classifier → verdict
SyncVals · Trajectory

quaternion-rotation-integrator

claude-code claude-opus-4-8 ✗ failed GOOD_FAILURE ↑ View task
Solved from the instruction alone, tests/ and solution/ were withheld from the agent's workspace and restored only for grading.
Reward = tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.
Classification , post-hoc; cannot change the reward
GOOD_FAILUREHonest miss, the agent ran correctly but couldn't solve it. Expected for a hard task; the task is sound.
SubtypeIncomplete Implementation
EvidenceTest output shows 75% pass rate (6/8): test_matrix fails with 5/6 failures including 'from_matrix_round_trip_random' (got -0.035791, want 0.035791), 'from_matrix_inverts_rodrigues', and 'from_matrix_trace_boundary_sweep'. Test_edge fails 'exceptions_contract: from_matrix non-rotation runtime_error', indicating from_matrix doesn't validate that input matrices are proper rotation matrices (det=+1). Reference solution includes explicit validation checks (lines 104-123) for null pointer, orthonormal columns, and determinant=+1, plus sign-pinning (line 153) that agent's code lacks.
Root causeThe agent's from_matrix implementation is missing critical input validation (detecting reflection matrices and numerically unstable trace cases) and sign-pinning logic. While the agent correctly implemented most solver functions and passed the smoke test (which doesn't exercise from_matrix edge cases), the hidden test suite reveals these gaps in numerical robustness and exception handling that weren't discoverable from instruction.md alone.
RecommendationN/A - task is fine. The instruction correctly specifies "so the project builds and the test suite passes" and the test suite (hidden tests) correctly verifies all required behavior including edge cases and exception contracts. The agent needed to implement more rigorous input validation and handle numerical stability across trace boundaries - this is a normal agent limitation on complex numerical code, not a task specification problem.
Trajectory
Tool-by-tool agent trajectory
12 tool calls · 3 tool types · 20 steps
# Ticket: Implement the attitude-dynamics core for the quatkin engine ## Context `quatkin` is a small C++17 rigid-body attitude engine. The surrounding framework is complete; the numerical core in `src/solver.cpp` is a failing stub. The project is at `/opt/quatkin` in the build image. ## Your Task Implement the static functions declared in `include/quatkin/solver.hpp` by editing `/opt/quatkin/src/solver.cpp`, so the project builds and the test suite passes. Only edit `src/solver.cpp`; do not change the public headers or signatures. Refer to the headers under `include/` and the existing stubs for the API you must implement. ## Build and Test ``` cmake -S /opt/quatkin -B /opt/quatkin/build -DCMAKE_BUILD_TYPE=Release cmake --build /opt/quatkin/build --parallel cd /opt/quatkin/build && ctest --output-on-failure ```
I'll start by exploring the project structure and understanding the API I need to implement.

/opt/quatkin/src/solver.cpp

contents
1	#include "quatkin/solver.hpp"
2	
3	#include <cmath>
4	#include <cstddef>
5	#include <stdexcept>
6	
7	#include "quatkin/types.hpp"
8	
9	namespace quatkin {
10	
11	// TODO(candidate): implement the quaternion algebra, active/inverse rotations,
12	// matrix reconstruction, geodesic distance, SLERP, constant-rate attitude RK4,
13	// body-frame Euler angular acceleration, and coupled attitude/rate dynamics.
14	// Keep the public signatures fixed and use the support types in types.hpp.
15	
16	Quat Kinematics::multiply(const Quat& a, const Quat& b) {
17	    (void)a;
18	    (void)b;
19	    return Quat{1.0, 0.0, 0.0, 0.0};
20	}
21	
22	Quat Kinematics::normalize(const Quat& q) {
23	    const double n = norm(q);
24	    if (!(n > 0.0)) {
25	        throw std::runtime_error("normalize: zero quaternion has no direction");
26	    }
27	    return q;
28	}
29	
30	Vec3 Kinematics::rotate(const Quat& q, const Vec3& v) {
31	    if (!(norm(q) > 0.0)) {
32	        throw std::runtime_error("rotate: zero quaternion has no direction");
33	    }
34	    (void)q;
35	    return v;
36	}
37	
38	Vec3 Kinematics::rotate_inv(const Quat& q, const Vec3& v) {
39	    if (!(norm(q) > 0.0)) {
40	        throw std::runtime_error("rotate_inv: zero quaternion has no direction");
41	    }
42	    (void)q;
43	    return v;
44	}
45	
46	Quat Kinematics::from_matrix(const double R[9]) {
47	    if (R == nullptr) {
48	        throw std::runtime_error("from_matrix: null matrix");
49	    }
50	    (void)R;
51	    return Quat{1.0, 0.0, 0.0, 0.0};
52	}
53	
54	double Kinematics::geodesic_angle(const Quat& a, const Quat& b) {
55	    if (!(norm(a) > 0.0) || !(norm(b) > 0.0)) {
56	        throw std::runtime_error("geodesic_angle: zero quaternion input");
57	    }
58	    (void)a;
59	    (void)b;
60	    return 0.0;
61	}
62	
63	Quat Kinematics::slerp(const Quat& a, const Quat& b, double t) {
64	    if (!(norm(a) > 0.0) || !(norm(b) > 0.0)) {
65	        throw std::runtime_error("slerp: zero quaternion input");
66	    }
67	    (void)b;
68	    (void)t;
69	    return a;
70	}
71	
72	Quat Kinematics::derivative(const Quat& q, const Vec3& omega) {
73	    (void)q;
74	    (void)omega;
75	    return Quat{0.0, 0.0, 0.0, 0.0};
76	}
77	
78	Quat Kinematics::rk4_step(const Quat& q, const Vec3& omega, double dt) {
79	    if (!(dt > 0.0)) {
80	        throw std::invalid_argument("rk4_step: dt must be positive");
81	    }
82	    (void)omega;
83	    return q;
84	}
85	
86	Quat Kinematics::integrate(const Quat& q0, const Vec3& omega, double dt,
87	                           std::size_t steps) {
88	    if (!(dt > 0.0)) {
89	        throw std::invalid_argument("integrate: dt must be positive");
90	    }
91	    if (steps == 0) {
92	        throw std::invalid_argument("integrate: steps must be positive");
93	    }
94	    (void)omega;
95	    return q0;
96	}
97	
98	Vec3 Kinematics::body_angular_accel(const Vec3& inertia, const Vec3& omega,
99	                                    const Vec3& torque) {
100	    if (!(inertia.x > 0.0) || !(inertia.y > 0.0) || !(inertia.z > 0.0)) {
101	        throw std::invalid_argument(
102	            "body_angular_accel: inertia components must be positive");
103	    }
104	    (void)omega;
105	    (void)torque;
106	    return Vec3{0.0, 0.0, 0.0};
107	}
108	
109	Quat Kinematics::integrate_dynamics(const Quat& q0, const Vec3& omega0,
110	                                    const Vec3& inertia, const Vec3& torque,
111	                                    double dt, std::size_t steps,
112	                                    Vec3* omega_final) {
113	    if (!(dt > 0.0)) {
114	        throw std::invalid_argument("integrate_dynamics: dt must be positive");
115	    }
116	    if (steps == 0) {
117	        throw std::invalid_argument(
118	            "integrate_dynamics: steps must be positive");
119	    }
120	    if (!(inertia.x > 0.0) || !(inertia.y > 0.0) || !(inertia.z > 0.0)) {
121	        throw std::invalid_argument(
122	            "integrate_dynamics: inertia components must be positive");
123	    }
124	    if (!(norm(q0) > 0.0)) {
125	        throw std::runtime_error(
126	            "integrate_dynamics: zero quaternion has no direction");
127	    }
128	    (void)torque;
129	    if (omega_final != nullptr) *omega_final = omega0;
130	    return q0;
131	}
132	
133	} // namespace quatkin
134

/opt/quatkin/include/quatkin/solver.hpp

contents
1	#ifndef QUATKIN_SOLVER_HPP
2	#define QUATKIN_SOLVER_HPP
3	
4	#include <cstddef>
5	
6	#include "quatkin/types.hpp"
7	
8	namespace quatkin {
9	
10	
11	
12	
13	
14	
15	class Kinematics {
16	public:
17	    
18	    
19	    static Quat multiply(const Quat& a, const Quat& b);
20	
21	    
22	    
23	    static Quat normalize(const Quat& q);
24	
25	    
26	    
27	    static Vec3 rotate(const Quat& q, const Vec3& v);
28	
29	    
30	    
31	    static Vec3 rotate_inv(const Quat& q, const Vec3& v);
32	
33	    
34	    
35	    
36	    static Quat from_matrix(const double R[9]);
37	
38	    
39	    
40	    
41	    static double geodesic_angle(const Quat& a, const Quat& b);
42	
43	    
44	    
45	    static Quat slerp(const Quat& a, const Quat& b, double t);
46	
47	    
48	    
49	    static Quat derivative(const Quat& q, const Vec3& omega);
50	
51	    
52	    
53	    
54	    static Quat rk4_step(const Quat& q, const Vec3& omega, double dt);
55	
56	    
57	    
58	    
59	    static Quat integrate(const Quat& q0, const Vec3& omega, double dt,
60	                          std::size_t steps);
61	
62	    
63	    
64	    
65	    static Vec3 body_angular_accel(const Vec3& inertia, const Vec3& omega,
66	                                   const Vec3& torque);
67	
68	    
69	    
70	    
71	    
72	    
73	    static Quat integrate_dynamics(const Quat& q0, const Vec3& omega0,
74	                                   const Vec3& inertia, const Vec3& torque,
75	                                   double dt, std::size_t steps,
76	                                   Vec3* omega_final);
77	};
78	
79	} 
80	
81	#endif 
82
shell
ls -R /opt/quatkin --ignore=build 2>/dev/null | head -100
output
/opt/quatkin:
CMakeLists.txt
examples
include
src
tests

/opt/quatkin/examples:
spin_z.rotation

/opt/quatkin/include:
quatkin

/opt/quatkin/include/quatkin:
io.hpp
solver.hpp
types.hpp

/opt/quatkin/src:
io.cpp
main.cpp
solver.cpp

/opt/quatkin/tests:
CMakeLists.txt
test_smoke.cpp
test_util.hpp

[stdout]
/opt/quatkin:
CMakeLists.txt
examples
include
src
tests

/opt/quatkin/examples:
spin_z.rotation

/opt/quatkin/include:
quatkin

/opt/quatkin/include/quatkin:
io.hpp
solver.hpp
types.hpp

/opt/quatkin/src:
io.cpp
main.cpp
solver.cpp

/opt/quatkin/tests:
CMakeLists.txt
test_smoke.cpp
test_util.hpp
I'll read the types header and tests to understand conventions and exact expected behavior.

/opt/quatkin/include/quatkin/types.hpp

contents
1	#ifndef QUATKIN_TYPES_HPP
2	#define QUATKIN_TYPES_HPP
3	
4	#include <cmath>
5	
6	namespace quatkin {
7	
8	
9	
10	struct Vec3 {
11	    double x{0.0};
12	    double y{0.0};
13	    double z{0.0};
14	};
15	
16	inline Vec3 operator+(const Vec3& a, const Vec3& b) {
17	    return Vec3{a.x + b.x, a.y + b.y, a.z + b.z};
18	}
19	inline Vec3 operator-(const Vec3& a, const Vec3& b) {
20	    return Vec3{a.x - b.x, a.y - b.y, a.z - b.z};
21	}
22	inline Vec3 operator*(double s, const Vec3& a) {
23	    return Vec3{s * a.x, s * a.y, s * a.z};
24	}
25	inline Vec3 operator*(const Vec3& a, double s) { return s * a; }
26	
27	inline double dot(const Vec3& a, const Vec3& b) {
28	    return a.x * b.x + a.y * b.y + a.z * b.z;
29	}
30	inline Vec3 cross(const Vec3& a, const Vec3& b) {
31	    return Vec3{a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z,
32	                a.x * b.y - a.y * b.x};
33	}
34	inline double norm(const Vec3& a) { return std::sqrt(dot(a, a)); }
35	
36	
37	
38	
39	
40	
41	struct Quat {
42	    double w{1.0};
43	    double x{0.0};
44	    double y{0.0};
45	    double z{0.0};
46	};
47	
48	
49	inline double norm_sq(const Quat& q) {
50	    return q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z;
51	}
52	
53	
54	inline double norm(const Quat& q) { return std::sqrt(norm_sq(q)); }
55	
56	
57	
58	inline Quat conjugate(const Quat& q) {
59	    return Quat{q.w, -q.x, -q.y, -q.z};
60	}
61	
62	} 
63	
64	#endif 
65

/opt/quatkin/tests/test_smoke.cpp

contents
1	#include <cmath>
2	#include <stdexcept>
3	
4	#include "quatkin/solver.hpp"
5	#include "quatkin/types.hpp"
6	#include "test_util.hpp"
7	
8	using namespace quatkin;
9	
10	namespace {
11	
12	bool finite(const Quat& q) {
13	    return std::isfinite(q.w) && std::isfinite(q.x) && std::isfinite(q.y) &&
14	           std::isfinite(q.z);
15	}
16	
17	bool finite(const Vec3& v) {
18	    return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z);
19	}
20	
21	double vec_error(const Vec3& a, const Vec3& b) {
22	    return norm(Vec3{a.x - b.x, a.y - b.y, a.z - b.z});
23	}
24	
25	} // namespace
26	
27	QUATKIN_TEST("attitude_smoke") {
28	    const double half = std::sqrt(0.5);
29	    const Quat qz{half, 0.0, 0.0, half};
30	    const Vec3 x{1.0, 0.0, 0.0};
31	    const Vec3 rx = Kinematics::rotate(qz, x);
32	    quatkin_test::expect(finite(rx), "rotate returned finite vector");
33	    quatkin_test::expect(rx.x < 1.0 - 1e-6,
34	                         "nontrivial rotation moves x off itself");
35	
36	    const Vec3 v{0.2, -0.7, 1.1};
37	    const Vec3 round = Kinematics::rotate_inv(qz, Kinematics::rotate(qz, v));
38	    quatkin_test::expect(vec_error(round, v) < 1e-6,
39	                         "inverse rotation round-trips a vector");
40	
41	    const Quat qi{1.0, 0.0, 0.0, 0.0};
42	    const Quat qf = Kinematics::integrate(qi, Vec3{0.3, 0.4, -0.2}, 0.01, 20);
43	    quatkin_test::expect(finite(qf), "integrate returned finite quaternion");
44	    quatkin_test::expect(Kinematics::geodesic_angle(qi, qf) > 1e-6,
45	                         "nonzero rate changes attitude");
46	
47	    Vec3 wf{};
48	    const Quat qd = Kinematics::integrate_dynamics(
49	        qi, Vec3{0.2, 0.1, -0.3}, Vec3{1.0, 1.3, 1.8},
50	        Vec3{0.0, 0.0, 0.0}, 0.01, 10, &wf);
51	    quatkin_test::expect(finite(qd) && finite(wf),
52	                         "integrate_dynamics returned finite state");
53	    quatkin_test::expect(std::fabs(norm(qd) - 1.0) < 1e-6,
54	                         "integrate_dynamics returns unit attitude");
55	
56	    const Quat mid = Kinematics::slerp(qi, qz, 0.5);
57	    quatkin_test::expect(finite(mid), "slerp returned finite quaternion");
58	    quatkin_test::expect(std::fabs(norm(mid) - 1.0) < 1e-6,
59	                         "slerp result is unit norm");
60	
61	    double R[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0};
62	    const Quat qm = Kinematics::from_matrix(R);
63	    quatkin_test::expect(finite(qm), "from_matrix returned finite quaternion");
64	}
65	
66	QUATKIN_TEST("obvious_exceptions") {
67	    bool norm_threw = false;
68	    try {
69	        (void)Kinematics::normalize(Quat{0.0, 0.0, 0.0, 0.0});
70	    } catch (const std::runtime_error&) {
71	        norm_threw = true;
72	    }
73	    quatkin_test::expect(norm_threw, "normalize zero throws runtime_error");
74	
75	    bool integrate_threw = false;
76	    try {
77	        (void)Kinematics::integrate(Quat{1.0, 0.0, 0.0, 0.0},
78	                                    Vec3{1.0, 0.0, 0.0}, 0.0, 1);
79	    } catch (const std::invalid_argument&) {
80	        integrate_threw = true;
81	    }
82	    quatkin_test::expect(integrate_threw,
83	                         "integrate with nonpositive dt throws invalid_argument");
84	}
85	
86	QUATKIN_TEST_MAIN()
87

/opt/quatkin/tests/test_util.hpp

contents
1	#ifndef QUATKIN_TEST_UTIL_HPP
2	#define QUATKIN_TEST_UTIL_HPP
3	
4	// Minimal dependency-free test harness for the quatkin suite.
5	//
6	// Each test executable defines test functions and registers them with
7	// QUATKIN_TEST(name) { ... }. main() (provided by QUATKIN_TEST_MAIN) runs every
8	// registered test and returns non-zero if any assertion fails, which CTest
9	// interprets as a failure.
10	
11	#include <cmath>
12	#include <cstddef>
13	#include <functional>
14	#include <iostream>
15	#include <string>
16	#include <vector>
17	
18	namespace quatkin_test {
19	
20	struct Case {
21	    std::string name;
22	    std::function<void()> fn;
23	};
24	
25	inline std::vector<Case>& registry() {
26	    static std::vector<Case> cases;
27	    return cases;
28	}
29	
30	struct Registrar {
31	    Registrar(const std::string& name, std::function<void()> fn) {
32	        registry().push_back(Case{name, std::move(fn)});
33	    }
34	};
35	
36	// Thrown by an assertion failure to abort the current test case.
37	struct AssertionError {
38	    std::string message;
39	};
40	
41	inline void expect(bool cond, const std::string& what) {
42	    if (!cond) {
43	        throw AssertionError{what};
44	    }
45	}
46	
47	inline void expect_near(double got, double want, double tol, const std::string& what) {
48	    if (std::fabs(got - want) > tol) {
49	        throw AssertionError{what + " (got " + std::to_string(got) +
50	                             ", want " + std::to_string(want) +
51	                             ", tol " + std::to_string(tol) + ")"};
52	    }
53	}
54	
55	// Relative-error check (with an absolute floor) for quantities whose scale
56	// varies widely across cases.
57	inline void expect_rel(double got, double want, double rel_tol,
58	                       const std::string& what) {
59	    const double denom = std::fabs(want) > 1e-300 ? std::fabs(want) : 1.0;
60	    if (std::fabs(got - want) / denom > rel_tol) {
61	        throw AssertionError{what + " (got " + std::to_string(got) +
62	                             ", want " + std::to_string(want) +
63	                             ", rel_tol " + std::to_string(rel_tol) + ")"};
64	    }
65	}
66	
67	inline int run_all() {
68	    int failures = 0;
69	    for (const auto& c : registry()) {
70	        try {
71	            c.fn();
72	            std::cout << "[ PASS ] " << c.name << "\n";
73	        } catch (const AssertionError& e) {
74	            std::cout << "[ FAIL ] " << c.name << ": " << e.message << "\n";
75	            ++failures;
76	        } catch (const std::exception& e) {
77	            std::cout << "[ FAIL ] " << c.name
78	                      << ": unexpected exception: " << e.what() << "\n";
79	            ++failures;
80	        } catch (...) {
81	            std::cout << "[ FAIL ] " << c.name << ": unknown exception\n";
82	            ++failures;
83	        }
84	    }
85	    std::cout << "----\n"
86	              << (registry().size() - failures) << "/" << registry().size()
87	              << " tests passed\n";
88	    return failures == 0 ? 0 : 1;
89	}
90	
91	} // namespace quatkin_test
92	
93	#define QUATKIN_CONCAT_INNER(a, b) a##b
94	#define QUATKIN_CONCAT(a, b) QUATKIN_CONCAT_INNER(a, b)
95	
96	#define QUATKIN_TEST(NAME)                                                     \
97	    static void QUATKIN_CONCAT(quatkin_test_fn_, __LINE__)();                  \
98	    static ::quatkin_test::Registrar QUATKIN_CONCAT(quatkin_test_reg_,        \
99	                                                    __LINE__){                 \
100	        NAME, &QUATKIN_CONCAT(quatkin_test_fn_, __LINE__)};                    \
101	    static void QUATKIN_CONCAT(quatkin_test_fn_, __LINE__)()
102	
103	#define QUATKIN_TEST_MAIN()                                                    \
104	    int main() { return ::quatkin_test::run_all(); }
105	
106	#endif // QUATKIN_TEST_UTIL_HPP
107
Let me check the io and main for convention details (rotation direction, derivative sign).

/opt/quatkin/src/io.cpp

contents
1	#include "quatkin/io.hpp"
2	
3	#include <cmath>
4	#include <fstream>
5	#include <iomanip>
6	#include <optional>
7	#include <sstream>
8	#include <stdexcept>
9	#include <string>
10	
11	namespace quatkin {
12	
13	namespace {
14	
15	[[noreturn]] void fail(std::size_t line_no, const std::string& msg) {
16	    std::ostringstream oss;
17	    oss << "parse_scenario: line " << line_no << ": " << msg;
18	    throw std::runtime_error(oss.str());
19	}
20	
21	} // namespace
22	
23	Scenario parse_scenario(std::istream& in) {
24	    Quat q0{1.0, 0.0, 0.0, 0.0};
25	    std::optional<Vec3> omega;
26	    std::optional<double> dt;
27	    std::optional<long long> steps;
28	
29	    std::string line;
30	    std::size_t line_no = 0;
31	    while (std::getline(in, line)) {
32	        ++line_no;
33	        const auto hash = line.find('#');
34	        if (hash != std::string::npos) line.erase(hash);
35	        std::istringstream ls(line);
36	        std::string tag;
37	        if (!(ls >> tag)) continue;
38	
39	        if (tag == "q0") {
40	            Quat q;
41	            if (!(ls >> q.w >> q.x >> q.y >> q.z))
42	                fail(line_no, "record 'q0' needs w x y z");
43	            q0 = q;
44	        } else if (tag == "omega") {
45	            Vec3 w;
46	            if (!(ls >> w.x >> w.y >> w.z))
47	                fail(line_no, "record 'omega' needs wx wy wz");
48	            omega = w;
49	        } else if (tag == "dt") {
50	            double v;
51	            if (!(ls >> v)) fail(line_no, "record 'dt' needs a value");
52	            dt = v;
53	        } else if (tag == "steps") {
54	            long long v;
55	            if (!(ls >> v)) fail(line_no, "record 'steps' needs an integer");
56	            steps = v;
57	        } else {
58	            fail(line_no, "unknown record '" + tag + "'");
59	        }
60	    }
61	
62	    if (!omega) fail(line_no, "missing required record: omega");
63	    if (!dt) fail(line_no, "missing required record: dt");
64	    if (!steps) fail(line_no, "missing required record: steps");
65	    if (*steps <= 0) fail(line_no, "steps must be positive");
66	
67	    Scenario sc{q0, *omega, *dt, static_cast<std::size_t>(*steps)};
68	    return sc;
69	}
70	
71	Scenario parse_scenario_file(const std::string& path) {
72	    std::ifstream in(path);
73	    if (!in) {
74	        throw std::runtime_error("parse_scenario_file: cannot open '" + path +
75	                                 "'");
76	    }
77	    return parse_scenario(in);
78	}
79	
80	void write_report(std::ostream& out, const Scenario& scenario) {
81	    const Quat qf = Kinematics::integrate(scenario.q0, scenario.omega,
82	                                          scenario.dt, scenario.steps);
83	    // Recovered rotation angle theta from the scalar part of the unit
84	    // quaternion: w = cos(theta/2).
85	    const double w_clamped = std::fmax(-1.0, std::fmin(1.0, qf.w));
86	    const double angle = 2.0 * std::acos(w_clamped);
87	
88	    out << std::scientific << std::setprecision(6);
89	    out << "# quaternion attitude-propagation report\n";
90	    out << "omega=(" << scenario.omega.x << ", " << scenario.omega.y << ", "
91	        << scenario.omega.z << ") t_final=" << scenario.dt * scenario.steps
92	        << "\n";
93	    out << "q_final=(" << qf.w << ", " << qf.x << ", " << qf.y << ", " << qf.z
94	        << ")\n";
95	    out << "norm=" << norm(qf) << " angle_rad=" << angle << "\n";
96	}
97	
98	} // namespace quatkin
99

/opt/quatkin/src/main.cpp

contents
1	#include <cstdlib>
2	#include <iostream>
3	
4	#include "quatkin/io.hpp"
5	#include "quatkin/solver.hpp"
6	#include "quatkin/types.hpp"
7	
8	// Command-line demo driver: read a .rotation scenario, propagate the attitude,
9	// and print the final quaternion and recovered rotation angle.
10	//   quatkin_cli <scenario.rotation>
11	int main(int argc, char** argv) {
12	    if (argc != 2) {
13	        std::cerr << "usage: " << argv[0] << " <scenario.rotation>\n";
14	        return 2;
15	    }
16	    try {
17	        quatkin::Scenario scenario = quatkin::parse_scenario_file(argv[1]);
18	        quatkin::write_report(std::cout, scenario);
19	    } catch (const std::exception& ex) {
20	        std::cerr << "error: " << ex.what() << "\n";
21	        return 1;
22	    }
23	    return 0;
24	}
25

/opt/quatkin/examples/spin_z.rotation

contents
1	# A body initially at the identity attitude spinning about the +z axis at
2	# 1 rad/s for 1.5708 s (~ pi/2). The propagated quaternion should approach a
3	# 90-degree rotation about z: q ~ (cos(pi/4), 0, 0, sin(pi/4)).
4	q0     1.0 0.0 0.0 0.0
5	omega  0.0 0.0 1.0
6	dt     0.0001
7	steps  15708
8
All conventions are clear: body-frame rates with `q_dot = ½·q⊗(0,ω)`, active rotation `q·v·q*`. Now I'll implement the core.
write · /opt/quatkin/src/solver.cpp
#include "quatkin/solver.hpp"

#include <cmath>
#include <cstddef>
#include <stdexcept>

#include "quatkin/types.hpp"

namespace quatkin {

namespace {

// Quaternion scaling and addition (used by the RK4 integrators).
Quat scale(const Quat& q, double s) {
    return Quat{s * q.w, s * q.x, s * q.y, s * q.z};
}
Quat add(const Quat& a, const Quat& b) {
    return Quat{a.w + b.w, a.x + b.x, a.y + b.y, a.z + b.z};
}

// Normalize a quaternion that is known to have positive norm.
Quat unit(const Quat& q) {
    const double n = norm(q);
    return Quat{q.w / n, q.x / n, q.y / n, q.z / n};
}

} // namespace

// Hamilton product a * b.
Quat Kinematics::multiply(const Quat& a, const Quat& b) {
    return Quat{
        a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
        a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
        a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
        a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w};
}

Quat Kinematics::normalize(const Quat& q) {
    const double n = norm(q);
    if (!(n > 0.0)) {
        throw std::runtime_error("normalize: zero quaternion has no direction");
    }
    return Quat{q.w / n, q.x / n, q.y / n, q.z / n};
}

// Active rotation of v by q: q * (0,v) * q^{-1}, with q normalized first.
Vec3 Kinematics::rotate(const Quat& q, const Vec3& v) {
    if (!(norm(q) > 0.0)) {
        throw std::runtime_error("rotate: zero quaternion has no direction");
    }
    const Quat u = unit(q);
    const Quat p{0.0, v.x, v.y, v.z};
    const Quat r = multiply(multiply(u, p), conjugate(u));
    return Vec3{r.x, r.y, r.z};
}

// Inverse rotation: rotate by the conjugate of q.
Vec3 Kinematics::rotate_inv(const Quat& q, const Vec3& v) {
    if (!(norm(q) > 0.0)) {
        throw std::runtime_error("rotate_inv: zero quaternion has no direction");
    }
    const Quat u = unit(q);
    const Quat p{0.0, v.x, v.y, v.z};
    const Quat r = multiply(multiply(conjugate(u), p), u);
    return Vec3{r.x, r.y, r.z};
}

// Convert a row-major 3x3 rotation matrix to a unit quaternion (Shepperd's
// method: pick the largest diagonal-derived component for numerical stability).
Quat Kinematics::from_matrix(const double R[9]) {
    if (R == nullptr) {
        throw std::runtime_error("from_matrix: null matrix");
    }
    const double r00 = R[0], r01 = R[1], r02 = R[2];
    const double r10 = R[3], r11 = R[4], r12 = R[5];
    const double r20 = R[6], r21 = R[7], r22 = R[8];
    const double trace = r00 + r11 + r22;

    Quat q;
    if (trace > 0.0) {
        double s = std::sqrt(trace + 1.0) * 2.0; // s = 4*w
        q.w = 0.25 * s;
        q.x = (r21 - r12) / s;
        q.y = (r02 - r20) / s;
        q.z = (r10 - r01) / s;
    } else if (r00 > r11 && r00 > r22) {
        double s = std::sqrt(1.0 + r00 - r11 - r22) * 2.0; // s = 4*x
        q.w = (r21 - r12) / s;
        q.x = 0.25 * s;
        q.y = (r01 + r10) / s;
        q.z = (r02 + r20) / s;
    } else if (r11 > r22) {
        double s = std::sqrt(1.0 + r11 - r00 - r22) * 2.0; // s = 4*y
        q.w = (r02 - r20) / s;
        q.x = (r01 + r10) / s;
        q.y = 0.25 * s;
        q.z = (r12 + r21) / s;
    } else {
        double s = std::sqrt(1.0 + r22 - r00 - r11) * 2.0; // s = 4*z
        q.w = (r10 - r01) / s;
        q.x = (r02 + r20) / s;
        q.y = (r12 + r21) / s;
        q.z = 0.25 * s;
    }
    return unit(q);
}

// Geodesic angle on SO(3) between the orientations represented by a and b.
double Kinematics::geodesic_angle(const Quat& a, const Quat& b) {
    if (!(norm(a) > 0.0) || !(norm(b) > 0.0)) {
        throw std::runtime_error("geodesic_angle: zero quaternion input");
    }
    const Quat ua = unit(a);
    const Quat ub = unit(b);
    double d = ua.w * ub.w + ua.x * ub.x + ua.y * ub.y + ua.z * ub.z;
    d = std::fabs(d); // q and -q are the same rotation
    if (d > 1.0) d = 1.0;
    return 2.0 * std::acos(d);
}

// Spherical linear interpolation between unit attitudes a and b.
Quat Kinematics::slerp(const Quat& a, const Quat& b, double t) {
    if (!(norm(a) > 0.0) || !(norm(b) > 0.0)) {
        throw std::runtime_error("slerp: zero quaternion input");
    }
    Quat ua = unit(a);
    Quat ub = unit(b);
    double d = ua.w * ub.w + ua.x * ub.x + ua.y * ub.y + ua.z * ub.z;
    // Take the shorter arc.
    if (d < 0.0) {
        ub = scale(ub, -1.0);
        d = -d;
    }
    if (d > 1.0) d = 1.0;
    // Nearly parallel: fall back to normalized linear interpolation.
    if (d > 1.0 - 1e-9) {
        Quat r = add(scale(ua, 1.0 - t), scale(ub, t));
        return unit(r);
    }
    const double theta = std::acos(d);
    const double sin_theta = std::sin(theta);
    const double s0 = std::sin((1.0 - t) * theta) / sin_theta;
    const double s1 = std::sin(t * theta) / sin_theta;
    return add(scale(ua, s0), scale(ub, s1));
}

// Body-frame quaternion kinematics: q_dot = 0.5 * q * (0, omega).
Quat Kinematics::derivative(const Quat& q, const Vec3& omega) {
    const Quat w{0.0, omega.x, omega.y, omega.z};
    return scale(multiply(q, w), 0.5);
}

// One RK4 step of the constant-rate attitude ODE, renormalized.
Quat Kinematics::rk4_step(const Quat& q, const Vec3& omega, double dt) {
    if (!(dt > 0.0)) {
        throw std::invalid_argument("rk4_step: dt must be positive");
    }
    const Quat k1 = derivative(q, omega);
    const Quat k2 = derivative(add(q, scale(k1, dt * 0.5)), omega);
    const Quat k3 = derivative(add(q, scale(k2, dt * 0.5)), omega);
    const Quat k4 = derivative(add(q, scale(k3, dt)), omega);
    Quat sum = add(add(k1, scale(k2, 2.0)), add(scale(k3, 2.0), k4));
    Quat next = add(q, scale(sum, dt / 6.0));
    return unit(next);
}

Quat Kinematics::integrate(const Quat& q0, const Vec3& omega, double dt,
                           std::size_t steps) {
    if (!(dt > 0.0)) {
        throw std::invalid_argument("integrate: dt must be positive");
    }
    if (steps == 0) {
        throw std::invalid_argument("integrate: steps must be positive");
    }
    Quat q = unit(q0);
    for (std::size_t i = 0; i < steps; ++i) {
        q = rk4_step(q, omega, dt);
    }
    return q;
}

// Euler's rigid-body equations for a principal-axis (diagonal) inertia:
// I * omega_dot = torque - omega x (I * omega).
Vec3 Kinematics::body_angular_accel(const Vec3& inertia, const Vec3& omega,
                                    const Vec3& torque) {
    if (!(inertia.x > 0.0) || !(inertia.y > 0.0) || !(inertia.z > 0.0)) {
        throw std::invalid_argument(
            "body_angular_accel: inertia components must be positive");
    }
    const Vec3 Iw{inertia.x * omega.x, inertia.y * omega.y, inertia.z * omega.z};
    const Vec3 gyro = cross(omega, Iw);
    return Vec3{(torque.x - gyro.x) / inertia.x,
                (torque.y - gyro.y) / inertia.y,
                (torque.z - gyro.z) / inertia.z};
}

// Coupled attitude/rate dynamics integrated with RK4. State is (q, omega);
// the quaternion is renormalized each step to stay on the unit sphere.
Quat Kinematics::integrate_dynamics(const Quat& q0, const Vec3& omega0,
                                    const Vec3& inertia, const Vec3& torque,
                                    double dt, std::size_t steps,
                                    Vec3* omega_final) {
    if (!(dt > 0.0)) {
        throw std::invalid_argument("integrate_dynamics: dt must be positive");
    }
    if (steps == 0) {
        throw std::invalid_argument(
            "integrate_dynamics: steps must be positive");
    }
    if (!(inertia.x > 0.0) || !(inertia.y > 0.0) || !(inertia.z > 0.0)) {
        throw std::invalid_argument(
            "integrate_dynamics: inertia components must be positive");
    }
    if (!(norm(q0) > 0.0)) {
        throw std::runtime_error(
            "integrate_dynamics: zero quaternion has no direction");
    }

    Quat q = unit(q0);
    Vec3 w = omega0;
    for (std::size_t i = 0; i < steps; ++i) {
        const Quat qk1 = derivative(q, w);
        const Vec3 wk1 = body_angular_accel(inertia, w, torque);

        const Quat q2 = add(q, scale(qk1, dt * 0.5));
        const Vec3 w2 = w + (dt * 0.5) * wk1;
        const Quat qk2 = derivative(q2, w2);
        const Vec3 wk2 = body_angular_accel(inertia, w2, torque);

        const Quat q3 = add(q, scale(qk2, dt * 0.5));
        const Vec3 w3 = w + (dt * 0.5) * wk2;
        const Quat qk3 = derivative(q3, w3);
        const Vec3 wk3 = body_angular_accel(inertia, w3, torque);

        const Quat q4 = add(q, scale(qk3, dt));
        const Vec3 w4 = w + dt * wk3;
        const Quat qk4 = derivative(q4, w4);
        const Vec3 wk4 = body_angular_accel(inertia, w4, torque);

        const Quat qsum =
            add(add(qk1, scale(qk2, 2.0)), add(scale(qk3, 2.0), qk4));
        q = unit(add(q, scale(qsum, dt / 6.0)));

        const Vec3 wsum = wk1 + 2.0 * wk2 + 2.0 * wk3 + wk4;
        w = w + (dt / 6.0) * wsum;
    }

    if (omega_final != nullptr) *omega_final = w;
    return q;
}

} // namespace quatkin
Now build and test:
shell
cmake -S /opt/quatkin -B /opt/quatkin/build -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && cmake --build /opt/quatkin/build --parallel 2>&1 | tail -5 && cd /opt/quatkin/build && ctest --output-on-failure
output
[ 71%] Building CXX object CMakeFiles/quatkin_cli.dir/src/main.cpp.o
[ 85%] Linking CXX executable quatkin_cli
[ 85%] Built target quatkin_cli
[100%] Linking CXX executable test_smoke
[100%] Built target test_smoke
Test project /opt/quatkin/build
    Start 1: smoke
1/1 Test #1: smoke ............................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.00 sec

[stdout]
[ 71%] Building CXX object CMakeFiles/quatkin_cli.dir/src/main.cpp.o
[ 85%] Linking CXX executable quatkin_cli
[ 85%] Built target quatkin_cli
[100%] Linking CXX executable test_smoke
[100%] Built target test_smoke
Test project /opt/quatkin/build
    Start 1: smoke
1/1 Test #1: smoke ............................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.00 sec
The build succeeds and the test suite passes. Let me also sanity-check the CLI against the physical example to confirm the conventions are right end-to-end:
shell
./quatkin_cli ../examples/spin_z.rotation
output
# quaternion attitude-propagation report
omega=(0.000000e+00, 0.000000e+00, 1.000000e+00) t_final=1.570800e+00
q_final=(7.071055e-01, 0.000000e+00, 0.000000e+00, 7.071081e-01)
norm=1.000000e+00 angle_rad=1.570800e+00

[stdout]
# quaternion attitude-propagation report
omega=(0.000000e+00, 0.000000e+00, 1.000000e+00) t_final=1.570800e+00
q_final=(7.071055e-01, 0.000000e+00, 0.000000e+00, 7.071081e-01)
norm=1.000000e+00 angle_rad=1.570800e+00
Done. I implemented the numerical core in `/opt/quatkin/src/solver.cpp` (the only file changed), and everything builds and passes. What I implemented: - **`multiply`** , Hamilton quaternion product. - **`normalize`** , divides by norm (still throws on the zero quaternion). - **`rotate` / `rotate_inv`** , active rotation `q·(0,v)·q*` and its inverse via the conjugate, with `q` normalized first. - **`from_matrix`** , row-major 3×3 → quaternion using Shepperd's branch-by-largest-component method for numerical stability. - **`geodesic_angle`** , `2·acos(|⟨a,b⟩|)`, accounting for the `q`/`−q` double cover and clamping. - **`slerp`** , shortest-arc spherical interpolation with a normalized-lerp fallback for nearly-parallel inputs. - **`derivative`** , body-frame kinematics `q̇ = ½·q⊗(0,ω)`. - **`rk4_step` / `integrate`** , constant-rate attitude RK4 with per-step renormalization. - **`body_angular_accel`** , Euler's rigid-body equations for diagonal inertia: `I·ω̇ = τ − ω×(Iω)`. - **`integrate_dynamics`** , coupled `(q, ω)` RK4, renormalizing `q` each step, writing the final rate through `omega_final`. Verification: - `ctest`: **1/1 passed** (smoke + exception tests). - The `spin_z` example propagates identity at 1 rad/s about +z for ~π/2 s and yields `q ≈ (0.7071, 0, 0, 0.7071)`, `norm = 1`, `angle = 1.5708 rad` , matching the expected 90° rotation, confirming the conventions are consistent end-to-end. No public headers or signatures were touched.
Final diff , what the agent changed

No file changes, the agent left the workspace untouched.

Verifier output , the proof behind the reward
tests/test.sh
== Configuring (cmake) ==
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/tmp.xdHpfXb5pU/quatkin/build_grade
== Building ==
[  4%] Building CXX object CMakeFiles/quatkin.dir/src/solver.cpp.o
[  9%] Building CXX object CMakeFiles/quatkin.dir/src/io.cpp.o
[ 14%] Linking CXX static library libquatkin.a
[ 14%] Built target quatkin
[ 19%] Building CXX object CMakeFiles/quatkin_cli.dir/src/main.cpp.o
[ 23%] Building CXX object tests/CMakeFiles/test_rotation.dir/test_rotation.cpp.o
[ 28%] Building CXX object tests/CMakeFiles/test_algebra.dir/test_algebra.cpp.o
[ 33%] Building CXX object tests/CMakeFiles/test_matrix.dir/test_matrix.cpp.o
[ 38%] Building CXX object tests/CMakeFiles/test_kinematics_hidden.dir/test_kinematics_hidden.cpp.o
[ 42%] Building CXX object tests/CMakeFiles/test_euler.dir/test_euler.cpp.o
[ 47%] Building CXX object tests/CMakeFiles/test_dynamics.dir/test_dynamics.cpp.o
[ 52%] Building CXX object tests/CMakeFiles/test_slerp.dir/test_slerp.cpp.o
[ 57%] Building CXX object tests/CMakeFiles/test_edge.dir/test_edge.cpp.o
[ 61%] Linking CXX executable quatkin_cli
[ 61%] Built target quatkin_cli
[ 66%] Linking CXX executable test_rotation
[ 71%] Linking CXX executable test_euler
[ 76%] Linking CXX executable test_algebra
[ 80%] Linking CXX executable test_edge
[ 85%] Linking CXX executable test_matrix
[ 90%] Linking CXX executable test_kinematics_hidden
[ 90%] Built target test_euler
[ 95%] Linking CXX executable test_slerp
[ 95%] Built target test_matrix
[ 95%] Built target test_rotation
[ 95%] Built target test_kinematics_hidden
[ 95%] Built target test_algebra
[ 95%] Built target test_edge
[ 95%] Built target test_slerp
[100%] Linking CXX executable test_dynamics
[100%] Built target test_dynamics
== Running hidden test suite ==
Test project /tmp/tmp.xdHpfXb5pU/quatkin/build_grade
    Start 1: test_algebra
1/8 Test #1: test_algebra .....................   Passed    0.00 sec
    Start 2: test_rotation
2/8 Test #2: test_rotation ....................   Passed    0.00 sec
    Start 3: test_matrix
3/8 Test #3: test_matrix ......................***Failed    0.00 sec
[ FAIL ] from_matrix_round_trip_random: from_matrix sign-pinned round trip w (got -0.035791, want 0.035791, tol 0.000000)
[ FAIL ] from_matrix_inverts_rodrigues: from_matrix inverts Rodrigues matrix
[ PASS ] from_matrix_180_degree_branches
[ FAIL ] from_matrix_trace_boundary_sweep: from_matrix across trace branch boundary
[ FAIL ] from_matrix_trace_near_minus_one_off_axis: near-180 off-axis matrix recovered across branch crossover
[ FAIL ] from_matrix_rejects_non_rotation: reflection matrix rejected
----
1/6 tests passed

    Start 4: test_kinematics_hidden
4/8 Test #4: test_kinematics_hidden ...........   Passed    0.00 sec
    Start 5: test_euler
5/8 Test #5: test_euler .......................   Passed    0.00 sec
    Start 6: test_dynamics
6/8 Test #6: test_dynamics ....................   Passed    0.01 sec
    Start 7: test_slerp
7/8 Test #7: test_slerp .......................   Passed    0.00 sec
    Start 8: test_edge
8/8 Test #8: test_edge ........................***Failed    0.00 sec
[ PASS ] geodesic_angle_self_zero_and_double_cover
[ PASS ] geodesic_angle_range
[ FAIL ] exceptions_contract: from_matrix non-rotation runtime_error
----
2/3 tests passed


75% tests passed, 2 tests failed out of 8

Label Time Summary:
hidden    =   0.03 sec*proc (8 tests)

Total Test time (real) =   0.03 sec

The following tests FAILED:
	  3 - test_matrix (Failed)
	  8 - test_edge (Failed)


Errors while running CTest
FAIL: one or more hidden tests failed

Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_6f372cee177e452a. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.

Trial trial_6f372cee177e452a · verifier authoritative; classifier explanatory.