Attitude Determination by the Davenport q-Method (space-systems/adcs/attitude-determination-quest)
Use when the task is optimal spacecraft attitude determination from
multiple weighted vector observations: fitting one rotation to N >= 2
body and reference direction pairs by minimizing Wahba's cost with the
Davenport q-method (QUEST-style), and gating the result on the
observation residuals and the consistency verdict. This leaf implements
the full pipeline in pure Python (stdlib only, deterministic): the
attitude profile matrix, the symmetric 4x4 K matrix, the fixed-sweep
Jacobi eigen-solution, and the scalar-last eigenvector read. It is the
N > 2 complement of the pair-based sibling leaf attitude-determination-triad,
and it consumes quaternion products through rotate_vector internally so
the numerics stay self-contained. Star-tracker and sun-pointing supply
the sensor directions this method consumes.
Convention: observations b_i (unit vectors in the BODY frame) relate to
the reference vectors r_i (unit vectors in the REFERENCE frame) by
b_i = A(q) r_i with the ACTIVE rotation convention A(q) v = q * (0, v) *
q_conj under the Hamilton quaternion product, q = (w, x, y, z) with w
the scalar.
Domain quick reference
- Wahba's cost: J(q) = sum_i w_i * |b_i - A(q) r_i|^2, with w_i the
positive weight of observation i. The optimal attitude is the unit
eigenvector of K for the LARGEST eigenvalue; the remaining
eigenvalues bound the attainable cost.
- Attitude profile matrix: B = sum_i w_i * b_i * r_i^T (3x3). Its trace
sigma = trace(B) feeds the K construction.
- Davenport K matrix: S = B + B^T; z = (B[2][1]-B[1][2], B[0][2]-B[2][0],
B[1][0]-B[0][1]); K = [[S - sigma*I3, z], [z^T, sigma]]. K is symmetric
4x4 by construction. The z-vector sign shown here is the one verified
to recover the generating quaternion under the active rotation
convention; flipping it recovers the inverse rotation instead.
- Eigen-solution: fixed-sweep Jacobi rotations zero the largest
off-diagonal entry, theta = 0.5 * atan2(2*K[p][q], K[q][q]-K[p][p])
(theta = pi/4 when the diagonal entries are equal), stopping when the
largest off-diagonal magnitude drops below JACOBI_TOL = 1e-13 or after
JACOBI_MAX_SWEEPS = 60 sweeps.
- Eigenvector read SCALAR-LAST: the optimal quaternion is q =
(V[3][imax], V[0][imax], V[1][imax], V[2][imax]) for the column imax of
the largest eigenvalue, normalized to unit length. q and -q denote the
same rotation.
- Consistency verdict: identity_ok = (max residual < 1e-6), where the
residual of observation i is |b_i - A(q) r_i|.
- Outputs: the optimal quaternion, lambda_max, the 3x3 attitude matrix
whose rows are A(q) applied to the reference axes, the residuals, the
achieved Wahba cost, and identity_ok.
Workflow
- Collect the measured unit directions in the body frame and the
matching unit directions in the reference frame (star tracker or sun
sensor outputs against an ephemeris or star catalog).
- Check the vectors are unit length: attitude_profile raises ValueError
for non-unit vectors, fewer than MIN_OBSERVATIONS = 2 observations,
count mismatches, weight mismatches and non-positive weights.
- Build B = attitude_profile(observations, references, weights) with
the per-observation confidence weights (optional, default all ones).
- Assemble the Davenport matrix with davenport_k_matrix(B), returning
(K, sigma, z); keep the z sign as returned.
- Solve the eigen-problem with jacobi_eigen_sym4(K), pick the largest
eigenvalue lambda_max and its eigenvector column.
- Read the quaternion scalar-last and normalize; recover the attitude
matrix with attitude_matrix_from_quaternion.
- Get the full verdict from quest_solution(observations, references,
weights): q_optimal, lambda_max, attitude_matrix, residuals,
wahba_cost and identity_ok.
- Check minimality with wahba_cost on a perturbed quaternion: the
optimal attitude must sit strictly below every neighbor in cost.
- Confirm the deterministic behavior with the contract test
scripts/test_attitude_determination_quest.py.
Worked example
Three noise-free synthetic observations generated from a KNOWN attitude
q_true = 90-degree rotation about the body z axis: q_true =
(cos(pi/4), 0, 0, sin(pi/4)) = (0.70710678, 0, 0, 0.70710678).
Reference vectors r = [(1,0,0), (0,1,0), (0,0,1)]; observations
b_i = A(q_true) r_i = [(0,1,0), (-1,0,0), (0,0,1)]. All weights 1.0.
quest_solution on this data returns the real module outputs:
- q_optimal = (0.70710678, 0.0, 0.0, 0.70710678), recovering q_true to
a sign-aware error of 1.1e-16 (spec bound 1e-9).
- lambda_max = 3.0000000000000004, about 3.0 as expected for three
unit-weight observations of a pure rotation.
- wahba_cost = 1.2e-31 (spec bound 1e-9); identity_ok True; residuals
per observation [2.5e-16, 2.5e-16, 0.0].
- attitude_matrix rows are the observed body axes (0,1,0), (-1,0,0),
(0,0,1) to 3.3e-16.
- A quaternion perturbed by an extra 5.7-degree rotation about z has
Wahba cost 0.01998, strictly larger than the optimum plus 1e-9, so
the minimality verdict holds.
A second non-symmetric case, 45 degrees about the (1,1,1)/sqrt(3) axis
with six reference vectors [(1,0,0),(0,1,0),(0,0,1),(1,1,0),(1,-1,0),
(0,1,1)] (the last three normalized), recovers q_true =
(0.92387953, 0.22094238, 0.22094238, 0.22094238) to 1.1e-16; this case
catches z-vector sign slips, since a flipped sign recovers the inverse
rotation and fails the 1e-9 consistency check on A(q) r_i = b_i.
Pitfalls
- Feeding unnormalized or mis-sized inputs: attitude_profile raises
ValueError for non-unit vectors (norm outside 1 +- 1e-6), fewer than
MIN_OBSERVATIONS = 2 observations, count mismatches, weight
mismatches, and non-positive weights.
- Flipping the z-vector sign when assembling K: the sign documented in
this leaf is the one verified to recover the generating quaternion
under the active rotation convention; a flipped sign recovers the
inverse rotation and fails the A(q) r_i = b_i consistency check.
- Reading the eigenvector in the wrong layout: the optimal quaternion
is read SCALAR-LAST from the Jacobi eigen-solution, and q and -q
denote the same rotation, so sign-aware comparison (min over q and
-q) is required when validating against a known truth.
- Trusting the optimum without the minimality check: confirm the
achieved Wahba cost sits strictly below a perturbed neighbor plus
1e-9 before gating the attitude reference.
- Confusing this leaf with its pair-based sibling: QUEST/Davenport is
the N > 2 complement of attitude-determination-triad; a single pair
of directions belongs to the triad leaf.
- Replacing the fixed-sweep Jacobi solver with an RNG-based one: this
pipeline is deterministic by contract, and two consecutive runs must
return byte-identical floats.
Verification
- Confirm quest_solution recovers both generating quaternions above to
1e-9 with the sign-aware comparison min over q and -q.
- Confirm lambda_max is about 3.0 and identity_ok is True on the
noise-free worked example.
- Confirm the perturbed-quaternion Wahba cost is strictly larger than
the optimal cost plus 1e-9.
- Confirm A(q) r_i equals b_i to 1e-9 on both worked cases (active
rotation convention consistency).
- Confirm two consecutive runs return byte-identical floats (no RNG).
- Confirm ValueError rejection: fewer than 2 observations, count
mismatch, non-unit vectors (norm outside 1 +- 1e-6), non-positive
weights, weight length mismatch.
- Confirm the solution dict exposes exactly the six documented keys.
- Run the offline contract test: python3
scripts/test_attitude_determination_quest.py (34 tests, deterministic).
Related leaves
- space-systems/adcs/attitude-determination-triad: the pack sibling for
the pair-of-directions case, whose body defers the N > 2 Wahba content
to this leaf.
- space-systems/adcs/star-tracker: supplies the measured body-frame
directions this method consumes.
- cross-cutting/numerics/quaternion-algebra: quaternion products and
conversions for downstream quaternion use; this leaf keeps its own
internal product kernels so the pipeline is self-contained.
- gnc-autonomy/space/attitude-dynamics: the dynamics context around the
attitude estimate this leaf produces.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_attitude_determination_quest.py
The test covers quaternion product identities and closed-form values,
the active rotation convention, the 90-degree z worked example
(recovery to 1e-9, lambda_max about 3.0, near-zero Wahba cost, matrix
rows equal to the observed axes, exact documented keys), minimality
under perturbation, the second 45-degree (1,1,1)/sqrt(3) six-observation
case with per-observation consistency, ValueError rejection of all
non-physical inputs, the Jacobi reconstruction identity on K,
determinism across runs, and weighted recovery.
Compliance
- Standards referenced, not reproduced: ECSS-E-ST-60 frames attitude
determination for European missions; the Davenport q-method and the
Wahba cost are open attitude-determination literature, summary-only
per standards-map.yaml.
- compliance: STANDARDS-REF, gated: false.
1---2name: attitude-determination-quest3description: Use when you must determine the optimal attitude of a spacecraft from multiple vector observations by the Davenport q-method: minimize the Wahba cost over N >= 2 weighted body and reference direction pairs, form the attitude profile matrix B, assemble the symmetric 4x4 Davenport K matrix, and extract the largest eigenvalue eigenvector with a deterministic fixed-sweep Jacobi iteration. Produces the optimal attitude quaternion, the 3x3 attitude matrix, the per-observation residuals, the achieved Wahba cost and the consistency verdict that gate a multi-vector attitude determination. Trigger: wahba problem, davenport q method, quest, optimal attitude quaternion, attitude matrix, observation residuals, body vector, reference vector, multi-vector attitude determination.4license: Apache-2.05---67# Attitude Determination by the Davenport q-Method (space-systems/adcs/attitude-determination-quest)89Use when the task is optimal spacecraft attitude determination from10multiple weighted vector observations: fitting one rotation to N >= 211body and reference direction pairs by minimizing Wahba's cost with the12Davenport q-method (QUEST-style), and gating the result on the13observation residuals and the consistency verdict. This leaf implements14the full pipeline in pure Python (stdlib only, deterministic): the15attitude profile matrix, the symmetric 4x4 K matrix, the fixed-sweep16Jacobi eigen-solution, and the scalar-last eigenvector read. It is the17N > 2 complement of the pair-based sibling leaf attitude-determination-triad,18and it consumes quaternion products through rotate_vector internally so19the numerics stay self-contained. Star-tracker and sun-pointing supply20the sensor directions this method consumes.2122Convention: observations b_i (unit vectors in the BODY frame) relate to23the reference vectors r_i (unit vectors in the REFERENCE frame) by24b_i = A(q) r_i with the ACTIVE rotation convention A(q) v = q * (0, v) *25q_conj under the Hamilton quaternion product, q = (w, x, y, z) with w26the scalar.2728## Domain quick reference2930- Wahba's cost: J(q) = sum_i w_i * |b_i - A(q) r_i|^2, with w_i the31 positive weight of observation i. The optimal attitude is the unit32 eigenvector of K for the LARGEST eigenvalue; the remaining33 eigenvalues bound the attainable cost.34- Attitude profile matrix: B = sum_i w_i * b_i * r_i^T (3x3). Its trace35 sigma = trace(B) feeds the K construction.36- Davenport K matrix: S = B + B^T; z = (B[2][1]-B[1][2], B[0][2]-B[2][0],37 B[1][0]-B[0][1]); K = [[S - sigma*I3, z], [z^T, sigma]]. K is symmetric38 4x4 by construction. The z-vector sign shown here is the one verified39 to recover the generating quaternion under the active rotation40 convention; flipping it recovers the inverse rotation instead.41- Eigen-solution: fixed-sweep Jacobi rotations zero the largest42 off-diagonal entry, theta = 0.5 * atan2(2*K[p][q], K[q][q]-K[p][p])43 (theta = pi/4 when the diagonal entries are equal), stopping when the44 largest off-diagonal magnitude drops below JACOBI_TOL = 1e-13 or after45 JACOBI_MAX_SWEEPS = 60 sweeps.46- Eigenvector read SCALAR-LAST: the optimal quaternion is q =47 (V[3][imax], V[0][imax], V[1][imax], V[2][imax]) for the column imax of48 the largest eigenvalue, normalized to unit length. q and -q denote the49 same rotation.50- Consistency verdict: identity_ok = (max residual < 1e-6), where the51 residual of observation i is |b_i - A(q) r_i|.52- Outputs: the optimal quaternion, lambda_max, the 3x3 attitude matrix53 whose rows are A(q) applied to the reference axes, the residuals, the54 achieved Wahba cost, and identity_ok.5556## Workflow57581. Collect the measured unit directions in the body frame and the59 matching unit directions in the reference frame (star tracker or sun60 sensor outputs against an ephemeris or star catalog).612. Check the vectors are unit length: attitude_profile raises ValueError62 for non-unit vectors, fewer than MIN_OBSERVATIONS = 2 observations,63 count mismatches, weight mismatches and non-positive weights.643. Build B = attitude_profile(observations, references, weights) with65 the per-observation confidence weights (optional, default all ones).664. Assemble the Davenport matrix with davenport_k_matrix(B), returning67 (K, sigma, z); keep the z sign as returned.685. Solve the eigen-problem with jacobi_eigen_sym4(K), pick the largest69 eigenvalue lambda_max and its eigenvector column.706. Read the quaternion scalar-last and normalize; recover the attitude71 matrix with attitude_matrix_from_quaternion.727. Get the full verdict from quest_solution(observations, references,73 weights): q_optimal, lambda_max, attitude_matrix, residuals,74 wahba_cost and identity_ok.758. Check minimality with wahba_cost on a perturbed quaternion: the76 optimal attitude must sit strictly below every neighbor in cost.779. Confirm the deterministic behavior with the contract test78 scripts/test_attitude_determination_quest.py.7980## Worked example8182Three noise-free synthetic observations generated from a KNOWN attitude83q_true = 90-degree rotation about the body z axis: q_true =84(cos(pi/4), 0, 0, sin(pi/4)) = (0.70710678, 0, 0, 0.70710678).85Reference vectors r = [(1,0,0), (0,1,0), (0,0,1)]; observations86b_i = A(q_true) r_i = [(0,1,0), (-1,0,0), (0,0,1)]. All weights 1.0.8788quest_solution on this data returns the real module outputs:8990- q_optimal = (0.70710678, 0.0, 0.0, 0.70710678), recovering q_true to91 a sign-aware error of 1.1e-16 (spec bound 1e-9).92- lambda_max = 3.0000000000000004, about 3.0 as expected for three93 unit-weight observations of a pure rotation.94- wahba_cost = 1.2e-31 (spec bound 1e-9); identity_ok True; residuals95 per observation [2.5e-16, 2.5e-16, 0.0].96- attitude_matrix rows are the observed body axes (0,1,0), (-1,0,0),97 (0,0,1) to 3.3e-16.98- A quaternion perturbed by an extra 5.7-degree rotation about z has99 Wahba cost 0.01998, strictly larger than the optimum plus 1e-9, so100 the minimality verdict holds.101102A second non-symmetric case, 45 degrees about the (1,1,1)/sqrt(3) axis103with six reference vectors [(1,0,0),(0,1,0),(0,0,1),(1,1,0),(1,-1,0),104(0,1,1)] (the last three normalized), recovers q_true =105(0.92387953, 0.22094238, 0.22094238, 0.22094238) to 1.1e-16; this case106catches z-vector sign slips, since a flipped sign recovers the inverse107rotation and fails the 1e-9 consistency check on A(q) r_i = b_i.108109110## Pitfalls111112- Feeding unnormalized or mis-sized inputs: attitude_profile raises113 ValueError for non-unit vectors (norm outside 1 +- 1e-6), fewer than114 MIN_OBSERVATIONS = 2 observations, count mismatches, weight115 mismatches, and non-positive weights.116- Flipping the z-vector sign when assembling K: the sign documented in117 this leaf is the one verified to recover the generating quaternion118 under the active rotation convention; a flipped sign recovers the119 inverse rotation and fails the A(q) r_i = b_i consistency check.120- Reading the eigenvector in the wrong layout: the optimal quaternion121 is read SCALAR-LAST from the Jacobi eigen-solution, and q and -q122 denote the same rotation, so sign-aware comparison (min over q and123 -q) is required when validating against a known truth.124- Trusting the optimum without the minimality check: confirm the125 achieved Wahba cost sits strictly below a perturbed neighbor plus126 1e-9 before gating the attitude reference.127- Confusing this leaf with its pair-based sibling: QUEST/Davenport is128 the N > 2 complement of attitude-determination-triad; a single pair129 of directions belongs to the triad leaf.130- Replacing the fixed-sweep Jacobi solver with an RNG-based one: this131 pipeline is deterministic by contract, and two consecutive runs must132 return byte-identical floats.133## Verification134135- Confirm quest_solution recovers both generating quaternions above to136 1e-9 with the sign-aware comparison min over q and -q.137- Confirm lambda_max is about 3.0 and identity_ok is True on the138 noise-free worked example.139- Confirm the perturbed-quaternion Wahba cost is strictly larger than140 the optimal cost plus 1e-9.141- Confirm A(q) r_i equals b_i to 1e-9 on both worked cases (active142 rotation convention consistency).143- Confirm two consecutive runs return byte-identical floats (no RNG).144- Confirm ValueError rejection: fewer than 2 observations, count145 mismatch, non-unit vectors (norm outside 1 +- 1e-6), non-positive146 weights, weight length mismatch.147- Confirm the solution dict exposes exactly the six documented keys.148- Run the offline contract test: python3149 scripts/test_attitude_determination_quest.py (34 tests, deterministic).150151## Related leaves152153- space-systems/adcs/attitude-determination-triad: the pack sibling for154 the pair-of-directions case, whose body defers the N > 2 Wahba content155 to this leaf.156- space-systems/adcs/star-tracker: supplies the measured body-frame157 directions this method consumes.158- cross-cutting/numerics/quaternion-algebra: quaternion products and159 conversions for downstream quaternion use; this leaf keeps its own160 internal product kernels so the pipeline is self-contained.161- gnc-autonomy/space/attitude-dynamics: the dynamics context around the162 attitude estimate this leaf produces.163164## Behavior contract (gate 3)165166Run the deterministic contract test (stdlib unittest, offline):167168 python3 scripts/test_attitude_determination_quest.py169170The test covers quaternion product identities and closed-form values,171the active rotation convention, the 90-degree z worked example172(recovery to 1e-9, lambda_max about 3.0, near-zero Wahba cost, matrix173rows equal to the observed axes, exact documented keys), minimality174under perturbation, the second 45-degree (1,1,1)/sqrt(3) six-observation175case with per-observation consistency, ValueError rejection of all176non-physical inputs, the Jacobi reconstruction identity on K,177determinism across runs, and weighted recovery.178179## Compliance180181- Standards referenced, not reproduced: ECSS-E-ST-60 frames attitude182 determination for European missions; the Davenport q-method and the183 Wahba cost are open attitude-determination literature, summary-only184 per standards-map.yaml.185- compliance: STANDARDS-REF, gated: false.