Source code for skplatform.satellite.satellitekepler

from typing import Tuple, Union
import sktimeutils
from math import cos, sin, radians, sqrt, pi, fabs, pow, atan2, nan, acos, degrees, log10, floor, tan, atan
import numpy as np
from datetime import datetime, timedelta
from sktimeutils.eci import gmst
from .satellitebase import SatelliteBase
from .gibbs import gibbs
from .coe import coe_from_state_vector, ClassicalOrbitalElements


# -----------------------------------------------------------------------------
#           degrees360
# -----------------------------------------------------------------------------
def degrees360(rad_angle: float) -> float:

    degangle = degrees(rad_angle)
    if degangle < 0.0:
        degangle += 360.0
    return degangle


# -----------------------------------------------------------------------------
#           UnitVector
# -----------------------------------------------------------------------------
def UnitVector(v):
    return v / np.linalg.norm(v)


# -----------------------------------------------------------------------------
#           Magnitude
# -----------------------------------------------------------------------------
def Magnitude(v):
    return np.linalg.norm(v)


# -----------------------------------------------------------------------------
#           power_exponent
# -----------------------------------------------------------------------------
def value_exponent(x):

    if (x == 0.0):
        expon = 0
        value = 0.0
        signstr = ' '
    else:
        expon = int(floor(log10(fabs(x)) + 1.0))
        value = x / pow(10.0, expon)
        signstr = ' ' if value >= 0.0 else '-'
    return (value, expon, signstr)


# -----------------------------------------------------------------------------
#           class SatelliteKepler
# -----------------------------------------------------------------------------
[docs] class SatelliteKepler(SatelliteBase):
[docs] def __init__(self, utc: Union[np.datetime64, datetime, float, str] = None, period_from_seconds: float = None, period_from_altitude: float = None, period_from_semi_major_axis: float = None, inclination_radians: float = None, inclination_is_sun_sync: bool = False, mean_anomaly: float = 0.0, argument_of_perigee: float = 0.0, localtime_of_ascending_node_hours: float = None, longitude_of_ascending_node_degrees: float = None, right_ascension_ascending_node: float = None, eccentricity: float = 0.0000001, orbitnumber: int = 0): """ Implements a simple Kepler orbit around the Earth. Multiple options are provided to describe the required orbit. A complete description of the various parameters is given in :meth:`~.from_elements` :param utc: option identical to that described in :meth:`~.from_elements` :param period_from_seconds: keyword option identical to that described in :meth:`~.from_elements` :param period_from_altitude: keyword option identical to that described in :meth:`~.from_elements` :param period_from_semi_major_axis: keyword option identical to that described in :meth:`~.from_elements` :param inclination_radians: keyword option identical to that described in :meth:`~.from_elements` :param inclination_is_sun_sync: keyword option identical to that described in :meth:`~.from_elements` :param mean_anomaly: keyword option identical to that described in :meth:`~.from_elements` :param true_anomaly: keyword option identical to that described in :meth:`~.from_elements` :param argument_of_perigee: keyword option identical to that described in :meth:`~.from_elements` :param localtime_of_ascending_node_hours: keyword option identical to that described in :meth:`~.from_elements` :param longitude_of_ascending_node_degrees: keyword option identical to that described in :meth:`~.from_elements` :param right_ascension_ascending_node: keyword option identical to that described in :meth:`~.from_elements` :param eccentricity: keyword option identical to that described in :meth:`~.from_elements` :param orbitnumber: keyword option identical to that described in :meth:`~.from_elements` """ super().__init__() self.m_epoch: datetime = None # The time when the kepler ali_elements are defined. # self.m_mu: float = 3.98601210E14 # The gravitational parameter for Earth self.m_mu: float = 3.986004418E14 # The gravitational parameter for Earth self.m_i: float = None # inclination in radians self.m_raan: float = None # Right Ascension of ascending node in radians self. m_argument_of_perigee: float = None # Argument of perigee in radians self.m_N0: float = 0.0 # Mean motion in radians per second self.m_M0: float = 0.0 # Mean Anomaly in radians per second at the specified epoch self.m_e: float = 0.0 # eccentricity self.m_a: float = 0.0 # semi-major axis in meters self.m_b: float = 0.0 # semi minor axis in meters self.m_h: float = 0.0 # angular momentum of the orbit. This is conserved for a pure kepler orbit self.m_xunit: np.ndarray = np.zeros([3]) # Get the unit vector pointing to the perigree (i.e. same direction as eccentricy vector) self.m_yunit: np.ndarray = np.zeros([3]) # Get the unit vector pointing along minor axis of ellips, (circa 90 degrees mean anomaly). self.m_zunit: np.ndarray = np.zeros([3]) # Get the unit vector perpendicular to the plane (ie in the direction of the angular momentum. if (utc is not None) and ((period_from_seconds is not None) or (period_from_altitude is not None) or (period_from_semi_major_axis is not None)): utcdatetime = sktimeutils.ut_to_datetime(utc) self.from_elements(utcdatetime, period_from_seconds=period_from_seconds, period_from_altitude=period_from_altitude, period_from_semi_major_axis=period_from_semi_major_axis, inclination_radians=inclination_radians, inclination_is_sun_sync=inclination_is_sun_sync, mean_anomaly=mean_anomaly, argument_of_perigee=argument_of_perigee, localtime_of_ascending_node_hours=localtime_of_ascending_node_hours, longitude_of_ascending_node_degrees=longitude_of_ascending_node_degrees, right_ascension_ascending_node=right_ascension_ascending_node, eccentricity=eccentricity, orbitnumber=orbitnumber)
# -------------------------------------------------------------------------- # SatelliteKepler::orbital_period # --------------------------------------------------------------------------
[docs] def period(self) -> timedelta: """ Return the orbital period of this orbit. Uses keplers third law. Returns: datetime.timedelta The orbital period as a timedelta. Use timedelta method total_seconds() to get the period in seconds. """ return timedelta(seconds=2.0 * pi * sqrt(self.m_a * self.m_a * self.m_a / self.m_mu))
# ------------------------------------------------------------------------------ # true_anomaly_to_mean_anomaly # ------------------------------------------------------------------------------
[docs] @staticmethod def true_anomaly_to_mean_anomaly(nu: float, e: float) -> float: """ Converts True anomaly, :math:`\\nu`, to mean anomaly, :math:`M` by first calculating the eccentric anomaly, :math:`E` using the formula, .. math:: \\tan \\frac{\\nu}{2} = \\sqrt{\\frac{1+e}{1-e}}\\tan\\frac{E}{2} and then calculating the mean anomaly with .. math:: M = E -e\\,\\sin E Parameters: nu : float True anomaly in radians e : float eccentricity Returns: float Mean anomaly in radians """ factor = sqrt((1.0 - e) / (1.0 + e)) y = factor * sin(nu * 0.5) x = cos(nu * 0.5) E = 2.0 * atan2(y, x) M = E - e * sin(E) return M
# ----------------------------------------------------------------------------- # eccentricity # -----------------------------------------------------------------------------
[docs] def eccentricity(self) -> float: """ Returns the eccentricity of the orbit ( 0.0 to 1.0) """ return self.m_e
# -------------------------------------------------------------------------- # SatelliteKepler::self.eccentric_anomaly # -------------------------------------------------------------------------- def _eccentric_anomaly(self, M: float, ecc: float, eps: float) -> float: """ Calculate the eccentric anomaly by solving Kepler's equation using a Newton-Raphsom method. Iterate to a precision specified by eps which is typically 1.0E-07 to 1.0E-10 Parameters: M: float Mean Anomaly in radians ecc: float Eccentricity eps: float Precision, typically 1.0E-07 to 1.0E-10 Returns: float The eccentric anomaly in radians """ E = M # first guess (which is exact for a circle) delta = 1.0E20 while (fabs(delta) >= eps): # and see if we have converged delta = E - ecc * sin(E) - M # estimate the correction E -= delta / (1 - ecc * cos(E)) # get the corrected guess return E # ----------------------------------------------------------------------------- # sun_synchronous_inclination # -----------------------------------------------------------------------------
[docs] def sun_synchronous_inclination(self, semi_major_axis_meters: float) -> float: """ Method that returns the inclination of a sun synchronous orbit given the semi-major axis. For reference see `Sun-Synchronous orbit <https://en.wikipedia.org/wiki/Sun-synchronous_orbit>`_ Parameters: semi_major_axis_meters: float Returns: float The required inclination of the orbit in radians """ cosi = -pow(semi_major_axis_meters / 12352000.0, 3.5) # see https://en.wikipedia.org/wiki/Sun-synchronous_orbit if (cosi < -1): raise Exception('The semi major axis of the orbit must be less than 12352 km for sun sync to work') return acos(cosi)
# -------------------------------------------------------------------------- # SatelliteKepler::update_eci_position # --------------------------------------------------------------------------
[docs] def update_eci_position(self, ut: Union[np.datetime64, datetime]): """ Updates the internal ECI position to the given time Parameters: ut: datetime, np.datetime64, float, str The time at which the new position is required. """ utc = sktimeutils.ut_to_datetime(ut) if (self._m_time is None) or (utc != self._m_time): # Do we need to update eciposition or is it already the cached value dt = (utc - self.m_epoch).total_seconds() # Get the difference in seconds M = self.m_M0 + self.m_N0 * dt # Get the new mean Anomaly E = self._eccentric_anomaly(M, self.m_e, 1.0E-10) # Solve Keplers equation to a precision of 1.0E-10 radians. x = self.m_a * (cos(E) - self.m_e) # Get the x coordinate of the location y = self.m_b * sin(E) # Get the y coordinate of the location r = sqrt(x * x + y * y) # Get the radial distance k = self.m_mu / self.m_h vx = -k * y / r vy = k * (x / r + self.m_e) loc = (self.m_xunit * x + self.m_yunit * y) # get the eci-location. vel = (self.m_xunit * vx + self.m_yunit * vy) # get the eci-velocity self._set_current_state(loc, vel, utc) # set the current state
# ----------------------------------------------------------------------------- # make_two_line_elements # -----------------------------------------------------------------------------
[docs] def make_two_line_elements(self, first_derivative=0.0, second_derivative=0.0, bstar=0.0, satnum=1) -> Tuple[str, str]: """ Returns the current orbital ali_elements as two line ali_elements. This is handy if you want to initialize an SGP4 predictor from a Kepler "starter orbit" using two line ali_elements. The kepler orbit does not directly support drag terms, these have to be provided via the `bstar` parameter. Parameters: first_derivative: float The first derivative term of the SGP4 two line ali_elements. This is usually not used and can be left as the default value of 0.0 second_derivative: float The second derivative term of the SGP4 two line ali_elements. This is usually not used and can be left as the default value of 0.0 bstar: float The drag term used by SGP4. It is usually entered from empirical measurements. A value of 0.0 should disable drag terms in the SGP4 propagator satnum: int The satellite number. This is provide for convenience Returns: Tuple(str,str) Returns the two lines of the ali_elements as a two element tuple of two strings """ t = self.time t0 = datetime(t.year, 1, 1) dt = t - t0 year = (t.year) % 100 dayofyear = dt.total_seconds() / 86400.0 + 1.0 # January 1 is day 1, not day 0 revsperday = (self.m_N0 * 86400.0) / (2 * pi) orbitnum = self.orbit_number(t)[0] secondderiv_value, secondderivexponent, secsign = value_exponent(second_derivative) bstar_value, bstarexponent, bstarsign = value_exponent(bstar) line1 = "1 {:5d}U 00001A {:2d}{:12.8f}{:11.8f} {}{:0>5d}{:2d} {}{:0>5d}{:2d} 0 0X".format(satnum, year, dayofyear, first_derivative, secsign, int(fabs(secondderiv_value) * 1.0E5 + 0.5), secondderivexponent, bstarsign, int(fabs(bstar_value) * 1.0E5 + 0.5), bstarexponent) line2 = "2 {:5d} {:8.4f} {:8.4f} {:07d} {:8.4f} {:8.4f} {:11.8f}{:5d}X".format(satnum, degrees(self.m_i), degrees360(self.m_raan), int(self.m_e * 1.0E7 + 0.5), degrees360(self.m_argument_of_perigee), degrees360(self.m_M0), revsperday, orbitnum) # Note that the checksum field has not been filled out in either line # print(line1) # print(line2) return (line1, line2)
# -------------------------------------------------------------------------- # SatelliteKepler::from_elements # --------------------------------------------------------------------------
[docs] def from_elements(self, autc: datetime, period_from_seconds: float = None, period_from_altitude: float = None, period_from_semi_major_axis: float = None, inclination_radians: float = None, inclination_is_sun_sync: bool = False, mean_anomaly: float = 0.0, argument_of_perigee: float = 0.0, localtime_of_ascending_node_hours: float = None, longitude_of_ascending_node_degrees: float = None, right_ascension_ascending_node: float = None, eccentricity: float = 0.0000001, # Eccentricity thats not quite zero avoids divide by zero problems in WXTRACK orbitnumber: int = 0): """ Define the Kepler orbit from the classic 6 `Keplerian ali_elements <https://en.wikipedia.org/wiki/Orbital_elements>`_. #. period #. inclination #. eccentricity #. mean anomaly #. right ascension of ascending node #. argument of perigee Several of the orbital ali_elements can be specified in multiple ways. For example the orbital period can be defined directly with seconds or it can be defined using the altitude of the satellite. You must use only one of the options to specify a parameter otherwise exceptions will/should be raised. The caller must ensure that they explictly set the following three orbital ali_elements, #. period or altitude must be set. #. inclination must be set. #. right ascension of the ascending node must be set. Leaving the remaining three ali_elements, (i) mean anomaly, (ii) argument of perigee and (iii) eccentricity as default values produces a circular orbit with the satellite initially placed at the location of the perigee (which for a circular orbit is somewhat undefined and arbitrary!). Parameters: platform_utc: datetime.datetime The UTC time of the ali_elements period_from_seconds : float specifies the orbital period in seconds. An alternative to specify the period is with the optional parameter `period_from_altitude` or `period_from_semi_major_axis`. One, but only one, of the three methods must be used. period_from_altitude : float specifies the orbital period using the altitude of the satellite in meters. The altitude is nominal as we do not account for oblateness of the Earth etc. The radius of the Earth is internally assumed to be 6378000.0 meters. An alternative to specify the period is with the optional parameter `period_from_seconds` or `period_from_semi_major_axis`. One, but only one, of the three optional methods must be used. period_from_semi_major_axis : float specifies the orbital period using the semi_major axis of the satellite in meters. An alternative method to specify the period is with the optional parameter `period_from_seconds` or `period_from_altitude`. One, but only one, of the three methods must be used. inclination_radians : float Specifes the inclination of the orbit in radians. An alternative method to specify the inclination is with the optional parameter `inclination_is_sun_sync`. One, but only one, of the two optional methods must be used. inclination_is_sun_sync : bool Default False. If True then specify the inclination of the orbit so it is sun-synchronous. This only works for orbits below an altitude of ~5974 km,. An alternative is to specify the inclination with the optional parameter `inclination_radians`. One, but only one, of the two optional methods may be used. Note this only sets the inclination of the orbit as a Kepler orbit propagator does not have the oblate Earth model (J2) necessary to model sun synchronicity. localtime_of_ascending_node_hours: float Default: None. Specifies the nominal local time of the ascending node in hours (0-24). If set then its value represents the "hour" at which you want the ascending node for example a floating point value of 18.25 will set the node to 18:15 LT. This value is implemented by overwriting the "longitude_of_ascending_node_degrees" keyword used to initialize the kepler orbit. It also forces the satellite to be close to the ascending node at time `platform_utc` by changing the mean_anomaly value. Default is None. longitude_of_ascending_node_degrees: float Default None. Specifies the geographic longitude of the ascending node (-180 to 360)at time `platform_utc`. This is an alternative method to using parameters `localtime_of_ascending_node_hours` and `right_ascension_ascending_node` to specify the right ascension of the ascending node. One, but only one, of the 3 optional methods may be used, if neither option is used the RAAN is set to 0.0. If this option is used then the satellite is forced to be close to the ascending node at time `platform_utc` by changing the mean_anomaly value. Note that longitude is in degrees not radians. right_ascension_ascending_node: float Default None. Specifies the Right Ascension of the ascending node in radians (0 to 2.pi). This is an alternative method to parameter `longitude_of_ascending_node_degrees` to specify the right ascension of the ascending node. One, but only one, of the two optional methods must be used, if neither option is used the RAAN is set to 0.0. mean_anomaly: float The mean anomaly in radians at time mjd (0-2.pi). This value will be overidden if you use options `localtime_of_ascending_node_hours` or `longitude_of_ascending_node_degrees`. argument_of_perigee: float Default 0. The argument of perigree in radians (0 to 2.pi). eccentricity: float Default 0. The eccentricity of the orbit (0 to 1) orbitnumber: int Default 0. The orbit number at the epoch givern by `mjd` """ # ---- determine the period from user supplied period or user supplied altitude utc = sktimeutils.ut_to_datetime(autc) if period_from_seconds is not None: assert (period_from_altitude is None) and (period_from_semi_major_axis is None), "You cannot set period from more than one method" N0 = 2 * pi / period_from_seconds elif period_from_altitude is not None: assert (period_from_semi_major_axis is None), "You cannot set period from more than one method" a = 6378135.0 + period_from_altitude N0 = sqrt(self.m_mu / (a * a * a)) elif period_from_semi_major_axis is not None: a = period_from_semi_major_axis N0 = sqrt(self.m_mu / (a * a * a)) else: raise ValueError("You must set one method of setting period") self.m_N0 = N0 # Get mean motion in radians per second self.m_a = pow(self.m_mu / (self.m_N0 * self.m_N0), 1.0 / 3.0) # Get the semi # ---- determine the inclination of the orbit from user supplied inclination or request for sun-synchronous orbit if inclination_radians is not None: assert inclination_is_sun_sync is False, "You cannot set both inclination_radians and inclination_is_sun_sync" I0 = inclination_radians elif inclination_is_sun_sync: I0 = self.sun_synchronous_inclination(self.m_a) else: raise ValueError("You must set one of (i) period_from_altitude or (ii) period_from_seconds") if (localtime_of_ascending_node_hours is not None): assert longitude_of_ascending_node_degrees is None, "You cannot set both (i) localtime_of_ascending_node_hours and (ii) longitude_of_ascending_node_degrees " ut_ha = float(utc.hour) + utc.minute / 60.0 + (utc.second + utc.microsecond * 1.0E-06) / 3600.0 # hour angle of Greenwich meridian deltaha = localtime_of_ascending_node_hours - ut_ha # get the diffrence between desired hour angle of ascending node and greenwich longitude_of_ascending_node_degrees = deltaha * 15.0 if longitude_of_ascending_node_degrees is not None: assert right_ascension_ascending_node is None, "You cannot set both (i) longitude_of_ascending_node_degrees (or localtime_of_ascending_node_hours) and (ii) right_ascension_ascending_node" st = degrees(gmst(utc)) ra = (st + longitude_of_ascending_node_degrees) # Get RA at the longitude in hours (0-24) if ra >= 360.0: ra -= 360.0 # put it in the range 0-24 if ra < 0.0: ra += 360.0 RAAN = radians(ra) # and then convert to radians mean_anomaly = -argument_of_perigee # Set the mean_anomaly so the satellite is close to the ascending node at the specified time elif right_ascension_ascending_node is not None: RAAN = right_ascension_ascending_node # use the provided right ascension else: RAAN = 0.0 # Use 0 if no option is provided self.m_i = I0 M0 = mean_anomaly if (M0 < 0.0): M0 += 2.0 * pi W0 = argument_of_perigee E0 = eccentricity self.m_argument_of_perigee = argument_of_perigee self.m_raan = RAAN self.m_M0 = M0 self.m_e = E0 eciz = np.array([0.0, 0.0, 1.0]) ecix = np.array([cos(RAAN), sin(RAAN), 0.0]) # Get vector to ascending node ytemp = np.array([cos(RAAN + pi / 2.0), sin(RAAN + pi / 2.0), 0.0]) # Get vector perpendicular to ascending node i the equatorial plane eciy = ytemp * cos(I0) + eciz * sin(I0) # Get vector perpendicular to ascending node but in the orbital plane self.m_zunit = UnitVector(np.cross(ecix, eciy)) # Get vector perpendicular to the orbital plane. self.m_xunit = UnitVector(ecix * cos(W0) + eciy * sin(W0)) # Get the unit vector pointing at the perigee (Which is the semi major axis) self.m_yunit = np.cross(self.m_zunit, self.m_xunit) e2 = 1.0 - self.m_e * self.m_e self.m_b = self.m_a * sqrt(e2) self.m_h = sqrt(self.m_mu * self.m_a * e2) self._m_time = None self.m_epoch = utc self.set_orbit_number_from_last_equator_crossing(orbitnumber, self.m_epoch) self.update_eci_position(self.m_epoch)
# ------------------------------------------------------------------------------ # from_classical_orbital_elements # ------------------------------------------------------------------------------
[docs] def from_classical_orbital_elements(self, utc: Union[datetime, np.datetime64, float, str], elements: ClassicalOrbitalElements, orbitnumber: int = 0): """ Update this Kepler object so its orbit follows the values given in the Classical Orbital Elements structure. Parameters: utc: datetime, np.datetime64, float, str The time of the ali_elements elements: ClassicalOrbitalElements The structure of classical orbital ali_elements orbitnumber: int Set the orbit number to this value, default 0. """ e = elements.e nu = elements.TA M = self.true_anomaly_to_mean_anomaly(nu, e) self.from_elements(utc, period_from_semi_major_axis=elements.a, inclination_radians=elements.i, argument_of_perigee=elements.W, right_ascension_ascending_node=elements.RA, mean_anomaly=M, eccentricity=e, orbitnumber=orbitnumber)
# -------------------------------------------------------------------------- # from_state_vector # --------------------------------------------------------------------------
[docs] def from_state_vector(self, utc: Union[datetime, np.datetime64, float, str], r: np.ndarray, v: np.ndarray, orbitnumber: int = 0): """ Update the Kepler object so it follows the orbit defined by the position and velocity state vector. Parameters: utc: datetime, np.datetime64, float, str The coordinated universal time of the state vector. r:np.ndarray [3] The ECI position of the satellite in meters v:np.ndarray[3] The ECI velocity of the satellite in m/s orbitnumber: int The orbit number at the epoch, default 0 """ elements = coe_from_state_vector(r, v) self.from_classical_orbital_elements(utc, elements, orbitnumber=orbitnumber)
# -------------------------------------------------------------------------- # from_state_vector # --------------------------------------------------------------------------
[docs] def from_three_positions(self, utc2: datetime, r1: np.ndarray, r2: np.ndarray, r3: np.ndarray, orbitnumber: int = 0): """ Determine the orbital ali_elements from three position vectors andUpdate the Kepler object so it follows the calculates orbits derived from the specified state vector Parameters: utc2: datetime, np.datetime64, float, str The coordinated universal time of the second point. r1:np.ndarray [3] The fiirst ECI position of the satelite in meters r2:np.ndarray [3] ECI coordinates at epoch (in metres) eciposition of the satellite r3:np.ndarray [3] ECI coordinates at epoch (in metres) eciposition of the satellite orbitnumber: int The orbit number at the epoch """ v2 = gibbs(r1, r2, r3) self.from_state_vector(utc2, r2, v2, orbitnumber=orbitnumber)