Source code for irfpy.juice.attitude0

""" Draft attitude of JUICE, based on the information available in Mar 2016.

Based on the Airbus document, ``JUI-ADST-INST-TN-000122_01``.

The attitude is defined from several information (vectors) at a certain time.
Usually the vectors are obtained using SPICE.
For use case, see the folder ``160222_jdc``.

The attitude convert between the spacecraft frame (``SC``) and
a selected refrence frame (``REF``).

- ``convert_to_nsc``: A given vector (REF-frame) is converted to SC-frame vector.
- ``convert_to_ref``: A given vector (SC-frame) is converted to REF-frame vector.

.. digraph:: class_relation

    JuiceNominal -> JuiceJupiterPointing;
    JuiceNominal -> JuiceEarthPointing;
    JuiceNominal -> JuiceFarFlyby -> JuiceMoonYaw;
    JuiceNominal -> JuiceNearFlyby -> JuiceMoonNoYaw;


"""
import logging
logging.basicConfig()
_logger = logging.getLogger('juice.attitude0')
_logger.setLevel(logging.DEBUG)

import numpy as np
from mpl_toolkits.mplot3d import Axes3D

_unitvector = lambda s: s / np.sqrt((s ** 2).sum())

[docs]class JuiceNominal: """ JUICE of nominal pointing. Based on JUI-ADST-INST-TN-000122_01. JUICE spacecraft frame (SC) is defined by the nominal approach. * +Z axis points some target (nadir deck): possible target is Sun, Jupiter, and Ganymede. * +-y axis along the normal of ecliptic plane. * -x should be illuminated by the sun. """ def __init__(self): self.vel = None # In this attitude, velocity vector is not used.
[docs] def set_posvel(self, pos, vel): raise NotImplementedError('Use set_configuration method.')
[docs] def set_configuration(self, pos, ecliptic_normal, sundir, vel=None): """ Set the configuration. :param pos: Position relative to the pointing object. *-pos* is the nadir direction, namely, *-z_SC*. :param vel: Velocity, you may use *None* if you do not know. :param ecliptic_normal: Normal vector of the ecliptic plane. It will be aligned to +y_SC or -y_SC axis. :param sundir: Sun direction in order to define the y_SC alignment. The parameters (vectors) above can be obtained usually using SPICE. No automated way has been implemented. See scenario1.py for the use case. """ self.pos = np.array(pos) if not self.vel is None: self.vel = np.array(vel) self.sundir = np.array(sundir) self.ecliptic_normal = np.array(ecliptic_normal) z_ref = -_unitvector(self.pos) # z_ref is +Zsc expressed in REF-frame x_ref = _unitvector(np.cross(z_ref, self.ecliptic_normal)) y_ref = np.cross(z_ref, x_ref) # _logger.debug(x_ref, y_ref, z_ref) self.ref2sc = np.array([x_ref, y_ref, z_ref]) ### Sun dir check. Sun dir is assessed in SC frame sun_sc = self.ref2sc.dot(sundir) sun_sc_x = sun_sc[0] if sun_sc_x > 0: _logger.debug('+X plane should be cold. Flip around y.') self.yaxis_swap()
[docs] def yaxis_swap(self): """ Y axis will be swapped to avoid +X illumination. This method will be used if one wants to swap the y-axis manually, if one realizes +x is illuminated to the Sun. """ swpmat = np.array([[-1, 0, 0], [0, -1, 0], [0, 0, 1]]) self.ref2sc = swpmat.dot(self.ref2sc)
[docs] def convert_to_ref(self, nscvecs): m = self.get_matrix_nsc2ref() return m.dot(nscvecs)
[docs] def convert_to_nsc(self, refvecs): m = self.get_matrix_ref2nsc() return m.dot(refvecs)
[docs] def get_matrix_nsc2ref(self): return self.ref2sc.T.copy()
[docs] def get_matrix_ref2nsc(self): return self.ref2sc.copy()
JuiceJupiterPointing = JuiceNominal """ JUICE during jupiter pointing attitude. Alias to :class:`JuiceNominal`. """ JuiceEarthPointing = JuiceNominal """ JUICE during earth pointing attitude. Alias to :class:`JuiceNominal`. """
[docs]def get_juice_jupiter_pointing(juice_ref_jupiter, ecliptic_normal_vector_ref, sun_ref_juice, vel=None): """ JUICE's Jupiter pointing attitude. :param juice_ref_jupiter: Position relative to the pointing object presented in REF frame. *-pos* is the nadir direction, namely, *-z_SC*. :param ecliptic_normal_vector_ref: Normal vector of the ecliptic plane in REF frame. It will be aligned to +y_SC or -y_SC axis. :param sun_ref_juice: Sun direction in REF frame in order to define the y_SC alignment. :param vel: Velocity of spacecraft relative to the object in REF frame, you may use *None* if you do not know. :return: :class:`JuiceJupiterPointing` object that support conversion between SC and REF. """ juice = JuiceJupiterPointing() juice.set_configuration(juice_ref_jupiter, ecliptic_normal_vector_ref, sun_ref_juice, vel=vel) return juice
[docs]def get_juice_earth_pointing(juice_ref_earth, ecliptic_normal_vector_ref, sun_ref_juice, vel=None): """ JUICE's Earth pointing attitude. :param juice_ref_earth: Position relative to the pointing object presented in REF frame. *-pos* is the nadir direction, namely, *-z_SC*. :param ecliptic_normal_vector_ref: Normal vector of the ecliptic plane in REF frame. It will be aligned to +y_SC or -y_SC axis. :param sun_ref_juice: Sun direction in REF frame in order to define the y_SC alignment. :param vel: Velocity of spacecraft relative to the object in REF frame, you may use *None* if you do not know. :return: :class:`JuiceEarthPointing` object that support conversion between SC and REF. """ juice = JuiceEarthPointing() juice.set_configuration(juice_ref_earth, ecliptic_normal_vector_ref, sun_ref_juice, vel=vel) return juice
[docs]class JuiceFarFlyby(JuiceNominal): def __init__(self): """ Flyby attitude for -12h to -1h and +1h to +12h. - Nadir pointing (+Z axis to the Moon) - Y axis perpendicular to Sun direction. :param juice_pos_from_cabody: (3,) array in km seen from the CA body to the spacecraft. :param sundir: (3,) array in km for the Sun direction from S/C. :returns: :class:`JuiceNominal` object with attitude configuration ready. """ JuiceNominal.__init__(self)
[docs] def set_configuration(self, pos, sundir, vel=None): """ Flyby configuration definition. :param pos: Position relative to the pointing object. *-pos* is the nadir direction, namely, *-z_SC*. :param vel: Velocity, you may use *None* if you do not know. :param sundir: Sun direction in order to define the y_SC alignment. The parameters (vectors) above can be obtained usually using SPICE. No automated way has been implemented. See scenario4.py for the use case. """ self.pos = np.array(pos) if not self.vel is None: self.vel = np.array(vel) self.sundir = np.array(sundir) z_ref = -_unitvector(self.pos) # z_ref is +Zsc expressed in REF-frame y_ref = _unitvector(np.cross(z_ref, sundir)) x_ref = _unitvector(np.cross(y_ref, z_ref)) _logger.debug(np.array_str(np.array([x_ref, y_ref, z_ref]))) self.ref2sc = np.array([x_ref, y_ref, z_ref]) ### Sun dir check. Sun dir is assessed in SC frame sun_sc = self.ref2sc.dot(sundir) sun_sc_x = sun_sc[0] if sun_sc_x > 0: # +x should be dark. _logger.debug('+X plane should be cold. Flip around y.') self.yaxis_swap()
[docs]def get_juice_farflyby(juice_ref_moon, sun_ref_juice, vel=None): """ JUICE's far flyby attitude used for \pm 12 hours except for \pm 1 hour. :param juice_ref_moon: Position relative to the moon for flyby. *-pos* is the nadir direction, namely, *-z_SC*. :param sun_ref_moon: Sun direction in REF-frame; used to define the y_SC alignment. :param vel: Velocity, you may use *None* if you do not know. :return: :class:`JuiceFarFlyby` object that support conversion between SC and REF. """ juice = JuiceFarFlyby() juice.set_configuration(juice_ref_moon, sun_ref_juice, vel=vel) return juice
JuiceMoonYaw = JuiceFarFlyby """ Juice attitude during the yaw-steering. """
[docs]def get_juice_ganymede_yaw_steering(juice_ref_ganymede, sun_ref_juice, vel=None): """ JUICE's far flyby attitude used for \pm 12 hours except for \pm 1 hour. :param juice_ref_moon: Position relative to the moon for flyby. *-pos* is the nadir direction, namely, *-z_SC*. :param sun_ref_moon: Sun direction in REF-frame; used to define the y_SC alignment. :param vel: Velocity, you may use *None* if you do not know. :return: :class:`JuiceFarFlyby` object that support conversion between SC and REF. """ juice = JuiceMoonYaw() juice.set_configuration(juice_ref_ganymede, sun_ref_juice, vel=vel) return juice
[docs]class JuiceNearFlyby(JuiceNominal): def __init__(self): """Flyby attitude for -1h and +1h. - Nadir pointing (+Z axis to Moon) - Y axis is aligned with the projection of the velocity vector in the plane orthogonal to Z. .. math:: \vec{Y} = \vec{V} - \vec{Z}\cdot\vec{V}\vec{Z} :return: """ JuiceNominal.__init__(self)
[docs] def set_configuration(self, pos, vel, sundir): """ Position and velocity in a reference frame to form the axes. :param pos: Position of the JUICE with respeictive to the moon in the reference system. :param vel: Velocity of the JUICE in the reference system :param sundir: Sundirection (to be used for y-flip condition evaluation) """ self.pos = np.array(pos) self.vel = np.array(vel) z_ref = -_unitvector(self.pos) # Z_ref is +Zsc expressed in REF-frame v_ref = _unitvector(self.vel) x_ref = _unitvector(np.cross(v_ref, z_ref)) y_ref = _unitvector(np.cross(z_ref, x_ref)) _logger.debug(np.array_str(np.array([x_ref, y_ref, z_ref]))) self.ref2sc = np.array([x_ref, y_ref, z_ref]) ### Sun dir check. Sun dir is assessed in SC frame sun_sc = self.ref2sc.dot(sundir) sun_sc_x = sun_sc[0] if sun_sc_x > 0: # +x should be dark. _logger.debug('+X plane should be cold. Flip around y.') self.yaxis_swap()
[docs]def get_juice_nearflyby(juice_ref_moon, v_juice_ref_moon, sun_ref_juice): """ JUICE's near flyby attitude. Used \pm 1 hour relative to CA. :param juice_ref_moon: Position relative to the moon for flyby. *-pos* is the nadir direction, namely, *-z_SC*. :param v_juice_ref_moon: Velocity of JUICE relative to the flybying moon. You need it to calculate x and y. :param sun_ref_moon: Sun direction in REF-frame; used to define the y_SC alignment. :return: :class:`JuiceFarFlyby` object that support conversion between SC and REF. """ juice = JuiceNearFlyby() juice.set_configuration(juice_ref_moon, v_juice_ref_moon, sun_ref_juice) return juice
JuiceMoonNoYaw = JuiceNearFlyby """ Moon orbiting attitude for no yaw steering. """
[docs]def get_juice_ganymede_no_yaw_steering(juice_ref_ganymede, v_juice_ref_ganymede, sun_ref_juice): """ JUICE's Ganymede orbiting (both eliptic / orbiting) attitude. :param juice_ref_ganymede: Position relative to the moon for flyby. *-pos* is the nadir direction, namely, *-z_SC*. :param v_juice_ref_ganymede: Velocity of JUICE relative to the flybying moon. You need it to calculate x and y. :param sun_ref_moon: Sun direction in REF-frame; used to define the y_SC alignment. :return: :class:`JuiceFarFlyby` object that support conversion between SC and REF. """ juice = JuiceMoonNoYaw() juice.set_configuration(juice_ref_ganymede, v_juice_ref_ganymede, sun_ref_juice) return juice
def _axis_configuration_main(sc, ix, iy, axis=None): """ Create subplot (Axis) displaying the configurations. """ import matplotlib.pyplot as plt if axis is None: fig = plt.figure() axis = fig.add_subplot(111) pos = sc.pos vel = sc.vel ### Origin is the nadir-pointing object. axis.plot([0], [0], 'kx') ### Spacecraft position in reference frame axis.plot([pos[ix]], [pos[iy]], 'ko') axis.text(pos[ix], pos[iy], 'S/C') ### Arm length is 10% of the position armlen = np.sqrt(pos[ix] ** 2 + pos[iy] ** 2) * 0.1 ### X-nsc to be represented in ref frame xnsc = np.array([1, 0, 0]) xref = sc.convert_to_ref(xnsc) * armlen axis.plot([pos[ix], pos[ix] + xref[ix]], [pos[iy], pos[iy] + xref[iy]], 'r-') axis.text(pos[ix] + xref[ix], pos[iy] + xref[iy], 'x_nsc', color='r') ### y-nsc to be represented in ref frame ynsc = np.array([0, 1, 0]) yref = sc.convert_to_ref(ynsc) * armlen axis.plot([pos[ix], pos[ix] + yref[ix]], [pos[iy], pos[iy] + yref[iy]], 'g-') axis.text(pos[ix] + yref[ix], pos[iy] + yref[iy], 'y_nsc', color='g') ### z-nsc to be represented in ref frame znsc = np.array([0, 0, 1]) zref = sc.convert_to_ref(znsc) * armlen axis.plot([pos[ix], pos[ix] + zref[ix]], [pos[iy], pos[iy] + zref[iy]], 'b-') axis.text(pos[ix] + zref[ix], pos[iy] + zref[iy], 'z_nsc', color='b') ### Velocity vector if not sc.vel is None: vref = sc.vel vref = vref / np.sqrt((vref ** 2).sum()) * armlen * 3 # axis.arrow(pos[ix], pos[iy], vref[ix], vref[iy], head_width=0.05, head_length=armlen * 0.3, fc='m', ec='m') axis.plot(pos[ix] + [0, vref[ix]], pos[iy] + [0, vref[iy]], 'm') axis.text(pos[ix] + vref[ix], pos[iy] + vref[iy], 'V', color='m') axis.set_aspect(1) return axis
[docs]def axis_configuration_xy(sc, axis=None): """ Create a plot of the configuration :param sc: Spacecraft aattitude class, :class:`irfpy.pep.pep_attitude.NadirLookingSc` for example. :param axis: Axis class, if you have already. If *None* is given, a new figure and axis can be produced. :return: Axis class. """ ax = _axis_configuration_main(sc, 0, 1, axis=axis) ax.set_xlabel('X [ref]') ax.set_ylabel('Y [ref]') return ax
[docs]def axis_configuration_xz(sc, axis=None): ax = _axis_configuration_main(sc, 0, 2, axis=axis) ax.set_xlabel('X [ref]') ax.set_ylabel('Z [ref]') return ax
[docs]def axis_configuration_yz(sc, axis=None): ax = _axis_configuration_main(sc, 1, 2, axis=axis) ax.set_xlabel('Y [ref]') ax.set_ylabel('Z [ref]') return ax
[docs]def axis_configuration_3d(sc, rbody=1): """ Create subplot (Axis) displaying the configurations. """ import matplotlib.pyplot as plt fig = plt.figure() axis = fig.add_subplot(111, projection='3d') pos = sc.pos ### Origin is the nadir-pointing object. axis.plot([0], [0], 'kx') ### Spacecraft position in reference frame axis.plot([pos[0]], [pos[1]], [pos[2]], 'ko') axis.text(pos[0], pos[1], pos[2], 'S/C') ### Arm length is 10% of the position armlen = np.sqrt((pos ** 2).sum()) * 0.5 xnsc = np.array([1, 0, 0]) xref = sc.convert_to_ref(xnsc) * armlen ynsc = np.array([0, 1, 0]) yref = sc.convert_to_ref(ynsc) * armlen znsc = np.array([0, 0, 1]) zref = sc.convert_to_ref(znsc) * armlen axis.plot([pos[0], pos[0] + xref[0]], [pos[1], pos[1] + xref[1]], [pos[2], pos[2] + xref[2]], 'r-') axis.text(pos[0] + xref[0], pos[1] + xref[1], pos[2] + xref[2], 'x_nsc', color='r') axis.plot([pos[0], pos[0] + yref[0]], [pos[1], pos[1] + yref[1]], [pos[2], pos[2] + yref[2]], 'g-') axis.text(pos[0] + yref[0], pos[1] + yref[1], pos[2] + yref[2], 'y_nsc', color='g') axis.plot([pos[0], pos[0] + zref[0]], [pos[1], pos[1] + zref[1]], [pos[2], pos[2] + zref[2]], 'b-') axis.text(pos[0] + zref[0], pos[1] + zref[1], pos[2] + zref[2], 'z_nsc', color='b') if not sc.vel is None: vref = sc.vel vref = vref / np.sqrt((vref ** 2).sum()) * armlen * 3 axis.plot([pos[0], pos[0] + vref[0]], [pos[1], pos[1] + vref[1]], [pos[2], pos[2] + vref[2]], 'm-') axis.text(pos[0] + vref[0], pos[1] + vref[1], pos[2] + vref[2], 'V', color='m') axis.set_xlabel('X (ref)') axis.set_ylabel('Y (ref)') axis.set_zlabel('Z (ref)') u = np.linspace(0, 2 * np.pi, 100) v = np.linspace(0, np.pi, 100) x = rbody * np.outer(np.cos(u), np.sin(v)) y = rbody * np.outer(np.sin(u), np.sin(v)) z = rbody * np.outer(np.ones(np.size(u)), np.cos(v)) axis.plot_surface(x, y, z, rstride=4, cstride=4, color='k') return axis