Source code for ADCS.CONOPS.goals.vector_goal
__all__ = ["Vector_Goal"]
import numpy as np
from typing import Tuple
from .goal import Goal
from ADCS.orbits.orbital_state import Orbital_State
from ADCS.helpers.math_helpers import normalize, rot_mat, norm
[docs]
class Vector_Goal(Goal):
r"""
Abstract base class for vector-alignment pointing goals.
This class specializes :class:`~ADCS.goals.goal.Goal` for goals that
align a specified spacecraft body-frame vector with a desired
inertial-frame direction. Typical examples include aligning a
sensor boresight with a target vector expressed in the ECI frame.
Subclasses define a desired inertial direction
:math:`\mathbf{v}_{\mathrm{ref}} \in \mathbb{R}^3` as a function of
the orbital state. The reference mapping takes the form
.. math::
G_{\mathrm{vec}}(\mathcal{O}(t)) =
\left(
\mathbf{v}_{\mathrm{ref}},
\boldsymbol{\omega}_{\mathrm{ref}}
\right)
where :math:`\boldsymbol{\omega}_{\mathrm{ref}}` is typically zero
or defined by the subclass.
This class provides a common geometric error computation based on
quaternion representations of vector alignment.
Subclasses must implement :meth:`to_ref`.
See Also
--------
:class:`~ADCS.goals.goal.Goal`
:class:`~ADCS.goals.attitude_goal.Attitude_Goal`
"""
def __init__(self, boresight_name: str | None = None) -> None:
r"""
Initialize a vector-alignment goal.
This constructor stores the optional boresight name to be used
when computing the error with respect to the satellite's boresight
dictionary.
:param boresight_name:
Optional name of the boresight to use from the satellite's
boresight dictionary. If ``None``, the first available boresight
is selected.
:type boresight_name:
str | None
:return:
None
:rtype:
None
"""
super().__init__()
self.boresight_name = boresight_name
[docs]
def to_ref(self, os0: Orbital_State) -> Tuple[np.ndarray, np.ndarray]:
r"""
Generate a reference inertial direction and angular velocity.
Subclasses must implement this method to compute a desired
inertial reference vector
.. math::
\mathbf{v}_{\mathrm{ref}} \in \mathbb{R}^3
based on the current orbital state. The returned vector is
expected to be nonzero and will typically be normalized by the
downstream error computation.
:param os0:
Current orbital state.
:type os0:
Orbital_State
:return:
Reference inertial direction and reference angular velocity.
:rtype:
Tuple[numpy.ndarray, numpy.ndarray]
"""
pass
[docs]
def error(self, q: np.ndarray, body_boresight: np.ndarray, os0: Orbital_State) -> np.ndarray:
r"""
Compute the vector-alignment attitude error.
Let :math:`\mathbf{v}_{b}` be the normalized boresight vector
expressed in the spacecraft body frame, and let
:math:`\mathbf{v}_{\mathrm{ref}}` be the desired inertial
direction returned by :meth:`to_ref`.
The reference direction is transformed into the body frame using
the direction cosine matrix derived from the current attitude
quaternion :math:`\mathbf{q}`:
.. math::
\mathbf{v}_{\mathrm{ref}}^{b}
= \mathbf{R}(\mathbf{q})^{\mathsf{T}}
\mathbf{v}_{\mathrm{ref}}
A quaternion representing the shortest rotation that aligns
:math:`\mathbf{v}_{b}` with
:math:`\mathbf{v}_{\mathrm{ref}}^{b}` is then constructed.
For the nominal case, the error quaternion is
.. math::
\mathbf{q}_{\mathrm{err}} =
\frac{1}{\sqrt{2(1 + d)}}
\begin{bmatrix}
1 + d \\
\mathbf{v}_{\mathrm{ref}}^{b} \times \mathbf{v}_{b}
\end{bmatrix}
where
.. math::
d = \mathbf{v}_{b}^{\mathsf{T}}
\mathbf{v}_{\mathrm{ref}}^{b}
In the singular case :math:`d \approx -1`, corresponding to a
180-degree misalignment, an arbitrary orthogonal rotation axis
is selected.
The returned error vector is the vector part of the error
quaternion, with sign chosen to enforce the shortest rotation.
:param q:
Current spacecraft attitude quaternion.
:type q:
numpy.ndarray
:param body_boresight:
Boresight direction expressed in the spacecraft body frame.
:type body_boresight:
numpy.ndarray
:param os0:
Current orbital state.
:type os0:
Orbital_State
:return:
Vector-alignment attitude error.
:rtype:
numpy.ndarray
"""
eci_goal, _ = self.to_ref(os0)
v_bore = normalize(body_boresight)
R_b2i = rot_mat(q) # q: body -> ECI (Hamilton)
v_goal_body = normalize(R_b2i.T @ eci_goal[1:4])
dot = np.dot(v_bore, v_goal_body)
if dot < -0.9999:
# 180° case: pick any orthogonal axis
axis = np.cross(v_bore, [1.0, 0.0, 0.0])
if norm(axis) < 1e-3:
axis = np.cross(v_bore, [0.0, 1.0, 0.0])
q_err_full = np.concatenate([[0.0], normalize(axis)])
else:
# NOTE: goal × bore, not bore × goal
cross = np.cross(v_goal_body, v_bore)
q_err_full = normalize(np.concatenate([[1.0 + dot], cross]))
q_err_vec = q_err_full[1:4] * np.sign(q_err_full[0])
return q_err_vec