appl121105_iotorus.observer

Observer related classes.

It could be too much, but there are three classes.

  • VirtualObserver

  • VirtualSpacecraft

  • VirtualSensor

They are usually used for calculating the fov direction considering ram velocity.

Spacecraft is located at 100000 km in x. It moves toward y direction with a speed of 30 km/s.

>>> spacecraft = VirtualSpacecraft([100000, 0., 0.], [0, 30, 0])

There is a sensor, looking toward -z, and the detecting velocity is 100 km/s

>>> sensor = VirtualSensor([0, 0, -1.5], 100.)

Note that the “absolute value” of FOV is meaningless, and inside the class, it is normalized.

>>> print(sensor.fov[2])
-1.0
>>> print(sensor.get_velocity_vector())
[  -0.   -0.  100.]

Then, you may instance them into a observer class.

>>> observer = VirtualObserver(spacecraft, sensor)

To obtained the ram-velocity corrected velocity, you can use VirtualObserver.get_observe_velocity() method.

>>> v_part = observer.get_observe_velocity()
>>> print(v_part[1])  # Vy is 30 km/s, because of ram velocity
30.0
>>> print(v_part[2])  # Vz is 100 km/s, because of particle velocity
100.0
''' Observer related classes.

It could be too much, but there are three classes.

* :class:`VirtualObserver`
* :class:`VirtualSpacecraft`
* :class:`VirtualSensor`

They are usually used for calculating the fov direction
considering ram velocity.

Spacecraft is located at 100000 km in ``x``.
It moves toward ``y`` direction with a speed of 30 km/s.

>>> spacecraft = VirtualSpacecraft([100000, 0., 0.], [0, 30, 0])

There is a sensor, looking toward ``-z``, and the detecting
velocity is 100 km/s

>>> sensor = VirtualSensor([0, 0, -1.5], 100.)

Note that the "absolute value" of FOV is meaningless, and
inside the class, it is normalized.

>>> print(sensor.fov[2])
-1.0

>>> print(sensor.get_velocity_vector())
[  -0.   -0.  100.]

Then, you may instance them into a observer class.

>>> observer = VirtualObserver(spacecraft, sensor)

To obtained the ram-velocity corrected velocity, you can use
:meth:`VirtualObserver.get_observe_velocity` method.

>>> v_part = observer.get_observe_velocity()
>>> print(v_part[1])  # Vy is 30 km/s, because of ram velocity
30.0
>>> print(v_part[2])  # Vz is 100 km/s, because of particle velocity
100.0
'''

import numpy as np

class VirtualObserver:
    ''' Virtual observer.

    Observer object.
    '''
    def __init__(self, spacecraft, sensor):
        ''' Setup the observer.

        :param spacecraft: :class:`VirtualSpacecraft` object.
        :param sensor: :class:`VirtualSensor` object.  '''
        self.spacecraft = spacecraft
        self.sensor = sensor

    def get_observe_velocity(self):
        r''' Return the velocity vector of the observing ENA in JEqS system

        :returns: np.array with (3,) shape. Velocity of ENA in JEqS system.
        '''
        v_noram = self.sensor.get_velocity_vector()
        return v_noram + self.spacecraft.vel

class VirtualSpacecraft:
    ''' Virtual spacecraft. Attributes of position, velocity, and attitude.
    '''
    def __init__(self, pos, vel, primary_axis=None, secondary_axis=None):
        ''' Set up the spacecraft.

        The unit of pos and vel should be in km and km/s.

        primary_axis and secondary_axis define the angles.
        They are normalized first so that uses do not need to normalize,
        but they should be non-zero and not be parallel.
    
        third_axis is first determined by the cross product of primary and secondary axes.
        secondary_axis is now re-defined from the cross product of third and first axes.

        primary_axis is 0 degrees in elevation toward 180 degrees.
        secondary_axis is 0 degrees in azimutha and third_axis is 90 degrees in azimuth.
        Both secondary_axis and third_axis should have 90 degrees in elevation angle.

        If they are not specified, the primary_axis is taken as -pos and secondary_axis is vel.
        Note that either secondary_axis or velocity of spacecraft must be specified.

        :param pos: Position of observer, in km. (3,) shaped.
        :param vel: Velocity of observer, in km/s. (3,) shaped.
        :keyword primary_axis: 
        '''
        self.pos = np.array(pos)
        self.vel = np.array(vel)

        self._setup_attitude(primary_axis, secondary_axis)

    def _setup_attitude(self, primary_axis, secondary_axis):
        if primary_axis == None:
            primary_axis = -np.array(self.pos)
        if secondary_axis == None:
            secondary_axis = np.array(self.vel)

        primary_axis = np.array(primary_axis)
        secondary_axis = np.array(secondary_axis)

        lp = np.sqrt((primary_axis * primary_axis).sum())
        ls = np.sqrt((secondary_axis * secondary_axis).sum())

        if lp == 0:
            raise ValueError('Primary axis should not be zero. (%g %g %g)' % (primary_axis[0], primary_axis[1], primary_axis[2]))

        if ls == 0:
            raise ValueError('Secondary axis should not be zero (if velocity of spacecraft is 0, secondary_axis should be given explicitly). (%g %g %g)' % (secondary_axis[0], secondary_axis[1], secondary_axis[2]))


        pax = primary_axis / lp
        sax = secondary_axis / ls

        tax = np.cross(pax, sax)
        tax = tax / np.sqrt((tax * tax).sum())

        sax = np.cross(tax, pax)

        self.primary_axis = pax
        self.secondary_axis = sax
        self.third_axis = tax
        
    def get_angles(self, kvector):
        ''' Return the angle corresponding to kvector in JEqS system.

        :returns: (theta, phi) in radians.

        See :meth:`get_vector` for usage.
        '''
        kvector = np.array(kvector)
        n_kvector = kvector / np.sqrt((kvector ** 2).sum())
        theta = np.arccos(n_kvector.dot(self.primary_axis))
        phi = -np.arctan2(n_kvector.dot(self.third_axis), n_kvector.dot(self.secondary_axis))
        # Minus is to rotate in a natural way (left-hand system). Just consider
        # if you are the spacecraft looking toward primary_axis, and secondary_axis to the right.
        # Natural roation (toward up) to be positive phi, the minus is needed.

        return theta, phi

    def get_vector(self, theta, phi):
        ''' Returns the vector corresponding to elevation and azimuth angle.

        :param theta: Elevation angle in radian.
        :param phi: Aximuth angle in radian.

        :returns: (3,) shaped np.array as a vector in JEqS frame corresponding to the given angles.

        Assume the spacecraft at (100, 0, 0) with velocity (0, 3, 0).
        In this case, the default primary axis is (-1, 0, 0) and
        the secondary axis (0, 1, 0).
        The third axis is derived to be (0, 0, -1).
        Theta is the angle from (-1, 0, 0).
        Phi is the angle from (0, 1, 0) toward -(0, 0, -1) [minus here!!].

        >>> sc = VirtualSpacecraft([100, 0, 0], [0, 3, 0])

        You may get the vector theta=0, phi=0. Expectation is [-1, 0, 0].

        >>> print(sc.get_vector(0, 0))
        [-1.  0.  0.]

        Reverting is also available.

        >>> print(sc.get_angles([-1., 0, 0.]))
        (0.0, -0)

        If theta = 180 deg, the vector should be [1, 0, 0].

        >>> v = sc.get_vector(np.pi, 0); print('%.2f %.2f %.2f' % (v[0], v[1], v[2]))
        1.00 0.00 0.00
        >>> t, p = np.rad2deg(sc.get_angles([1, 0, 0])); print('%.2f %.2f' % (t, p))
        180.00 -0.00

        If theta=90, x is always 0.  For azimuth=0, it returns secondary_axis, [0, 1, 0].

        >>> v = sc.get_vector(np.pi / 2., 0); print('%.2f %.2f %.2f' % (v[0], v[1], v[2]))
        -0.00 1.00 0.00
        >>> t, p = np.rad2deg(sc.get_angles([0, 1, 0])); print('%.2f %.2f' % (t, p))
        90.00 -0.00

        For azimuth 90, it returns minus of third_axis, i.e. [0, 0, 1]

        >>> v = sc.get_vector(np.pi / 2., np.pi / 2.); print('%.2f %.2f %.2f' % (v[0], v[1], v[2]))
        -0.00 0.00 1.00
        >>> t, p = np.rad2deg(sc.get_angles([0, 0, 1])); print('%.2f %.2f' % (t, p))
        90.00 90.00

        You can give specific primary and secondary axes.

        >>> sc = VirtualSpacecraft([100, 0, 0], [0, 3, 0], secondary_axis=[-100, 3, 0])
        >>> v = sc.get_vector(np.pi / 2., np.pi / 2.); print('%.2f %.2f %.2f' % (v[0], v[1], v[2]))
        -0.00 0.00 1.00
        >>> t, p = np.rad2deg(sc.get_angles([0, 0, 1])); print('%.2f %.2f' % (t, p))
        90.00 90.00
        '''
        p = np.cos(theta)
        s = np.sin(theta) * np.cos(-phi)
        t = np.sin(theta) * np.sin(-phi)

        return self.primary_axis * p + self.secondary_axis * s + self.third_axis * t
        

            

class VirtualSensor:
    ''' Virtual sensor. Attribute of FOV vector and velocity. Spacecraft coordinate system.
    '''
    def __init__(self, fov, velocity):
        ''' Set up the sensor.

        An optimal sensor that has fov vector and velocity.

        :param fov: Field of view direction, in (3,) shape.
            This will be normalized.
        :param velocity: Velocity to observe in km/s.
        '''
        fov = np.array(fov)
        lenfov = np.sqrt((fov * fov).sum())
        self.fov = fov / lenfov
        self.velocity = velocity

    def get_velocity_vector(self):
        ''' Return a velocity vector that sensor detects in S/C frame.

        :param velocity: Scalar. Positive (usually). Unit in km/s.
        :returns: Velocity vector, (3,) shape.
            ``-velocity X fov``.
        '''

        return -self.velocity * self.fov
        

import unittest
import doctest


def doctests():
    return unittest.TestSuite((
        doctest.DocTestSuite(),
        ))
if __name__ == '__main__':
    unittest.main(defaultTest='doctests')