""" 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