Reaction Wheel Control (space-systems/adcs/reaction-wheel-control)
Use when the task is the reaction wheel attitude control law for a
spacecraft pointing mode: commanding wheel torques from the quaternion
error and the body rate with PD gains, propagating the wheel momentum
including the body rate transport coupling, clipping torque commands
and flagging wheel momentum saturation during an acquisition, and
computing the magnetorquer momentum desaturation command that unloads
an accumulated wheel momentum excess. This leaf implements the wheel
control loop in pure Python, stdlib only. It pairs with
space-systems/adcs/attitude-control-sizing for actuator sizing and
margins, space-systems/adcs/magnetorquer-control for the desaturation
actuator, and gnc-autonomy/space/attitude-dynamics for the rigid body
plant; this leaf is the control law and momentum management, not the
sizing, not the detumbling law, not the attitude determination.
Conventions: quaternions are scalar-first (w, x, y, z) unit tuples with
the Hamilton product. The error quaternion is q_err = q_current (x)
q_ref^-1 (rotation from the reference attitude to the current attitude,
identity at the target), which equals the q_ref^-1 (x) q form under the
Shuster product convention. tau_cmd is the torque the wheel cluster
produces about the body axes, so in the closed loop demo the bus obeys
I * omega_dot = tau_cmd with the module spacecraft inertia I_SC = 0.5
kg m^2 (inertially spherical, documented module constant). The wheel
momentum h_w follows the commanded torque impulse, so during the z slew
of the worked example it mirrors the z command sign.
Domain quick reference
- Error quaternion: q_err = q_current (x) q_ref^-1, identity when the
spacecraft is at the target; small angle attitude error
theta_err ~= 2 * q_err_vec (rad).
- Rate error: omega_err = omega_body - omega_ref (omega_ref = 0 for
inertial pointing).
- PD wheel torque command (body frame): tau_cmd = -kp * theta_err -
kd * omega_err, kp in 1/s^2, kd in 1/s. Integral action lives in the
wheel momentum, not in an attitude integrator.
- Wheel momentum plant: h_w_dot = tau_cmd - omega_body x h_w, where
the cross term is the body rate transport coupling. Three orthogonal
body axis wheels, each of inertia j_w, give wheel speeds
omega_w_i = h_w_i / j_w.
- Torque saturation: clip each axis at +-tau_max and flag when clipped.
- Momentum saturation: flag when |h_w_i| exceeds h_max_i and report the
excess; desaturation is then required.
- Momentum desaturation torque over horizon T_desat:
tau_desat = -(h_w - h_target) / T_desat, h_target default 0.
- Desaturation dipole: m_desat = (B x tau_desat) / |B|^2 solves
torque = m x B, so m_desat x B equals tau_desat for the torque
component perpendicular to B; warn when tau_desat lies nearly along B
(the cross product magnitude is then small and the achievable torque
is small).
- Units are SI throughout: N m, N m s, rad, rad/s, kg m^2, s, T, A m^2.
- ECSS frames the spacecraft control context; the relations above are
standard engineering methodology, summary-only.
Workflow
- Set the pointing target: reference quaternion q_ref, reference rate
omega_ref (default 0), and the initial attitude q and body rate
omega from the attitude determination leaf or the mission timeline.
- Form the error with quaternion_error(q_current, q_ref) and map it to
the small angle vector with attitude_error_vector (2 * q_err_vec).
- Command the wheel torque with pd_wheel_torque(kp, kd, theta_err,
omega_err). Choose kp and kd from the closed loop frequency and
damping: omega_n = sqrt(kp / I) and zeta = kd / (2 * sqrt(kp * I))
for a bus inertia I.
- Clip the command at the wheel torque authority with
torque_saturation(tau_cmd, tau_max) and honor the flag.
- Advance the wheel momentum with wheel_momentum_update(h_w, tau_cmd,
omega_body, dt), then check momentum_saturation(h_w, h_max); a flag
here means desaturation is needed.
- For acquisition, run the closed loop demo with run_wheel_control
(omega_body_samples = None integrates the body rate from omega0);
pass a sampled body rate profile instead to evaluate commands along
a recorded trajectory. The demo uses the module bus inertia I_SC.
- When the wheels are saturated, compute the unload torque with
desaturation_torque(h_w, h_target, t_desat) and the dipole with
dipole_from_torque(tau_desat, b_field), and heed the alignment
warning before passing the dipole to the magnetorquer leaf.
- Confirm the deterministic checks with the contract test
scripts/test_reaction_wheel_control.py.
Worked example
Spacecraft with three orthogonal wheels, j_w = 0.01 kg m^2 each, at
rest at q0 = [1, 0, 0, 0]. Target q_ref = [cos 5 deg, 0, 0, sin 5 deg]
rotates the spacecraft 10 deg about the body z axis. Gains kp = 0.05
1/s^2, kd = 0.2 1/s, dt = 0.01 s, run 20 s (2000 steps), tau_max =
0.1 N m, h_max = 0.5 N m s.
- Error at start: q_err = q0 (x) q_ref^-1 gives q_err_vec =
(0, 0, -sin 5 deg), so theta_err = (0, 0, -0.1743) rad and the first
wheel torque command is +0.0087 N m about +z, driving the bus toward
the target (the wheel reaction on the bus is opposite about the z
axis). The rate term damps once the bus rotates.
- Acquisition: the bus settles with small overshoot (attitude error
peaks near 0.76 deg after the first crossing) and the attitude error
at 20 s is 0.11 deg, below the 1 deg acquisition tolerance.
- Wheel momentum: h_w follows the signed torque impulse integral; the
magnitude peaks at 0.0134 N m s (wheel speed 1.34 rad/s, about 13
rpm) and returns near zero as the braking torque cancels the
acceleration impulse. Both saturation flags stay clear with the
generous limits.
- Torque saturation: rerun with tau_max = 1e-4 N m; the command clips
immediately, the flag trips, and the error at 20 s is 7.7 deg, much
slower than the unconstrained 0.11 deg.
- Momentum saturation: rerun with h_max = 0.004 N m s; the momentum
flag trips at the peak of the maneuver.
- Desaturation: with h_w = [0, 0, 0.2] N m s excess over h_target and
B = [2e-5, 0, 0] T over T_desat = 100 s: tau_desat =
-[0, 0, 0.2] / 100 = [0, 0, -0.002] N m and m_desat =
(B x tau_desat) / |B|^2 = [0, 100, 0] A m^2; m_desat x B reproduces
tau_desat exactly (perpendicular geometry) with no alignment warning.
Pitfalls
- Getting the error quaternion convention backwards: q_err =
q_current (x) q_ref^-1 under the scalar-first Hamilton product; the
sign of the first torque command must drive the bus toward the
target (the worked example commands +z toward a +z rotation).
- Picking gains without the bus inertia: kp and kd map to omega_n and
zeta through I (omega_n = sqrt(kp / I)); gains tuned on the wrong
inertia oscillate or stall the acquisition.
- Dropping the transport term: wheel momentum integrates
h_w_dot = tau_cmd - omega_body x h_w; the cross term only vanishes
for geometry parallel to the rotation axis, so it must stay in the
propagation for general slews.
- Ignoring the saturation flags: a clipped torque command or a wheel
past h_max silently degrades pointing (the 1e-4 N m tau_max rerun
ends at 7.7 deg instead of 0.11 deg) - honor both flags before
declaring acquisition.
- Commanding desaturation along the field: when tau_desat lies nearly
along B the m x B torque is small; heed the alignment warning before
passing the dipole to the magnetorquer leaf.
- Feeding non-physical inputs: non-positive gains, j_w <= 0, dt <= 0,
tau_max <= 0, h_max <= 0, t_desat <= 0, or a field at or below
1e-12 T raise ValueError.
Verification
- Confirm quaternion_error returns identity when q_current = q_ref and
unit norm otherwise; doubling kp doubles the initial torque
magnitude; zero error and zero rate give exactly zero torque.
- Confirm the acquisition run reaches the 10 deg z target with the
attitude error below 1 deg at 20 s (measured 0.11 deg on this
implementation) and small overshoot.
- Confirm h_w equals the signed torque impulse integral during the z
slew (the transport term vanishes for the parallel geometry) and
stays bounded under generous tau_max and h_max.
- Confirm torque_saturation clips per axis and flags, and that the
tiny tau_max rerun flags with slower settling.
- Confirm the desaturation numbers: tau_desat = -0.002 N m about z,
m_desat = +100 A m^2 about y for B along x, m_desat x B within
1e-12 of tau_desat, and the alignment warning trips for a torque
demand along the field.
- Confirm ValueError rejection of non-positive gains, j_w <= 0,
dt <= 0, h_max <= 0, tau_max <= 0, t_desat <= 0, a field at or below
1e-12 T, and any non-finite input.
- Run the contract test offline: python3
scripts/test_reaction_wheel_control.py (35 tests, deterministic).
Related leaves
- space-systems/adcs/attitude-control-sizing: momentum wheel sizing and
margins for a commanded maneuver; this leaf commands what that leaf
sizes.
- space-systems/adcs/magnetorquer-control: the desaturation actuator
and the dipole to torque link, same m x B convention.
- space-systems/adcs/attitude-determination-triad: the attitude
reference used by the control law.
- space-systems/adcs/star-tracker: fine attitude reference for
precision pointing.
- space-systems/adcs/sun-pointing: the safe hold pointing target.
- gnc-autonomy/space/attitude-dynamics: the rigid body plant and
propagation that this leaf keeps simplified.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_reaction_wheel_control.py
The test covers the worked example acquisition (10 deg z slew reaches
the target below 1 deg error at 20 s, small overshoot), the PD
identities (zero error gives zero torque, doubling kp doubles the
initial command, the rate term opposes the rate), the quaternion error
identity at the target and its worked example vector part, wheel
momentum integration with the transport term, per axis torque clipping
and momentum excess flags, torque and momentum saturation in the run,
the momentum equals impulse integral identity, the desaturation torque
and dipole worked example with the m x B reconstruction to 1e-12 and
the alignment warning, the rate profile evaluation mode, and ValueError
rejection of every non-physical input class.
Compliance
- Standards referenced, not reproduced: ECSS covers spacecraft control
(standards-map.yaml); the control law relations above are standard
engineering methodology, summary-only.
- compliance: STANDARDS-REF, gated: false.
1---2name: reaction-wheel-control3description: Use when you must design the reaction wheel control law for a spacecraft pointing maneuver: command wheel torques from quaternion error feedback and body rate with PD gains, integrate wheel momentum with the body rate transport term, clip torque commands and flag wheel momentum saturation, and compute the momentum desaturation command with its magnetorquer dipole estimate. Produces the commanded wheel torque vector, the accumulated wheel momentum, the saturation verdict, and the desaturation torque and dipole. Trigger: reaction wheel control, wheel torque command, wheel momentum saturation, momentum desaturation, desaturation horizon, wheel momentum excess, unloads wheel cluster, local magnetic field, magnetorquer dipole, torque demand along the field, warn when the demand lies along the field, find the desaturation dipole for the target momentum, quaternion error feedback, desaturation torque, spacecraft pointing.4license: Apache-2.05---67# Reaction Wheel Control (space-systems/adcs/reaction-wheel-control)89Use when the task is the reaction wheel attitude control law for a10spacecraft pointing mode: commanding wheel torques from the quaternion11error and the body rate with PD gains, propagating the wheel momentum12including the body rate transport coupling, clipping torque commands13and flagging wheel momentum saturation during an acquisition, and14computing the magnetorquer momentum desaturation command that unloads15an accumulated wheel momentum excess. This leaf implements the wheel16control loop in pure Python, stdlib only. It pairs with17space-systems/adcs/attitude-control-sizing for actuator sizing and18margins, space-systems/adcs/magnetorquer-control for the desaturation19actuator, and gnc-autonomy/space/attitude-dynamics for the rigid body20plant; this leaf is the control law and momentum management, not the21sizing, not the detumbling law, not the attitude determination.2223Conventions: quaternions are scalar-first (w, x, y, z) unit tuples with24the Hamilton product. The error quaternion is q_err = q_current (x)25q_ref^-1 (rotation from the reference attitude to the current attitude,26identity at the target), which equals the q_ref^-1 (x) q form under the27Shuster product convention. tau_cmd is the torque the wheel cluster28produces about the body axes, so in the closed loop demo the bus obeys29I * omega_dot = tau_cmd with the module spacecraft inertia I_SC = 0.530kg m^2 (inertially spherical, documented module constant). The wheel31momentum h_w follows the commanded torque impulse, so during the z slew32of the worked example it mirrors the z command sign.3334## Domain quick reference3536- Error quaternion: q_err = q_current (x) q_ref^-1, identity when the37 spacecraft is at the target; small angle attitude error38 theta_err ~= 2 * q_err_vec (rad).39- Rate error: omega_err = omega_body - omega_ref (omega_ref = 0 for40 inertial pointing).41- PD wheel torque command (body frame): tau_cmd = -kp * theta_err -42 kd * omega_err, kp in 1/s^2, kd in 1/s. Integral action lives in the43 wheel momentum, not in an attitude integrator.44- Wheel momentum plant: h_w_dot = tau_cmd - omega_body x h_w, where45 the cross term is the body rate transport coupling. Three orthogonal46 body axis wheels, each of inertia j_w, give wheel speeds47 omega_w_i = h_w_i / j_w.48- Torque saturation: clip each axis at +-tau_max and flag when clipped.49- Momentum saturation: flag when |h_w_i| exceeds h_max_i and report the50 excess; desaturation is then required.51- Momentum desaturation torque over horizon T_desat:52 tau_desat = -(h_w - h_target) / T_desat, h_target default 0.53- Desaturation dipole: m_desat = (B x tau_desat) / |B|^2 solves54 torque = m x B, so m_desat x B equals tau_desat for the torque55 component perpendicular to B; warn when tau_desat lies nearly along B56 (the cross product magnitude is then small and the achievable torque57 is small).58- Units are SI throughout: N m, N m s, rad, rad/s, kg m^2, s, T, A m^2.59- ECSS frames the spacecraft control context; the relations above are60 standard engineering methodology, summary-only.6162## Workflow63641. Set the pointing target: reference quaternion q_ref, reference rate65 omega_ref (default 0), and the initial attitude q and body rate66 omega from the attitude determination leaf or the mission timeline.672. Form the error with quaternion_error(q_current, q_ref) and map it to68 the small angle vector with attitude_error_vector (2 * q_err_vec).693. Command the wheel torque with pd_wheel_torque(kp, kd, theta_err,70 omega_err). Choose kp and kd from the closed loop frequency and71 damping: omega_n = sqrt(kp / I) and zeta = kd / (2 * sqrt(kp * I))72 for a bus inertia I.734. Clip the command at the wheel torque authority with74 torque_saturation(tau_cmd, tau_max) and honor the flag.755. Advance the wheel momentum with wheel_momentum_update(h_w, tau_cmd,76 omega_body, dt), then check momentum_saturation(h_w, h_max); a flag77 here means desaturation is needed.786. For acquisition, run the closed loop demo with run_wheel_control79 (omega_body_samples = None integrates the body rate from omega0);80 pass a sampled body rate profile instead to evaluate commands along81 a recorded trajectory. The demo uses the module bus inertia I_SC.827. When the wheels are saturated, compute the unload torque with83 desaturation_torque(h_w, h_target, t_desat) and the dipole with84 dipole_from_torque(tau_desat, b_field), and heed the alignment85 warning before passing the dipole to the magnetorquer leaf.868. Confirm the deterministic checks with the contract test87 scripts/test_reaction_wheel_control.py.8889## Worked example9091Spacecraft with three orthogonal wheels, j_w = 0.01 kg m^2 each, at92rest at q0 = [1, 0, 0, 0]. Target q_ref = [cos 5 deg, 0, 0, sin 5 deg]93rotates the spacecraft 10 deg about the body z axis. Gains kp = 0.05941/s^2, kd = 0.2 1/s, dt = 0.01 s, run 20 s (2000 steps), tau_max =950.1 N m, h_max = 0.5 N m s.9697- Error at start: q_err = q0 (x) q_ref^-1 gives q_err_vec =98 (0, 0, -sin 5 deg), so theta_err = (0, 0, -0.1743) rad and the first99 wheel torque command is +0.0087 N m about +z, driving the bus toward100 the target (the wheel reaction on the bus is opposite about the z101 axis). The rate term damps once the bus rotates.102- Acquisition: the bus settles with small overshoot (attitude error103 peaks near 0.76 deg after the first crossing) and the attitude error104 at 20 s is 0.11 deg, below the 1 deg acquisition tolerance.105- Wheel momentum: h_w follows the signed torque impulse integral; the106 magnitude peaks at 0.0134 N m s (wheel speed 1.34 rad/s, about 13107 rpm) and returns near zero as the braking torque cancels the108 acceleration impulse. Both saturation flags stay clear with the109 generous limits.110- Torque saturation: rerun with tau_max = 1e-4 N m; the command clips111 immediately, the flag trips, and the error at 20 s is 7.7 deg, much112 slower than the unconstrained 0.11 deg.113- Momentum saturation: rerun with h_max = 0.004 N m s; the momentum114 flag trips at the peak of the maneuver.115- Desaturation: with h_w = [0, 0, 0.2] N m s excess over h_target and116 B = [2e-5, 0, 0] T over T_desat = 100 s: tau_desat =117 -[0, 0, 0.2] / 100 = [0, 0, -0.002] N m and m_desat =118 (B x tau_desat) / |B|^2 = [0, 100, 0] A m^2; m_desat x B reproduces119 tau_desat exactly (perpendicular geometry) with no alignment warning.120121122## Pitfalls123124- Getting the error quaternion convention backwards: q_err =125 q_current (x) q_ref^-1 under the scalar-first Hamilton product; the126 sign of the first torque command must drive the bus toward the127 target (the worked example commands +z toward a +z rotation).128- Picking gains without the bus inertia: kp and kd map to omega_n and129 zeta through I (omega_n = sqrt(kp / I)); gains tuned on the wrong130 inertia oscillate or stall the acquisition.131- Dropping the transport term: wheel momentum integrates132 h_w_dot = tau_cmd - omega_body x h_w; the cross term only vanishes133 for geometry parallel to the rotation axis, so it must stay in the134 propagation for general slews.135- Ignoring the saturation flags: a clipped torque command or a wheel136 past h_max silently degrades pointing (the 1e-4 N m tau_max rerun137 ends at 7.7 deg instead of 0.11 deg) - honor both flags before138 declaring acquisition.139- Commanding desaturation along the field: when tau_desat lies nearly140 along B the m x B torque is small; heed the alignment warning before141 passing the dipole to the magnetorquer leaf.142- Feeding non-physical inputs: non-positive gains, j_w <= 0, dt <= 0,143 tau_max <= 0, h_max <= 0, t_desat <= 0, or a field at or below144 1e-12 T raise ValueError.145## Verification146147- Confirm quaternion_error returns identity when q_current = q_ref and148 unit norm otherwise; doubling kp doubles the initial torque149 magnitude; zero error and zero rate give exactly zero torque.150- Confirm the acquisition run reaches the 10 deg z target with the151 attitude error below 1 deg at 20 s (measured 0.11 deg on this152 implementation) and small overshoot.153- Confirm h_w equals the signed torque impulse integral during the z154 slew (the transport term vanishes for the parallel geometry) and155 stays bounded under generous tau_max and h_max.156- Confirm torque_saturation clips per axis and flags, and that the157 tiny tau_max rerun flags with slower settling.158- Confirm the desaturation numbers: tau_desat = -0.002 N m about z,159 m_desat = +100 A m^2 about y for B along x, m_desat x B within160 1e-12 of tau_desat, and the alignment warning trips for a torque161 demand along the field.162- Confirm ValueError rejection of non-positive gains, j_w <= 0,163 dt <= 0, h_max <= 0, tau_max <= 0, t_desat <= 0, a field at or below164 1e-12 T, and any non-finite input.165- Run the contract test offline: python3166 scripts/test_reaction_wheel_control.py (35 tests, deterministic).167168## Related leaves169170- space-systems/adcs/attitude-control-sizing: momentum wheel sizing and171 margins for a commanded maneuver; this leaf commands what that leaf172 sizes.173- space-systems/adcs/magnetorquer-control: the desaturation actuator174 and the dipole to torque link, same m x B convention.175- space-systems/adcs/attitude-determination-triad: the attitude176 reference used by the control law.177- space-systems/adcs/star-tracker: fine attitude reference for178 precision pointing.179- space-systems/adcs/sun-pointing: the safe hold pointing target.180- gnc-autonomy/space/attitude-dynamics: the rigid body plant and181 propagation that this leaf keeps simplified.182183## Behavior contract (gate 3)184185Run the deterministic contract test (stdlib unittest, offline):186187 python3 scripts/test_reaction_wheel_control.py188189The test covers the worked example acquisition (10 deg z slew reaches190the target below 1 deg error at 20 s, small overshoot), the PD191identities (zero error gives zero torque, doubling kp doubles the192initial command, the rate term opposes the rate), the quaternion error193identity at the target and its worked example vector part, wheel194momentum integration with the transport term, per axis torque clipping195and momentum excess flags, torque and momentum saturation in the run,196the momentum equals impulse integral identity, the desaturation torque197and dipole worked example with the m x B reconstruction to 1e-12 and198the alignment warning, the rate profile evaluation mode, and ValueError199rejection of every non-physical input class.200201## Compliance202203- Standards referenced, not reproduced: ECSS covers spacecraft control204 (standards-map.yaml); the control law relations above are standard205 engineering methodology, summary-only.206- compliance: STANDARDS-REF, gated: false.