orbit-predictor-1.9.3/0000775000175000017500000000000013473027405015461 5ustar juanlujuanlu00000000000000orbit-predictor-1.9.3/PKG-INFO0000664000175000017500000001121013473027405016551 0ustar juanlujuanlu00000000000000Metadata-Version: 2.1 Name: orbit-predictor Version: 1.9.3 Summary: Python library to propagate satellite orbits. Home-page: https://github.com/satellogic/orbit-predictor Author: Satellogic SA Author-email: oss@satellogic.com License: MIT Description: Orbit Predictor =============== .. image:: https://travis-ci.org/satellogic/orbit-predictor.svg?branch=master :target: https://travis-ci.org/satellogic/orbit-predictor .. image:: https://coveralls.io/repos/github/satellogic/orbit-predictor/badge.svg?branch=master :target: https://coveralls.io/github/satellogic/orbit-predictor?branch=master Orbit Predictor is a Python library to propagate orbits of Earth-orbiting objects (satellites, ISS, Santa Claus, etc) using `TLE (Two-Line Elements set) `_ Al the hard work is done by Brandon Rhodes implementation of `SGP4 `_. We can say *Orbit predictor* is kind of a "wrapper" for the python implementation of SGP4 To install it ------------- You can install orbit-predictor from pypi:: pip install orbit-predictor Use example ----------- When will be the ISS over Argentina? :: In [1]: from orbit_predictor.sources import EtcTLESource In [2]: from orbit_predictor.locations import ARG In [3]: source = EtcTLESource(filename="examples/iss.tle") In [4]: predictor = source.get_predictor("ISS") In [5]: predictor.get_next_pass(ARG) Out[5]: In [6]: predicted_pass = _ In [7]: position = predictor.get_position(predicted_pass.aos) In [8]: ARG.is_visible(position) # Can I see the ISS from this location? Out[8]: True In [9]: import datetime In [10]: position_delta = predictor.get_position(predicted_pass.los + datetime.timedelta(minutes=20)) In [11]: ARG.is_visible(position_delta) Out[11]: False In [12]: tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1) In [13]: predictor.get_next_pass(ARG, tomorrow, max_elevation_gt=20) Out[13]: Simplified creation of predictor from TLE lines: :: In [1]: import datetime In [2]: from orbit_predictor.sources import get_predictor_from_tle_lines In [3]: TLE_LINES = ( "1 43204U 18015K 18339.11168986 .00000941 00000-0 42148-4 0 9999", "2 43204 97.3719 104.7825 0016180 271.1347 174.4597 15.23621941 46156") In [4]: predictor = get_predictor_from_tle_lines(TLE_LINES) In [5]: predictor.get_position(datetime.datetime(2019, 1, 1)) Out[5]: Position(when_utc=datetime.datetime(2019, 1, 1, 0, 0), position_ecef=(-5280.795613274576, -3977.487633239489, -2061.43227648734), velocity_ecef=(-2.4601788971676903, -0.47182217472755117, 7.167517631852518), error_estimate=None) Currently you have available these sources ------------------------------------------ - Memorytlesource: in memory storage. - EtcTLESource: a uniq TLE is stored in `/etc/latest_tle` - WSTLESource: It reads a REST API currently used inside Satellogic. We are are working to make it publicly available. How to contribute ----------------- - Write pep8 complaint code. - Wrap the code on 100 collumns. - Always use a branch for each feature and Merge Proposals. - Always run the tests before to push. (test implies pep8 validation) Keywords: orbit,sgp4,TLE,space,satellites Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.4 Provides-Extra: dev Provides-Extra: fast orbit-predictor-1.9.3/README.rst0000664000175000017500000000622013473027330017145 0ustar juanlujuanlu00000000000000Orbit Predictor =============== .. image:: https://travis-ci.org/satellogic/orbit-predictor.svg?branch=master :target: https://travis-ci.org/satellogic/orbit-predictor .. image:: https://coveralls.io/repos/github/satellogic/orbit-predictor/badge.svg?branch=master :target: https://coveralls.io/github/satellogic/orbit-predictor?branch=master Orbit Predictor is a Python library to propagate orbits of Earth-orbiting objects (satellites, ISS, Santa Claus, etc) using `TLE (Two-Line Elements set) `_ Al the hard work is done by Brandon Rhodes implementation of `SGP4 `_. We can say *Orbit predictor* is kind of a "wrapper" for the python implementation of SGP4 To install it ------------- You can install orbit-predictor from pypi:: pip install orbit-predictor Use example ----------- When will be the ISS over Argentina? :: In [1]: from orbit_predictor.sources import EtcTLESource In [2]: from orbit_predictor.locations import ARG In [3]: source = EtcTLESource(filename="examples/iss.tle") In [4]: predictor = source.get_predictor("ISS") In [5]: predictor.get_next_pass(ARG) Out[5]: In [6]: predicted_pass = _ In [7]: position = predictor.get_position(predicted_pass.aos) In [8]: ARG.is_visible(position) # Can I see the ISS from this location? Out[8]: True In [9]: import datetime In [10]: position_delta = predictor.get_position(predicted_pass.los + datetime.timedelta(minutes=20)) In [11]: ARG.is_visible(position_delta) Out[11]: False In [12]: tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1) In [13]: predictor.get_next_pass(ARG, tomorrow, max_elevation_gt=20) Out[13]: Simplified creation of predictor from TLE lines: :: In [1]: import datetime In [2]: from orbit_predictor.sources import get_predictor_from_tle_lines In [3]: TLE_LINES = ( "1 43204U 18015K 18339.11168986 .00000941 00000-0 42148-4 0 9999", "2 43204 97.3719 104.7825 0016180 271.1347 174.4597 15.23621941 46156") In [4]: predictor = get_predictor_from_tle_lines(TLE_LINES) In [5]: predictor.get_position(datetime.datetime(2019, 1, 1)) Out[5]: Position(when_utc=datetime.datetime(2019, 1, 1, 0, 0), position_ecef=(-5280.795613274576, -3977.487633239489, -2061.43227648734), velocity_ecef=(-2.4601788971676903, -0.47182217472755117, 7.167517631852518), error_estimate=None) Currently you have available these sources ------------------------------------------ - Memorytlesource: in memory storage. - EtcTLESource: a uniq TLE is stored in `/etc/latest_tle` - WSTLESource: It reads a REST API currently used inside Satellogic. We are are working to make it publicly available. How to contribute ----------------- - Write pep8 complaint code. - Wrap the code on 100 collumns. - Always use a branch for each feature and Merge Proposals. - Always run the tests before to push. (test implies pep8 validation) orbit-predictor-1.9.3/orbit_predictor/0000775000175000017500000000000013473027405020653 5ustar juanlujuanlu00000000000000orbit-predictor-1.9.3/orbit_predictor/__init__.py0000664000175000017500000000000013473027330022747 0ustar juanlujuanlu00000000000000orbit-predictor-1.9.3/orbit_predictor/accuratepredictor.py0000664000175000017500000000055013473027330024725 0ustar juanlujuanlu00000000000000# For backwards compatibility import warnings from .predictors.base import ONE_SECOND from .predictors.accurate import HighAccuracyTLEPredictor warnings.warn( "Use `from orbit_predictor.predictors import TLEPredictor` instead, " "this module will be removed in the future", FutureWarning, ) __all__ = ["HighAccuracyTLEPredictor", "ONE_SECOND"] orbit-predictor-1.9.3/orbit_predictor/angles.py0000664000175000017500000000735113473027330022501 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Inspired by https://github.com/poliastro/poliastro/blob/86f971c/src/poliastro/twobody/angles.py # Copyright (c) 2012-2017 Juan Luis Cano Rodríguez, MIT license """Angles and anomalies. """ from math import sin, cos, tan, atan, sqrt from orbit_predictor.utils import njit @njit def _kepler_equation(E, M, ecc): return E - ecc * sin(E) - M @njit def _kepler_equation_prime(E, _, ecc): return 1 - ecc * cos(E) @njit def ta_to_E(ta, ecc): """Eccentric anomaly from true anomaly. Parameters ---------- ta : float True anomaly (rad). ecc : float Eccentricity. Returns ------- E : float Eccentric anomaly. """ E = 2 * atan(sqrt((1 - ecc) / (1 + ecc)) * tan(ta / 2)) return E @njit def E_to_ta(E, ecc): """True anomaly from eccentric anomaly. Parameters ---------- E : float Eccentric anomaly (rad). ecc : float Eccentricity. Returns ------- ta : float True anomaly (rad). """ ta = 2 * atan(sqrt((1 + ecc) / (1 - ecc)) * tan(E / 2)) return ta @njit def M_to_E(M, ecc): """Eccentric anomaly from mean anomaly. Parameters ---------- M : float Mean anomaly (rad). ecc : float Eccentricity. Returns ------- E : float Eccentric anomaly. Note ----- Algorithm taken from Vallado 2007, pp. 73. """ E = M while True: E_new = E + (M - E + ecc * sin(E)) / (1 - ecc * cos(E)) if (E_new == E) or (abs((E_new - E) / E) < 1e-15): break else: E = E_new return E_new @njit def E_to_M(E, ecc): """Mean anomaly from eccentric anomaly. Parameters ---------- E : float Eccentric anomaly (rad). ecc : float Eccentricity. Returns ------- M : float Mean anomaly (rad). """ M = _kepler_equation(E, 0.0, ecc) return M @njit def M_to_ta(M, ecc): """True anomaly from mean anomaly. Parameters ---------- M : float Mean anomaly (rad). ecc : float Eccentricity. Returns ------- ta : float True anomaly (rad). Examples -------- >>> ta = M_to_ta(radians(30.0), 0.06) >>> rad2deg(ta) 33.673284930211658 """ E = M_to_E(M, ecc) ta = E_to_ta(E, ecc) return ta @njit def ta_to_M(ta, ecc): """Mean anomaly from true anomaly. Parameters ---------- ta : float True anomaly (rad). ecc : float Eccentricity. Returns ------- M : float Mean anomaly (rad). """ E = ta_to_E(ta, ecc) M = E_to_M(E, ecc) return M orbit-predictor-1.9.3/orbit_predictor/coordinate_systems.py0000664000175000017500000001156313473027330025146 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from math import asin, atan, atan2, cos, degrees, pi, radians, sin, sqrt def llh_to_ecef(lat_deg, lon_deg, h_km): """ Latitude is geodetic, height is above ellipsoid. Output is in km. Formula from http://mathforum.org/library/drmath/view/51832.html """ f = 1 / 298.257224 a = 6378.137 lat_rad = radians(lat_deg) lon_rad = radians(lon_deg) cos_lat = cos(lat_rad) sin_lat = sin(lat_rad) C = 1 / sqrt(cos_lat ** 2 + (1 - f)**2 * sin_lat ** 2) S = (1 - f) ** 2 * C k1 = a * C + h_km return (k1 * cos_lat * cos(lon_rad), k1 * cos_lat * sin(lon_rad), (a * S + h_km) * sin_lat) # TODO: Same transformation as llh_to_ecef def geodetic_to_ecef(lat, lon, height_km): a = 6378.137 b = 6356.7523142 f = (a - b) / a e2 = ((2 * f) - (f * f)) normal = a / sqrt(1. - (e2 * (sin(lat) * sin(lat)))) x = (normal + height_km) * cos(lat) * cos(lon) y = (normal + height_km) * cos(lat) * sin(lon) z = ((normal * (1. - e2)) + height_km) * sin(lat) return x, y, z def ecef_to_llh(ecef_km): # WGS-84 ellipsoid parameters */ a = 6378.1370 b = 6356.752314 p = sqrt(ecef_km[0] ** 2 + ecef_km[1] ** 2) thet = atan(ecef_km[2] * a / (p * b)) esq = 1.0 - (b / a) ** 2 epsq = (a / b) ** 2 - 1.0 lat = atan((ecef_km[2] + epsq * b * sin(thet) ** 3) / (p - esq * a * cos(thet) ** 3)) lon = atan2(ecef_km[1], ecef_km[0]) n = a * a / sqrt(a * a * cos(lat) ** 2 + b ** 2 * sin(lat) ** 2) h = p / cos(lat) - n lat = degrees(lat) lon = degrees(lon) return lat, lon, h def eci_to_ecef(eci_coords, gmst): # ccar.colorado.edu/ASEN5070/handouts/coordsys.doc # # [X] [C -S 0][X] # [Y] = [S C 0][Y] # [Z]eci [0 0 1][Z]ecef # # # Inverse: # [X] [C S 0][X] # [Y] = [-S C 0][Y] # [Z]ecef [0 0 1][Z]e sin_gmst = sin(gmst) cos_gmst = cos(gmst) eci_x, eci_y, eci_z = eci_coords x = (eci_x * cos_gmst) + (eci_y * sin_gmst) y = (eci_x * (-sin_gmst)) + (eci_y * cos_gmst) z = eci_z return x, y, z def ecef_to_eci(eci_coords, gmst): # ccar.colorado.edu/ASEN5070/handouts/coordsys.doc # # [X] [C -S 0][X] # [Y] = [S C 0][Y] # [Z]eci [0 0 1][Z]ecef # # # Inverse: # [X] [C S 0][X] # [Y] = [-S C 0][Y] # [Z]ecef [0 0 1][Z]e x = (eci_coords[0] * cos(gmst)) - (eci_coords[1] * sin(gmst)) y = (eci_coords[0] * (sin(gmst))) + (eci_coords[1] * cos(gmst)) z = eci_coords[2] return x, y, z def horizon_to_az_elev(top_s, top_e, top_z): range_sat = sqrt((top_s * top_s) + (top_e * top_e) + (top_z * top_z)) elevation = asin(top_z / range_sat) azimuth = atan2(-top_e, top_s) + pi return azimuth, elevation def to_horizon(observer_pos_lat_rad, observer_pos_long_rad, observer_pos_ecef, object_coords_ecef): # http://www.celestrak.com/columns/v02n02/ # TS Kelso's method, except I'm using ECF frame # and he uses ECI. rx = object_coords_ecef[0] - observer_pos_ecef[0] ry = object_coords_ecef[1] - observer_pos_ecef[1] rz = object_coords_ecef[2] - observer_pos_ecef[2] sin_observer_lat = sin(observer_pos_lat_rad) sin_observer_long = sin(observer_pos_long_rad) cos_observer_lat = cos(observer_pos_lat_rad) cos_observer_long = cos(observer_pos_long_rad) top_s = ((sin_observer_lat * cos_observer_long * rx) + (sin_observer_lat * sin_observer_long * ry) - (cos_observer_lat * rz)) top_e = -sin_observer_long * rx + cos_observer_long * ry top_z = ((cos_observer_lat * cos_observer_long * rx) + (cos_observer_lat * sin_observer_long * ry) + (sin_observer_lat * rz)) return top_s, top_e, top_z def deg_to_dms(deg): d = int(deg) md = abs(deg - d) * 60 m = int(md) sd = (md - m) * 60 return [d, m, sd] orbit-predictor-1.9.3/orbit_predictor/exceptions.py0000664000175000017500000000246413473027330023411 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Exceptions for orbit predictor """ class NotReachable(Exception): """Raised when a pass is not reachable on a time window""" class PropagationError(Exception): """Raised when a calculation issue is found""" orbit-predictor-1.9.3/orbit_predictor/keplerian.py0000664000175000017500000001031713473027330023176 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Inspired by # https://github.com/poliastro/poliastro/blob/1d2f3ca/src/poliastro/twobody/classical.py # https://github.com/poliastro/poliastro/blob/1d2f3ca/src/poliastro/twobody/rv.py # Copyright (c) 2012-2017 Juan Luis Cano Rodríguez, MIT license from math import cos, sin, sqrt import numpy as np from numpy.linalg import norm from orbit_predictor.utils import transform, njit, cross @njit def rv_pqw(k, p, ecc, nu): """Returns r and v vectors in perifocal frame. """ position_pqw = np.array([cos(nu), sin(nu), 0]) * p / (1 + ecc * cos(nu)) velocity_pqw = np.array([-sin(nu), (ecc + cos(nu)), 0]) * sqrt(k / p) return position_pqw, velocity_pqw @njit def coe2rv(k, p, ecc, inc, raan, argp, ta): """Converts from classical orbital elements to vectors. Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2). p : float Semi-latus rectum or parameter (km). ecc : float Eccentricity. inc : float Inclination (rad). raan : float Longitude of ascending node (rad). argp : float Argument of perigee (rad). ta : float True anomaly (rad). """ position_pqw, velocity_pqw = rv_pqw(k, p, ecc, ta) position_eci = transform(position_pqw, 2, -argp) position_eci = transform(position_eci, 0, -inc) position_eci = transform(position_eci, 2, -raan) velocity_eci = transform(velocity_pqw, 2, -argp) velocity_eci = transform(velocity_eci, 0, -inc) velocity_eci = transform(velocity_eci, 2, -raan) return position_eci, velocity_eci @njit def rv2coe(k, r, v, tol=1e-8): """Converts from vectors to classical orbital elements. Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2). r : ndarray Position vector (km). v : ndarray Velocity vector (km / s). tol : float, optional Tolerance for eccentricity and inclination checks, default to 1e-8. """ h = cross(r, v) n = cross([0, 0, 1], h) / norm(h) e = ((np.dot(v, v) - k / (norm(r))) * r - np.dot(r, v) * v) / k ecc = norm(e) p = np.dot(h, h) / k inc = np.arccos(h[2] / norm(h)) circular = ecc < tol equatorial = abs(inc) < tol if equatorial and not circular: raan = 0 argp = np.arctan2(e[1], e[0]) % (2 * np.pi) # Longitude of periapsis ta = (np.arctan2(np.dot(h, cross(e, r)) / norm(h), np.dot(r, e)) % (2 * np.pi)) elif not equatorial and circular: raan = np.arctan2(n[1], n[0]) % (2 * np.pi) argp = 0 # Argument of latitude ta = (np.arctan2(np.dot(r, cross(h, n)) / norm(h), np.dot(r, n)) % (2 * np.pi)) elif equatorial and circular: raan = 0 argp = 0 ta = np.arctan2(r[1], r[0]) % (2 * np.pi) # True longitude else: raan = np.arctan2(n[1], n[0]) % (2 * np.pi) argp = (np.arctan2(np.dot(e, cross(h, n)) / norm(h), np.dot(e, n)) % (2 * np.pi)) ta = (np.arctan2(np.dot(r, cross(h, e)) / norm(h), np.dot(r, e)) % (2 * np.pi)) return p, ecc, inc, raan, argp, ta orbit-predictor-1.9.3/orbit_predictor/locations.py0000664000175000017500000003215613473027330023224 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import datetime as dt import importlib from math import asin, cos, degrees, radians, sin, sqrt import os from orbit_predictor import coordinate_systems from orbit_predictor.utils import reify, sun_azimuth_elevation LIGHT_SPEED_KMS = 299792.458 class Location: def __init__(self, name, latitude_deg, longitude_deg, elevation_m): self.name = name self.latitude_deg = latitude_deg self.longitude_deg = longitude_deg self.elevation_m = elevation_m self.position_ecef = coordinate_systems.geodetic_to_ecef( radians(latitude_deg), radians(longitude_deg), elevation_m / 1000.) self.position_llh = latitude_deg, longitude_deg, elevation_m def __eq__(self, other): return all([issubclass(other.__class__, Location), self.name == other.name, self.latitude_deg == other.latitude_deg, self.longitude_deg == other.longitude_deg, self.elevation_m == other.elevation_m]) def __repr__(self): return "".format(self.name) def __str__(self): return self.name @reify def latitude_rad(self): return radians(self.latitude_deg) @reify def longitude_rad(self): return radians(self.longitude_deg) @reify def _cached_elevation_calculation_data(self): sin_lat, sin_long = sin(self.latitude_rad), sin(self.longitude_rad) cos_lat, cos_long = cos(self.latitude_rad), cos(self.longitude_rad) return (cos_lat * cos_long, cos_lat * sin_long, sin_lat) def sun_elevation_on_earth(self, when_utc=None): """Return Sun elevation on Earth of location at when_utc.""" if when_utc is None: when_utc = dt.datetime.utcnow() _, elevation = sun_azimuth_elevation(self.latitude_deg, self.longitude_deg, when_utc) return elevation def elevation_for(self, position): """Returns elevation to given position in radians calculation is made inline to have better performance """ observer_pos_ecef = self.position_ecef object_coords_ecef = position rx = object_coords_ecef[0] - observer_pos_ecef[0] ry = object_coords_ecef[1] - observer_pos_ecef[1] rz = object_coords_ecef[2] - observer_pos_ecef[2] a, b, c = self._cached_elevation_calculation_data top_z = (a * rx) + (b * ry) + (c * rz) range_sat = sqrt((rx * rx) + (ry * ry) + (rz * rz)) return asin(top_z / range_sat) def get_azimuth_elev(self, position): """Return azimuth and elevation of position_ecef from the current Location instance.""" top = coordinate_systems.to_horizon(self.latitude_rad, self.longitude_rad, self.position_ecef, position.position_ecef) return coordinate_systems.horizon_to_az_elev(*top) def get_azimuth_elev_deg(self, position): """Idem that get_azimuth_elev() but using degrees.""" az, el = self.get_azimuth_elev(position) return degrees(az), degrees(el) def is_visible(self, position, elevation=0): """Return True if the Satellite if visible from the current instance.""" _, elev_deg = self.get_azimuth_elev_deg(position) return elev_deg >= elevation def slant_range_km(self, position_ecef): """Distance to the satellite in straight line""" pos = position_ecef loc = self.position_ecef return sqrt((pos[0]-loc[0])**2 + (pos[1]-loc[1])**2 + (pos[2]-loc[2])**2) def slant_range_velocity_kms(self, position): """Velocity the satellite from location's point of view""" pos = position.position_ecef vel = position.velocity_ecef current_range = self.slant_range_km(pos) next_pos = (pos[0]+vel[0], pos[1]+vel[1], pos[2]+vel[2]) next_range = self.slant_range_km(next_pos) return next_range - current_range def doppler_factor(self, position): """Doppler effect factor relative to 1""" range_rate = self.slant_range_velocity_kms(position) return 1. + (range_rate / LIGHT_SPEED_KMS) # A hardcoded list of locations. Some of them are satellite groundstations or HAM AFRICA1 = Location( "AFRICA1", latitude_deg=-4.2937711, longitude_deg=15.4493049, elevation_m=266.00) AFRICA2 = Location( "AFRICA2", latitude_deg=-19.9243839, longitude_deg=23.439418, elevation_m=939.12) AFRICA3 = Location( "AFRICA3", latitude_deg=-26.0317764, longitude_deg=28.254681, elevation_m=1617.62) AFRICA4 = Location( "AFRICA4", latitude_deg=0.3979327, longitude_deg=32.5021788, elevation_m=1165.38) AFRICA5 = Location( "AFRICA5", latitude_deg=-1.2960418, longitude_deg=36.9340893, elevation_m=1599.74) AMERICA1 = Location( "AMERICA1", latitude_deg=40.6599903, longitude_deg=-74.1713736, elevation_m=10.46) AMERICA10 = Location( "AMERICA10", latitude_deg=34.0863943, longitude_deg=-118.0329261, elevation_m=88.67) AMERICA11 = Location( "AMERICA11", latitude_deg=37.9916467, longitude_deg=-122.0559013, elevation_m=6.53) AMERICA2 = Location( "AMERICA2", latitude_deg=38.9054965, longitude_deg=-77.0230685, elevation_m=25.25) AMERICA3 = Location( "AMERICA3", latitude_deg=33.7800684, longitude_deg=-84.5208486, elevation_m=245.74) AMERICA4 = Location( "AMERICA4", latitude_deg=29.9414947, longitude_deg=-90.0633866, elevation_m=3.64) AMERICA5 = Location( "AMERICA5", latitude_deg=29.9865571, longitude_deg=-95.3423456, elevation_m=29.35) AMERICA6 = Location( "AMERICA6", latitude_deg=19.4361691, longitude_deg=-99.0719249, elevation_m=2224.95) AMERICA7 = Location( "AMERICA7", latitude_deg=20.5216683, longitude_deg=-103.310728, elevation_m=1530.03) AMERICA8 = Location( "AMERICA8", latitude_deg=35.0401972, longitude_deg=-106.6090026, elevation_m=1619.52) AMERICA9 = Location( "AMERICA9", latitude_deg=33.6928889, longitude_deg=-112.078808, elevation_m=450.69) ARG = Location("ARG", latitude_deg=-31.2884, longitude_deg=-64.2032868, elevation_m=492.96) ASIA1 = Location("ASIA1", latitude_deg=32.0092853, longitude_deg=34.8945777, elevation_m=38.03) ASIA10 = Location("ASIA10", latitude_deg=23.8450823, longitude_deg=90.4016501, elevation_m=11.71) ASIA11 = Location("ASIA11", latitude_deg=16.9069935, longitude_deg=96.1342117, elevation_m=24.81) ASIA12 = Location("ASIA12", latitude_deg=13.9125333, longitude_deg=100.6068365, elevation_m=6.22) ASIA13 = Location("ASIA13", latitude_deg=21.2137962, longitude_deg=105.805638, elevation_m=12.45) ASIA14 = Location("ASIA14", latitude_deg=23.3924882, longitude_deg=113.2990193, elevation_m=5.97) ASIA15 = Location("ASIA15", latitude_deg=31.7392443, longitude_deg=118.8733768, elevation_m=13.34) ASIA16 = Location("ASIA16", latitude_deg=37.5602464, longitude_deg=126.7909134, elevation_m=20.35) ASIA17 = Location("ASIA17", latitude_deg=33.5846715, longitude_deg=130.4510901, elevation_m=5.67) ASIA18 = Location("ASIA18", latitude_deg=34.7854932, longitude_deg=135.4384313, elevation_m=10.01) ASIA19 = Location("ASIA19", latitude_deg=35.7120096, longitude_deg=139.4033569, elevation_m=91.00) ASIA2 = Location("ASIA2", latitude_deg=24.5536664, longitude_deg=39.7053953, elevation_m=638.85) ASIA3 = Location("ASIA3", latitude_deg=33.2621459, longitude_deg=44.234124, elevation_m=36.05) ASIA4 = Location("ASIA4", latitude_deg=35.6883245, longitude_deg=51.3143664, elevation_m=1185.27) ASIA5 = Location("ASIA5", latitude_deg=36.2320987, longitude_deg=59.6430435, elevation_m=996.20) ASIA6 = Location("ASIA6", latitude_deg=31.628871, longitude_deg=65.7371749, elevation_m=1014.35) ASIA7 = Location("ASIA7", latitude_deg=33.6187486, longitude_deg=73.0960301, elevation_m=502.00) ASIA8 = Location("ASIA8", latitude_deg=28.5543983, longitude_deg=77.086455, elevation_m=226.08) ASIA9 = Location("ASIA9", latitude_deg=12.950055, longitude_deg=77.66856, elevation_m=889.07) AUSTRALIA1 = Location( "AUSTRALIA1", latitude_deg=-31.9170947, longitude_deg=115.970206, elevation_m=12.73) AUSTRALIA2 = Location( "AUSTRALIA2", latitude_deg=-17.8184141, longitude_deg=122.2364966, elevation_m=28.67) AUSTRALIA5 = Location( "AUSTRALIA5", latitude_deg=-34.7794086, longitude_deg=138.6370729, elevation_m=22.43) AUSTRALIA6 = Location( "AUSTRALIA6", latitude_deg=-36.7103328, longitude_deg=144.3303179, elevation_m=197.96) AUSTRALIA7 = Location( "AUSTRALIA7", latitude_deg=-34.0096484, longitude_deg=150.6926073, elevation_m=74.77) BA1 = Location("BA1", latitude_deg=-34.5561944, longitude_deg=-58.41368, elevation_m=7.02) CHILE = Location("CHILE", latitude_deg=-33.3631552, longitude_deg=-70.7904123, elevation_m=477.00) EASTER_ISLAND = Location( "EASTER_ISLAND", latitude_deg=-27.0578009, longitude_deg=-109.3817317, elevation_m=61.69) EUROPA1 = Location("EUROPA1", latitude_deg=41.2486859, longitude_deg=-8.6813677, elevation_m=56.44) EUROPA10 = Location("EUROPA10", latitude_deg=45.7274069, longitude_deg=65.37, elevation_m=122.98) EUROPA11 = Location( "EUROPA11", latitude_deg=45.4452575, longitude_deg=9.2767394, elevation_m=106.02) EUROPA12 = Location( "EUROPA12", latitude_deg=48.3534778, longitude_deg=11.7864782, elevation_m=447.39) EUROPA13 = Location( "EUROPA13", latitude_deg=42.4310529, longitude_deg=14.1828016, elevation_m=9.81) EUROPA14 = Location( "EUROPA14", latitude_deg=41.1150865, longitude_deg=16.8624173, elevation_m=13.89) EUROPA15 = Location("EUROPA15", latitude_deg=37.9364517, longitude_deg=23.94452, elevation_m=80.00) EUROPA16 = Location( "EUROPA16", latitude_deg=38.2925088, longitude_deg=27.1556125, elevation_m=119.68) EUROPA17 = Location( "EUROPA17", latitude_deg=35.1544144, longitude_deg=33.3585865, elevation_m=172.25) EUROPA3 = Location("EUROPA3", latitude_deg=37.4189722, longitude_deg=-5.8929429, elevation_m=27.52) EUROPA5 = Location( "EUROPA5", latitude_deg=40.4915238, longitude_deg=-3.5677712, elevation_m=597.39) EUROPA7 = Location("EUROPA7", latitude_deg=39.4892396, longitude_deg=-0.4819177, elevation_m=60.64) EUROPA9 = Location("EUROPA9", latitude_deg=49.0067717, longitude_deg=2.5529958, elevation_m=102.37) MADAGASCAR1 = Location( "MADAGASCAR1", latitude_deg=15.4967687, longitude_deg=44.2171958, elevation_m=2186.00) MADAGASCAR2 = Location( "MADAGASCAR2", latitude_deg=-18.7825536, longitude_deg=47.4800904, elevation_m=1260.62) NZ1 = Location("NZ1", latitude_deg=-44.7149065, longitude_deg=169.2468643, elevation_m=339.58) NZ2 = Location("NZ2", latitude_deg=-36.5886632, longitude_deg=174.8717244, elevation_m=0.00) RIO = Location("RIO", latitude_deg=-22.910590, longitude_deg=-43.188958, elevation_m=16.92) USA = Location("USA", latitude_deg=40.24, longitude_deg=-101.9, elevation_m=1100) australia = Location('australia', latitude_deg=-25.1, longitude_deg=134.5, elevation_m=290) brazil = Location("brazil", latitude_deg=-11.2, longitude_deg=-54.66, elevation_m=310) blq_leafline = Location('blq_leafline', latitude_deg=45.59, longitude_deg=9.361, elevation_m=194) central_america = Location( "central_america", latitude_deg=11.17, longitude_deg=-87.23, elevation_m=310) central_argentina = Location( 'central_argentina', latitude_deg=-35.75, longitude_deg=-63.9, elevation_m=133) china = Location('china', latitude_deg=35.4, longitude_deg=110, elevation_m=1000) eastern_russia = Location('eastern_russia', latitude_deg=66, longitude_deg=145, elevation_m=650) france = Location('france', latitude_deg=46.4, longitude_deg=2.75, elevation_m=300) germany = Location("ALEMANIA", latitude_deg=52.515083, longitude_deg=13.323723, elevation_m=30) india = Location('india', latitude_deg=23.5, longitude_deg=78.5, elevation_m=550) moscu = Location('moscu', latitude_deg=55.7, longitude_deg=37.5, elevation_m=137) niger = Location('niger', latitude_deg=20, longitude_deg=12.5, elevation_m=430) riogrande = Location("RIOGRANDE", latitude_deg=-53.8, longitude_deg=-67.75, elevation_m=30) def extend_from_module(module, vars): mod = importlib.import_module(module) vars.update(mod.__dict__) # Load custom locations, if the variable is specified if os.getenv("ORBIT_PREDICTOR_CUSTOM_LOCATIONS"): extend_from_module(os.environ["ORBIT_PREDICTOR_CUSTOM_LOCATIONS"], locals()) orbit-predictor-1.9.3/orbit_predictor/predictors/0000775000175000017500000000000013473027405023031 5ustar juanlujuanlu00000000000000orbit-predictor-1.9.3/orbit_predictor/predictors/__init__.py0000664000175000017500000000037113473027330025140 0ustar juanlujuanlu00000000000000from orbit_predictor.predictors.base import Position, PredictedPass from orbit_predictor.exceptions import NotReachable from orbit_predictor.predictors.tle import TLEPredictor __all__ = ["Position", "PredictedPass", "NotReachable", "TLEPredictor"] orbit-predictor-1.9.3/orbit_predictor/predictors/accurate.py0000664000175000017500000001135413473027330025173 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Accurate Predictor ~~~~~~~~~~~~~~~~~~ Provides a faster an better predictor IMPORTANT!! All calculations use radians, instead degrees Be warned! Known Issues ~~~~~~~~~~~~ In some cases not studied deeply, (because we don't have enough data) ascending or descending point are not found and propagation fails. Code use some hacks to prevent multiple calculations, and function calls are small as posible. Some stuff won't be trivial to understand, but comments and fixes are welcome """ import datetime as dt from functools import lru_cache from sgp4 import ext, model from sgp4.earth_gravity import wgs84 from sgp4.io import twoline2rv from sgp4.propagation import _gstime from orbit_predictor import coordinate_systems from orbit_predictor.utils import reify from .base import CartesianPredictor, logger # Hack Zone be warned @lru_cache(maxsize=365) def jday_day(year, mon, day): return (367.0 * year - 7.0 * (year + ((mon + 9.0) // 12.0)) * 0.25 // 1.0 + 275.0 * mon // 9.0 + day + 1721013.5) def jday(year, mon, day, hr, minute, sec): base = jday_day(year, mon, day) return base + ((sec / 60.0 + minute) / 60.0 + hr) / 24.0 ext.jday = jday model.jday = jday # finish hack zone class HighAccuracyTLEPredictor(CartesianPredictor): """A pass predictor with high accuracy on estimations""" @reify def tle(self): return self.source.get_tle(self.sate_id, dt.datetime.utcnow()) @reify def propagator(self): tle_line_1, tle_line_2 = self.tle.lines return twoline2rv(tle_line_1, tle_line_2, wgs84) @reify def mean_motion(self): return self.propagator.no # this speed is in radians/minute @lru_cache(maxsize=3600 * 24 * 7) # Max cache, a week def _propagate_only_position_ecef(self, timetuple): """Return position in the given date using ECEF coordinate system.""" position_eci, _ = self.propagator.propagate(*timetuple) gmst = _gstime(jday(*timetuple)) return coordinate_systems.eci_to_ecef(position_eci, gmst) def _propagate_eci(self, when_utc=None): """Return position and velocity in the given date using ECI coordinate system.""" tle = self.source.get_tle(self.sate_id, when_utc) logger.debug("Propagating using ECI. sate_id: %s, when_utc: %s, tle: %s", self.sate_id, when_utc, tle) tle_line_1, tle_line_2 = tle.lines sgp4_sate = twoline2rv(tle_line_1, tle_line_2, wgs84) timetuple = when_utc.timetuple()[:6] timetuple[5] = timetuple[5] + when_utc.microsecond * 1e-6 position_eci, velocity_eci = sgp4_sate.propagate(*timetuple) return position_eci, velocity_eci def _propagate_ecef(self, when_utc): """Return position and velocity in the given date using ECEF coordinate system.""" timetuple = (when_utc.year, when_utc.month, when_utc.day, when_utc.hour, when_utc.minute, when_utc.second + when_utc.microsecond * 1e-6) position_eci, velocity_eci = self.propagator.propagate(*timetuple) gmst = _gstime(jday(*timetuple)) position_ecef = coordinate_systems.eci_to_ecef(position_eci, gmst) velocity_ecef = coordinate_systems.eci_to_ecef(velocity_eci, gmst) return (position_ecef, velocity_ecef) def get_only_position(self, when_utc): """Return a tuple in ECEF coordinate system Code is optimized, dont complain too much! """ timetuple = (when_utc.year, when_utc.month, when_utc.day, when_utc.hour, when_utc.minute, when_utc.second + when_utc.microsecond * 1e-6) return self._propagate_only_position_ecef(timetuple) orbit-predictor-1.9.3/orbit_predictor/predictors/base.py0000664000175000017500000003323713473027330024322 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import datetime as dt import logging import warnings from collections import namedtuple from math import pi, acos, degrees, radians from orbit_predictor.exceptions import NotReachable, PropagationError from orbit_predictor import coordinate_systems from orbit_predictor.utils import ( cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime ) logger = logging.getLogger(__name__) ONE_SECOND = dt.timedelta(seconds=1) def round_datetime(dt_): return dt_ class Position(namedtuple( "Position", ['when_utc', 'position_ecef', 'velocity_ecef', 'error_estimate'])): @reify def position_llh(self): return coordinate_systems.ecef_to_llh(self.position_ecef) class PredictedPass: def __init__(self, location, sate_id, max_elevation_deg, aos, los, duration_s, max_elevation_position=None, max_elevation_date=None): self.location = location self.sate_id = sate_id self.max_elevation_position = max_elevation_position self.max_elevation_date = max_elevation_date self.max_elevation_deg = max_elevation_deg self.aos = aos self.los = los self.duration_s = duration_s @property def midpoint(self): """Returns a datetime of the midpoint of the pass""" return self.aos + (self.los - self.aos) / 2 def __repr__(self): return "".format(self.sate_id, self.location, self.aos) def __eq__(self, other): return all([issubclass(other.__class__, PredictedPass), self.location == other.location, self.sate_id == other.sate_id, self.max_elevation_position == other.max_elevation_position, self.max_elevation_date == other.max_elevation_date, self.max_elevation_deg == other.max_elevation_deg, self.aos == other.aos, self.los == other.los, self.duration_s == other.duration_s]) def get_off_nadir_angle(self): warnings.warn("This method is deprecated!", DeprecationWarning) return self.off_nadir_deg @reify def off_nadir_deg(self): """Computes off-nadir angle calculation Given satellite position ``sate_pos``, velocity ``sate_vel``, and location ``target`` in a common frame, off-nadir angle ``off_nadir_angle`` is given by: t2b = sate_pos - target cos(off_nadir_angle) = (sate_pos · t2b) # Vectorial dot product _______________________ || sate_pos || || t2b|| Sign for the rotation is calculated this way cross = target ⨯ sate_pos sign = cross · sate_vel ____________________ | cross · sate_vel | """ sate_pos = self.max_elevation_position.position_ecef sate_vel = self.max_elevation_position.velocity_ecef target = self.location.position_ecef t2b = vector_diff(sate_pos, target) angle = acos( dot_product(sate_pos, t2b) / (vector_norm(sate_pos) * vector_norm(t2b)) ) cross = cross_product(target, sate_pos) dot = dot_product(cross, sate_vel) try: sign = dot / abs(dot) except ZeroDivisionError: sign = 1 return degrees(angle) * sign class Predictor: def __init__(self, sate_id, source): self.sate_id = sate_id self.source = source def get_position(self, when_utc=None): raise NotImplementedError("You have to implement it!") class CartesianPredictor(Predictor): def _propagate_eci(self, when_utc=None): raise NotImplementedError def _propagate_ecef(self, when_utc=None): """Return position and velocity in the given date using ECEF coordinate system.""" position_eci, velocity_eci = self._propagate_eci(when_utc) gmst = gstime_from_datetime(when_utc) position_ecef = coordinate_systems.eci_to_ecef(position_eci, gmst) velocity_ecef = coordinate_systems.eci_to_ecef(velocity_eci, gmst) return position_ecef, velocity_ecef @reify def mean_motion(self): raise NotImplementedError def get_position(self, when_utc=None): """Return a Position namedtuple in ECEF coordinate system""" if when_utc is None: when_utc = dt.datetime.utcnow() position_ecef, velocity_ecef = self._propagate_ecef(when_utc) return Position(when_utc=when_utc, position_ecef=position_ecef, velocity_ecef=velocity_ecef, error_estimate=None) def get_only_position(self, when_utc): """Return a tuple in ECEF coordinate system""" return self.get_position(when_utc).position_ecef def passes_over(self, location, when_utc, limit_date=None, max_elevation_gt=0, aos_at_dg=0): return LocationPredictor(location, self, when_utc, limit_date, max_elevation_gt, aos_at_dg) def get_next_pass(self, location, when_utc=None, max_elevation_gt=5, aos_at_dg=0, limit_date=None): """Return a PredictedPass instance with the data of the next pass over the given location locattion_llh: point on Earth we want to see from the satellite. when_utc: datetime UTC. max_elevation_gt: filter passings with max_elevation under it. aos_at_dg: This is if we want to start the pass at a specific elevation. """ if when_utc is None: when_utc = dt.datetime.utcnow() for pass_ in self.passes_over(location, when_utc, limit_date, max_elevation_gt=max_elevation_gt, aos_at_dg=aos_at_dg): return pass_ else: raise NotReachable('Propagation limit date exceeded') class GPSPredictor(Predictor): pass class LocationPredictor: """Predicts passes over a given location Exposes an iterable interface """ def __init__(self, location, propagator, start_date, limit_date=None, max_elevation_gt=0, aos_at_dg=0): self.location = location self.propagator = propagator self.start_date = start_date self.limit_date = limit_date self.max_elevation_gt = radians(max([max_elevation_gt, aos_at_dg])) self.aos_at = radians(aos_at_dg) def __iter__(self): """Returns one pass each time""" current_date = self.start_date while True: if self.is_ascending(current_date): # we need a descending point ascending_date = current_date descending_date = self._find_nearest_descending(ascending_date) pass_ = self._refine_pass(ascending_date, descending_date) if pass_.valid: if self.limit_date is not None and pass_.aos > self.limit_date: break yield self._build_predicted_pass(pass_) if self.limit_date is not None and current_date > self.limit_date: break current_date = pass_.tca + self._orbit_step(0.6) else: current_date = self._find_nearest_ascending(current_date) def _build_predicted_pass(self, accuratepass): """Returns a classic predicted pass""" tca_position = self.propagator.get_position(accuratepass.tca) return PredictedPass(self.location, self.propagator.sate_id, max_elevation_deg=accuratepass.max_elevation_deg, aos=accuratepass.aos, los=accuratepass.los, duration_s=accuratepass.duration.total_seconds(), max_elevation_position=tca_position, max_elevation_date=accuratepass.tca, ) def _find_nearest_descending(self, ascending_date): for candidate in self._sample_points(ascending_date): if not self.is_ascending(candidate): return candidate else: logger.error('Could not find a descending pass over %s start date: %s - TLE: %s', self.location, ascending_date, self.propagator.tle) raise PropagationError("Can not find an descending phase") def _find_nearest_ascending(self, descending_date): for candidate in self._sample_points(descending_date): if self.is_ascending(candidate): return candidate else: logger.error('Could not find an ascending pass over %s start date: %s - TLE: %s', self.location, descending_date, self.propagator.tle) raise PropagationError('Can not find an ascending phase') def _sample_points(self, date): """Helper method to found ascending or descending phases of elevation""" start = date end = date + self._orbit_step(0.99) mid = self.midpoint(start, end) mid_right = self.midpoint(mid, end) mid_left = self.midpoint(start, mid) return [end, mid, mid_right, mid_left] def _refine_pass(self, ascending_date, descending_date): tca = self._find_tca(ascending_date, descending_date) elevation = self._elevation_at(tca) if elevation > self.max_elevation_gt: aos = self._find_aos(tca) los = self._find_los(tca) else: aos = los = None return AccuratePredictedPass(aos, tca, los, elevation) def _find_tca(self, ascending_date, descending_date): while not self._precision_reached(ascending_date, descending_date): midpoint = self.midpoint(ascending_date, descending_date) if self.is_ascending(midpoint): ascending_date = midpoint else: descending_date = midpoint return ascending_date def _precision_reached(self, start, end): # TODO: Allow the precision to change from the outside return end - start <= ONE_SECOND @staticmethod def midpoint(start, end): """Returns the midpoint between two dates""" return start + (end - start) / 2 def _elevation_at(self, when_utc): position = self.propagator.get_only_position(when_utc) return self.location.elevation_for(position) def is_passing(self, when_utc): """Returns a boolean indicating if satellite is actually visible""" return bool(self._elevation_at(when_utc)) def is_ascending(self, when_utc): """Check is elevation is ascending or descending on a given point""" elevation = self._elevation_at(when_utc) next_elevation = self._elevation_at(when_utc + ONE_SECOND) return elevation <= next_elevation def _orbit_step(self, size): """Returns a time step, that will make the satellite advance a given number of orbits""" step_in_radians = size * 2 * pi seconds = (step_in_radians / self.propagator.mean_motion) * 60 return dt.timedelta(seconds=seconds) def _find_aos(self, tca): end = tca start = tca - self._orbit_step(0.34) # On third of the orbit elevation = self._elevation_at(start) assert elevation < 0 while not self._precision_reached(start, end): midpoint = self.midpoint(start, end) elevation = self._elevation_at(midpoint) if elevation < self.aos_at: start = midpoint else: end = midpoint return end def _find_los(self, tca): start = tca end = tca + self._orbit_step(0.34) while not self._precision_reached(start, end): midpoint = self.midpoint(start, end) elevation = self._elevation_at(midpoint) if elevation < self.aos_at: end = midpoint else: start = midpoint return start class AccuratePredictedPass: def __init__(self, aos, tca, los, max_elevation): self.aos = round_datetime(aos) if aos is not None else None self.tca = round_datetime(tca) self.los = round_datetime(los) if los is not None else None self.max_elevation = max_elevation @property def valid(self): return self.max_elevation > 0 and self.aos is not None and self.los is not None @reify def max_elevation_deg(self): return degrees(self.max_elevation) @reify def duration(self): return self.los - self.aos orbit-predictor-1.9.3/orbit_predictor/predictors/keplerian.py0000664000175000017500000001254313473027330025357 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import datetime as dt from math import degrees, radians, sqrt import numpy as np from sgp4.earth_gravity import wgs84 from sgp4.io import twoline2rv from orbit_predictor import coordinate_systems from orbit_predictor.angles import ta_to_M, M_to_ta from orbit_predictor.keplerian import rv2coe, coe2rv from orbit_predictor.predictors import TLEPredictor from orbit_predictor.predictors.base import CartesianPredictor from orbit_predictor.utils import gstime_from_datetime, mean_motion MU_E = wgs84.mu def kepler(argp, delta_t_sec, ecc, inc, p, raan, sma, ta): # Initial mean anomaly M_0 = ta_to_M(ta, ecc) # Mean motion n = sqrt(wgs84.mu / sma ** 3) # Propagation M = M_0 + n * delta_t_sec # New true anomaly ta = M_to_ta(M, ecc) # Position and velocity vectors position_eci, velocity_eci = coe2rv(MU_E, p, ecc, inc, raan, argp, ta) return position_eci, velocity_eci class KeplerianPredictor(CartesianPredictor): """Propagator that uses the Keplerian osculating orbital elements. We use a naïve propagation algorithm that advances the anomaly the corresponding amount depending on the time difference and keeps all the rest of the osculating elements. It's robust against singularities as long as the starting elements are well specified but only works for elliptical orbits (ecc < 1). This limitation is not a problem since the object of study are artificial satellites orbiting the Earth. """ def __init__(self, sma, ecc, inc, raan, argp, ta, epoch): """Initializes predictor. :param sma: Semimajor axis, km :param ecc: Eccentricity :param inc: Inclination, deg :param raan: Right ascension of the ascending node, deg :param argp: Argument of perigee, deg :param ta: True anomaly, deg :param epoch: Epoch, datetime """ if ecc >= 1.0: raise NotImplementedError("Parabolic and elliptic orbits " "are not implemented") self._sma = sma self._ecc = ecc self._inc = inc self._raan = raan self._argp = argp self._ta = ta self._epoch = epoch @property def sate_id(self): # Keplerian predictors are not made of actual observations return "" @property def mean_motion(self): return mean_motion(self._sma) * 60 # this speed is in radians/minute @classmethod def from_tle(cls, sate_id, source, date=None): """Returns approximate keplerian elements from TLE. The conversion between mean elements in the TEME reference frame to osculating elements in any standard reference frame is not well defined in literature (see Vallado 3rd edition, pp 236 to 240) """ # Get latest TLE, or the one corresponding to a specified date if date is None: date = dt.datetime.utcnow() tle = source.get_tle(sate_id, date) # Retrieve TLE epoch and corresponding position epoch = twoline2rv(tle.lines[0], tle.lines[1], wgs84).epoch pos = TLEPredictor(sate_id, source).get_position(epoch) # Convert position from ECEF to ECI gmst = gstime_from_datetime(epoch) position_eci = coordinate_systems.ecef_to_eci(pos.position_ecef, gmst) velocity_eci = coordinate_systems.ecef_to_eci(pos.velocity_ecef, gmst) # Convert position to Keplerian osculating elements p, ecc, inc, raan, argp, ta = rv2coe( wgs84.mu, np.array(position_eci), np.array(velocity_eci)) sma = p / (1 - ecc ** 2) return cls(sma, ecc, degrees(inc), degrees(raan), degrees(argp), degrees(ta), epoch) def _propagate_eci(self, when_utc): """Return position and velocity in the given date using ECI coordinate system. """ # Orbit parameters sma = self._sma ecc = self._ecc p = sma * (1 - ecc ** 2) inc = radians(self._inc) raan = radians(self._raan) argp = radians(self._argp) ta = radians(self._ta) delta_t_sec = (when_utc - self._epoch).total_seconds() # Propagate position_eci, velocity_eci = kepler(argp, delta_t_sec, ecc, inc, p, raan, sma, ta) return tuple(position_eci), tuple(velocity_eci) orbit-predictor-1.9.3/orbit_predictor/predictors/numerical.py0000664000175000017500000001543513473027330025367 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from math import degrees, radians, sqrt, cos, sin import datetime as dt import numpy as np from sgp4.earth_gravity import wgs84 from orbit_predictor.predictors.keplerian import KeplerianPredictor from orbit_predictor.angles import ta_to_M, M_to_ta from orbit_predictor.keplerian import coe2rv from orbit_predictor.utils import njit, raan_from_ltan, float_to_hms OMEGA = 2 * np.pi / (86400 * 365.2421897) # rad / s MU_E = wgs84.mu R_E_KM = wgs84.radiusearthkm J2 = wgs84.j2 def sun_sync_plane_constellation(num_satellites, *, alt_km=None, ecc=None, inc_deg=None, ltan_h=12, date=None): """Creates num_satellites in the same Sun-synchronous plane, uniformly spaced. Parameters ---------- num_satellites : int Number of satellites. alt_km : float, optional Altitude, in km. ecc : float, optional Eccentricity. inc_deg : float, optional Inclination, in degrees. ltan_h : int, optional Local Time of the Ascending Node, in hours (default to noon). date : datetime.date, optional Reference date for the orbit, (default to today). """ for ta_deg in np.linspace(0, 360, num_satellites, endpoint=False): yield J2Predictor.sun_synchronous( alt_km=alt_km, ecc=ecc, inc_deg=inc_deg, ltan_h=ltan_h, date=date, ta_deg=ta_deg ) @njit def pkepler(argp, delta_t_sec, ecc, inc, p, raan, sma, ta): """Perturbed Kepler problem (only J2) Notes ----- Based on algorithm 64 of Vallado 3rd edition """ # Mean motion n = sqrt(MU_E / sma ** 3) # Initial mean anomaly M_0 = ta_to_M(ta, ecc) # Update for perturbations delta_raan = ( - (3 * n * R_E_KM ** 2 * J2) / (2 * p ** 2) * cos(inc) * delta_t_sec ) raan = raan + delta_raan delta_argp = ( (3 * n * R_E_KM ** 2 * J2) / (4 * p ** 2) * (4 - 5 * sin(inc) ** 2) * delta_t_sec ) argp = argp + delta_argp M0_dot = ( (3 * n * R_E_KM ** 2 * J2) / (4 * p ** 2) * (2 - 3 * sin(inc) ** 2) * sqrt(1 - ecc ** 2) ) M_dot = n + M0_dot # Propagation M = M_0 + M_dot * delta_t_sec # New true anomaly ta = M_to_ta(M, ecc) # Position and velocity vectors position_eci, velocity_eci = coe2rv(MU_E, p, ecc, inc, raan, argp, ta) return position_eci, velocity_eci class InvalidOrbitError(Exception): pass class J2Predictor(KeplerianPredictor): """Propagator that uses secular variations due to J2. """ @classmethod def sun_synchronous(cls, *, alt_km=None, ecc=None, inc_deg=None, ltan_h=12, date=None, ta_deg=0): """Creates Sun synchronous predictor instance. Parameters ---------- alt_km : float, optional Altitude, in km. ecc : float, optional Eccentricity. inc_deg : float, optional Inclination, in degrees. ltan_h : int, optional Local Time of the Ascending Node, in hours (default to noon). date : datetime.date, optional Reference date for the orbit, (default to today). ta_deg : float Increment or decrement of true anomaly, will adjust the epoch accordingly. """ if date is None: date = dt.datetime.today().date() try: with np.errstate(invalid="raise"): if alt_km is not None and ecc is not None: # Normal case, solve for inclination sma = R_E_KM + alt_km inc_deg = degrees(np.arccos( (-2 * sma ** (7 / 2) * OMEGA * (1 - ecc ** 2) ** 2) / (3 * R_E_KM ** 2 * J2 * np.sqrt(MU_E)) )) elif alt_km is not None and inc_deg is not None: # Not so normal case, solve for eccentricity sma = R_E_KM + alt_km ecc = np.sqrt( 1 - np.sqrt( (-3 * R_E_KM ** 2 * J2 * np.sqrt(MU_E) * np.cos(radians(inc_deg))) / (2 * OMEGA * sma ** (7 / 2)) ) ) elif ecc is not None and inc_deg is not None: # Rare case, solve for altitude sma = (-np.cos(radians(inc_deg)) * (3 * R_E_KM ** 2 * J2 * np.sqrt(MU_E)) / (2 * OMEGA * (1 - ecc ** 2) ** 2)) ** (2 / 7) else: raise ValueError( "Exactly two of altitude, eccentricity and inclination must be given" ) except FloatingPointError: raise InvalidOrbitError("Cannot find Sun-synchronous orbit with given parameters") # TODO: Allow change in time or location # Right the epoch is fixed given the LTAN, as well as the sub-satellite point epoch = dt.datetime(date.year, date.month, date.day, *float_to_hms(ltan_h)) raan = raan_from_ltan(epoch, ltan_h) return cls(sma, ecc, inc_deg, raan, 0, ta_deg, epoch) def _propagate_eci(self, when_utc=None): """Return position and velocity in the given date using ECI coordinate system. """ # Orbit parameters sma = self._sma ecc = self._ecc p = sma * (1 - ecc ** 2) inc = radians(self._inc) raan = radians(self._raan) argp = radians(self._argp) ta = radians(self._ta) delta_t_sec = (when_utc - self._epoch).total_seconds() # Propagate position_eci, velocity_eci = pkepler(argp, delta_t_sec, ecc, inc, p, raan, sma, ta) return tuple(position_eci), tuple(velocity_eci) orbit-predictor-1.9.3/orbit_predictor/predictors/tle.py0000664000175000017500000000016213473027330024163 0ustar juanlujuanlu00000000000000from .accurate import HighAccuracyTLEPredictor # Backwards compatibility TLEPredictor = HighAccuracyTLEPredictor orbit-predictor-1.9.3/orbit_predictor/sources.py0000664000175000017500000001534713473027330022717 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import logging from collections import defaultdict, namedtuple import warnings import requests from urllib import parse as urlparse from urllib.parse import urlencode from sgp4.earth_gravity import wgs84 from sgp4.io import twoline2rv from orbit_predictor.predictors import TLEPredictor logger = logging.getLogger(__name__) TLE = namedtuple('TLE', ['sate_id', 'lines', 'date']) class GPSSource: def get_position_ecef(self, sate_id, when_utc): raise NotImplementedError("You have to implement it.") class TLESource: def add_tle(self, sate_id, tle, epoch): raise NotImplementedError("You have to implement it.") def _get_tle(self, sate_id, date): raise NotImplementedError("You have to implement it.") def get_tle(self, sate_id, date): logger.debug("searching a TLE for %s, date: %s", sate_id, date) lines = self._get_tle(sate_id, date) return TLE(sate_id=sate_id, date=date, lines=lines) def get_predictor(self, sate_id, precise=True): """Return a Predictor instance using the current storage.""" if not precise: warnings.warn( "There is no `precise=False` predictor anymore " "and the parameter will be removed in the future", FutureWarning, ) return TLEPredictor(sate_id, self) class MemoryTLESource(TLESource): def __init__(self): self.tles = defaultdict(set) def add_tle(self, sate_id, tle, epoch): self.tles[sate_id].add((epoch, tle)) def _get_tle(self, sate_id, date): candidates = self.tles[sate_id] winner = None winner_dt = float("inf") for epoch, candidate in candidates: c_dt = abs((epoch - date).total_seconds()) if c_dt < winner_dt: winner = candidate winner_dt = c_dt if winner is None: raise LookupError("no tles in storage") return winner class EtcTLESource(TLESource): def __init__(self, filename="/etc/latest_tle"): self.filename = filename def add_tle(self, sate_id, tle, epoch): with open(self.filename, "w") as fd: fd.write(sate_id + "\n") for l in tle: fd.write(l + "\n") def _get_tle(self, sate_id, date): with open(self.filename) as fd: data = fd.read() lines = data.split("\n") if not lines[0] == sate_id: raise LookupError("Stored satellite id not") return tuple(lines[1:3]) class WSTLESource(TLESource): def __init__(self, url): self.url = url self.cache = MemoryTLESource() def add_tle(self, *args): raise ValueError("You can't add TLEs. The service has his own update task.") def _get_tle(self, sate_id, date): # first lookup on cache try: lines_from_cache = self.cache._get_tle(sate_id, date) except LookupError: pass else: return lines_from_cache lines = self.get_tle_for_date(sate_id, date) # save on cache self.cache.add_tle(sate_id, lines, date) return lines def get_last_update(self, sate_id): return self._fetch_tle("api/tle/last/", sate_id) def get_tle_for_date(self, sate_id, date): return self._fetch_tle("api/tle/closest/", sate_id, date) def _fetch_tle(self, path, sate_id, date=None): url = urlparse.urljoin(self.url, path) url = urlparse.urlparse(url) qargs = {'satellite_number': sate_id} if date is not None: date_str = date.strftime("%Y-%m-%d") qargs['date'] = date_str query_string = urlencode(qargs) url = urlparse.urlunsplit((url.scheme, url.netloc, url.path, query_string, url.fragment)) headers = {'user-agent': 'orbit-predictor', 'Accept': 'application/json'} try: response = requests.get(url, headers=headers) except requests.exceptions.RequestException as error: logger.error("Exception requesting TLE: %s", error) raise if response.ok and 'lines' in response.json(): lines = tuple(response.json()['lines']) return lines else: raise ValueError("Error requesting TLE: %s", response.text) class NoradTLESource(TLESource): """ This source is intended to be used with norad-like multi-line files eg. https://www.celestrak.com/NORAD/elements/resource.txt """ def __init__(self, content): self.content = content @classmethod def from_url(cls, url): headers = {'user-agent': 'orbit-predictor', 'Accept': 'text/plain'} try: response = requests.get(url, headers=headers) except requests.exceptions.RequestException as error: logger.error("Exception requesting TLE: %s", error) raise lines = response.content.decode("UTF-8").splitlines() return cls(lines) @classmethod def from_file(cls, filename): with open(filename, 'r') as f: lines = f.read().splitlines() return cls(lines) def _get_tle(self, sate_id, date): content = iter(self.content) for sate, line_1, line_2 in zip(content, content, content): if sate_id in sate: return tuple([line_1, line_2]) raise LookupError("Couldn't find it. Wrong file?") def get_predictor_from_tle_lines(tle_lines): db = MemoryTLESource() sgp4_sat = twoline2rv(tle_lines[0], tle_lines[1], wgs84) db.add_tle(sgp4_sat.satnum, tuple(tle_lines), sgp4_sat.epoch) predictor = TLEPredictor(sgp4_sat.satnum, db) return predictor orbit-predictor-1.9.3/orbit_predictor/utils.py0000664000175000017500000002653713473027330022377 0ustar juanlujuanlu00000000000000# MIT License # # Copyright (c) 2017 Satellogic SA # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import functools from collections import namedtuple import datetime as dt from math import asin, atan2, cos, degrees, floor, radians, sin, sqrt, modf import numpy as np from sgp4.earth_gravity import wgs84 from sgp4.ext import jday from sgp4.propagation import _gstime # Inspired in https://github.com/poliastro/poliastro/blob/88edda8/src/poliastro/jit.py try: from numba import njit except ImportError: import inspect def njit(first=None, *args, **kwargs): """Identity JIT, returns unchanged function.""" def _jit(f): return f if inspect.isfunction(first): return first else: return _jit # This function was ported from its Matlab equivalent here: # http://www.mathworks.com/matlabcentral/fileexchange/23051-vectorized-solar-azimuth-and-elevation-estimation DECEMBER_31TH_1999_MIDNIGHT_JD = 2451543.5 def compose(*functions): """Performs function composition with variadic arguments""" return functools.reduce(lambda f, g: lambda *args: f(g(*args)), functions, lambda x: x) cos_d = compose(cos, radians) sin_d = compose(sin, radians) atan2_d = compose(degrees, atan2) asin_d = compose(degrees, asin) AzimuthElevation = namedtuple('AzimuthElevation', 'azimuth elevation') def euclidean_distance(*components): """Returns the norm of a vector""" return sqrt(sum(c**2 for c in components)) def dot_product(a, b): """Computes dot product between two vectors writen as tuples or lists""" return sum(ai * bj for ai, bj in zip(a, b)) def vector_diff(a, b): """Computes difference between two vectors""" return tuple((ai - bi) for ai, bi in zip(a, b)) def cross_product(a, b): """Computes cross product between two vectors""" return ( a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ) def vector_norm(a): """Returns the norm of a vector""" return euclidean_distance(*a) @njit def cross(a, b): """Computes cross product between two vectors""" # np.cross is not supported in numba nopython mode, see # https://github.com/numba/numba/issues/2978 return np.array(( a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] )) # Inspired by https://github.com/poliastro/poliastro/blob/aaa1bb2/poliastro/util.py # and https://github.com/poliastro/poliastro/blob/06ef6ba/poliastro/util.py # Copyright (c) 2012-2017 Juan Luis Cano Rodríguez, MIT license @njit def rotate(vec, ax, angle): """Rotates the coordinate system around axis x, y or z a CCW angle. Parameters ---------- vec : ndarray Dimension 3 vector. ax : int Axis to be rotated. angle : float Angle of rotation (rad). Notes ----- This performs a so-called active or alibi transformation: rotates the vector while the coordinate system remains unchanged. To do the opposite operation (passive or alias transformation) call the function as `rotate(vec, ax, -angle)` or use the convenience function `transform`, see `[1]_`. References ---------- .. [1] http://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities """ assert vec.shape == (3,) rot = np.eye(3) if ax == 0: sl = slice(1, 3, 1) elif ax == 1: sl = slice(0, 3, 2) elif ax == 2: sl = slice(0, 2, 1) else: raise ValueError("Invalid axis: must be one of 0, 1 or 2") rot[sl, sl] = np.array(( (cos(angle), -sin(angle)), (sin(angle), cos(angle)) )) if ax == 1: rot = rot.T # np.dot() arguments must all have the same dtype return np.dot(rot, vec.astype(rot.dtype)) @njit def transform(vec, ax, angle): """Rotates a coordinate system around axis a positive right-handed angle. Notes ----- This is a convenience function, equivalent to `rotate(vec, ax, -angle)`. Refer to the documentation of that function for further information. """ return rotate(vec, ax, -angle) def raan_from_ltan(when, ltan=12.0): # TODO: Avoid code duplication # compute apparent right ascension of the sun (radians) utc_time_tuple = when.timetuple() utc_time_tuple = list(utc_time_tuple[:6]) utc_time_tuple[5] = utc_time_tuple[5] + when.microsecond * 1e-6 jd = juliandate(utc_time_tuple) date = jd - DECEMBER_31TH_1999_MIDNIGHT_JD w = 282.9404 + 4.70935e-5 * date # longitude of perihelion degrees eccentricity = 0.016709 - 1.151e-9 * date # eccentricity M = (356.0470 + 0.9856002585 * date) % 360 # mean anomaly degrees oblecl = 23.4393 - 3.563e-7 * date # Sun's obliquity of the ecliptic # auxiliary angle auxiliary_angle = M + degrees(eccentricity * sin_d(M) * (1 + eccentricity * cos_d(M))) # rectangular coordinates in the plane of the ecliptic (x axis toward perhilion) x = cos_d(auxiliary_angle) - eccentricity y = sin_d(auxiliary_angle) * sqrt(1 - eccentricity ** 2) # find the distance and true anomaly r = euclidean_distance(x, y) v = atan2_d(y, x) # find the longitude of the sun sun_lon = v + w # compute the ecliptic rectangular coordinates xeclip = r * cos_d(sun_lon) yeclip = r * sin_d(sun_lon) zeclip = 0.0 # rotate these coordinates to equatorial rectangular coordinates xequat = xeclip yequat = yeclip * cos_d(oblecl) + zeclip * sin_d(oblecl) # convert equatorial rectangular coordinates to RA and Decl: RA = atan2_d(yequat, xequat) # degrees # Idea from # https://www.mathworks.com/matlabcentral/fileexchange/39085-mean-local-time-of-the-ascending-node raan = (RA + 15.0 * (ltan - 12.0)) % 360 return raan def sun_azimuth_elevation(latitude_deg, longitude_deg, when=None): """ Return (azimuth, elevation) of the Sun at ground point :param latitude_deg: a float number representing latitude on degrees :param longitude_deg: a float number representing longitude on degrees :param when: a ``datetime.datetime`` object in utc, if not provided, utcnow() is used :returns: an ``AzimuthElevation`` namedtuple """ if when is None: when = dt.datetime.utcnow() utc_time_tuple = when.timetuple() utc_time_list = list(utc_time_tuple[:6]) utc_time_list[5] = utc_time_list[5] + when.microsecond * 1e-6 jd = juliandate(utc_time_list) date = jd - DECEMBER_31TH_1999_MIDNIGHT_JD w = 282.9404 + 4.70935e-5 * date # longitude of perihelion degrees eccentricity = 0.016709 - 1.151e-9 * date # eccentricity M = (356.0470 + 0.9856002585 * date) % 360 # mean anomaly degrees L = w + M # Sun's mean longitude degrees oblecl = 23.4393 - 3.563e-7 * date # Sun's obliquity of the ecliptic # auxiliary angle auxiliary_angle = M + degrees(eccentricity * sin_d(M) * (1 + eccentricity * cos_d(M))) # rectangular coordinates in the plane of the ecliptic (x axis toward perhilion) x = cos_d(auxiliary_angle) - eccentricity y = sin_d(auxiliary_angle) * sqrt(1 - eccentricity**2) # find the distance and true anomaly r = euclidean_distance(x, y) v = atan2_d(y, x) # find the longitude of the sun sun_lon = v + w # compute the ecliptic rectangular coordinates xeclip = r * cos_d(sun_lon) yeclip = r * sin_d(sun_lon) zeclip = 0.0 # rotate these coordinates to equitorial rectangular coordinates xequat = xeclip yequat = yeclip * cos_d(oblecl) + zeclip * sin_d(oblecl) zequat = yeclip * sin_d(23.4406) + zeclip * cos_d(oblecl) # convert equatorial rectangular coordinates to RA and Decl: r = euclidean_distance(xequat, yequat, zequat) RA = atan2_d(yequat, xequat) delta = asin_d(zequat/r) # Following the RA DEC to Az Alt conversion sequence explained here: # http://www.stargazing.net/kepler/altaz.html sidereal = sidereal_time(utc_time_tuple, longitude_deg, L) # Replace RA with hour angle HA HA = sidereal * 15 - RA # convert to rectangular coordinate system x = cos_d(HA) * cos_d(delta) y = sin_d(HA) * cos_d(delta) z = sin_d(delta) # rotate this along an axis going east-west. xhor = x * cos_d(90 - latitude_deg) - z * sin_d(90 - latitude_deg) yhor = y zhor = x * sin_d(90 - latitude_deg) + z * cos_d(90 - latitude_deg) # Find the h and AZ azimuth = atan2_d(yhor, xhor) + 180 elevation = asin_d(zhor) return AzimuthElevation(azimuth, elevation) def juliandate(utc_tuple): year, month, day, hour, minute, sec = utc_tuple[:6] if month <= 2: year -= 1 month += 12 return (floor(365.25*(year + 4716.0)) + floor(30.6001*(month+1.0)) + 2.0 - floor(year / 100.0) + floor(floor(year / 100.0) / 4.0) + day - 1524.5 + (hour + minute / 60.0 + sec / 3600.0) / 24.0) def sidereal_time(utc_tuple, local_lon, sun_lon): # Find the J2000 value # J2000 = jd - 2451545.0; UTH = utc_tuple.tm_hour + utc_tuple.tm_min / 60.0 + utc_tuple.tm_sec / 3600.0 # Calculate local siderial time GMST0 = ((sun_lon + 180) % 360) / 15 return GMST0 + UTH + local_lon / 15 def gstime_from_datetime(when_utc): timelist = list(when_utc.timetuple()[:6]) timelist[5] = timelist[5] + when_utc.microsecond * 1e-6 return _gstime(jday(*timelist)) def float_to_hms(hour): rem, hour = modf(hour) rem, minute = modf(rem * 60) rem, second = modf(rem * 60) return int(hour), int(minute), int(second), int(rem * 1e6) def mean_motion(sma_km): return sqrt(wgs84.mu / sma_km ** 3) # rad / s class reify: """ Use as a class method decorator. It operates almost exactly like the Python ``@property`` decorator, but it puts the result of the method it decorates into the instance dict after the first call, effectively replacing the function it decorates with an instance variable. It is, in Python parlance, a non-data descriptor. Taken from: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/decorator.html """ def __init__(self, wrapped): self.wrapped = wrapped functools.update_wrapper(self, wrapped) def __get__(self, inst, objtype=None): if inst is None: return self val = self.wrapped(inst) setattr(inst, self.wrapped.__name__, val) return val orbit-predictor-1.9.3/orbit_predictor/version.py0000664000175000017500000000010213473027330022700 0ustar juanlujuanlu00000000000000# https://www.python.org/dev/peps/pep-0440/ __version__ = '1.9.3' orbit-predictor-1.9.3/orbit_predictor.egg-info/0000775000175000017500000000000013473027405022345 5ustar juanlujuanlu00000000000000orbit-predictor-1.9.3/orbit_predictor.egg-info/PKG-INFO0000664000175000017500000001121013473027402023432 0ustar juanlujuanlu00000000000000Metadata-Version: 2.1 Name: orbit-predictor Version: 1.9.3 Summary: Python library to propagate satellite orbits. Home-page: https://github.com/satellogic/orbit-predictor Author: Satellogic SA Author-email: oss@satellogic.com License: MIT Description: Orbit Predictor =============== .. image:: https://travis-ci.org/satellogic/orbit-predictor.svg?branch=master :target: https://travis-ci.org/satellogic/orbit-predictor .. image:: https://coveralls.io/repos/github/satellogic/orbit-predictor/badge.svg?branch=master :target: https://coveralls.io/github/satellogic/orbit-predictor?branch=master Orbit Predictor is a Python library to propagate orbits of Earth-orbiting objects (satellites, ISS, Santa Claus, etc) using `TLE (Two-Line Elements set) `_ Al the hard work is done by Brandon Rhodes implementation of `SGP4 `_. We can say *Orbit predictor* is kind of a "wrapper" for the python implementation of SGP4 To install it ------------- You can install orbit-predictor from pypi:: pip install orbit-predictor Use example ----------- When will be the ISS over Argentina? :: In [1]: from orbit_predictor.sources import EtcTLESource In [2]: from orbit_predictor.locations import ARG In [3]: source = EtcTLESource(filename="examples/iss.tle") In [4]: predictor = source.get_predictor("ISS") In [5]: predictor.get_next_pass(ARG) Out[5]: In [6]: predicted_pass = _ In [7]: position = predictor.get_position(predicted_pass.aos) In [8]: ARG.is_visible(position) # Can I see the ISS from this location? Out[8]: True In [9]: import datetime In [10]: position_delta = predictor.get_position(predicted_pass.los + datetime.timedelta(minutes=20)) In [11]: ARG.is_visible(position_delta) Out[11]: False In [12]: tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1) In [13]: predictor.get_next_pass(ARG, tomorrow, max_elevation_gt=20) Out[13]: Simplified creation of predictor from TLE lines: :: In [1]: import datetime In [2]: from orbit_predictor.sources import get_predictor_from_tle_lines In [3]: TLE_LINES = ( "1 43204U 18015K 18339.11168986 .00000941 00000-0 42148-4 0 9999", "2 43204 97.3719 104.7825 0016180 271.1347 174.4597 15.23621941 46156") In [4]: predictor = get_predictor_from_tle_lines(TLE_LINES) In [5]: predictor.get_position(datetime.datetime(2019, 1, 1)) Out[5]: Position(when_utc=datetime.datetime(2019, 1, 1, 0, 0), position_ecef=(-5280.795613274576, -3977.487633239489, -2061.43227648734), velocity_ecef=(-2.4601788971676903, -0.47182217472755117, 7.167517631852518), error_estimate=None) Currently you have available these sources ------------------------------------------ - Memorytlesource: in memory storage. - EtcTLESource: a uniq TLE is stored in `/etc/latest_tle` - WSTLESource: It reads a REST API currently used inside Satellogic. We are are working to make it publicly available. How to contribute ----------------- - Write pep8 complaint code. - Wrap the code on 100 collumns. - Always use a branch for each feature and Merge Proposals. - Always run the tests before to push. (test implies pep8 validation) Keywords: orbit,sgp4,TLE,space,satellites Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.4 Provides-Extra: dev Provides-Extra: fast orbit-predictor-1.9.3/orbit_predictor.egg-info/SOURCES.txt0000664000175000017500000000135213473027402024227 0ustar juanlujuanlu00000000000000README.rst setup.cfg setup.py orbit_predictor/__init__.py orbit_predictor/accuratepredictor.py orbit_predictor/angles.py orbit_predictor/coordinate_systems.py orbit_predictor/exceptions.py orbit_predictor/keplerian.py orbit_predictor/locations.py orbit_predictor/sources.py orbit_predictor/utils.py orbit_predictor/version.py orbit_predictor.egg-info/PKG-INFO orbit_predictor.egg-info/SOURCES.txt orbit_predictor.egg-info/dependency_links.txt orbit_predictor.egg-info/requires.txt orbit_predictor.egg-info/top_level.txt orbit_predictor/predictors/__init__.py orbit_predictor/predictors/accurate.py orbit_predictor/predictors/base.py orbit_predictor/predictors/keplerian.py orbit_predictor/predictors/numerical.py orbit_predictor/predictors/tle.pyorbit-predictor-1.9.3/orbit_predictor.egg-info/dependency_links.txt0000664000175000017500000000000113473027402026410 0ustar juanlujuanlu00000000000000 orbit-predictor-1.9.3/orbit_predictor.egg-info/requires.txt0000664000175000017500000000024013473027402024736 0ustar juanlujuanlu00000000000000numpy>=1.8.2 sgp4 requests [dev] hypothesis flake8 hypothesis[datetime] mock logassert pytest pytest-cov pytest-benchmark pytz [fast] numba>=0.38 scipy>=0.16 orbit-predictor-1.9.3/orbit_predictor.egg-info/top_level.txt0000664000175000017500000000002013473027402025064 0ustar juanlujuanlu00000000000000orbit_predictor orbit-predictor-1.9.3/setup.cfg0000664000175000017500000000025413473027405017303 0ustar juanlujuanlu00000000000000[flake8] max-line-length = 99 exclude = .git, __pycache__, .ropeproject, .fades [isort] line_length = 79 multi_line_output = 3 [egg_info] tag_build = tag_date = 0 orbit-predictor-1.9.3/setup.py0000775000175000017500000000320313473027330017171 0ustar juanlujuanlu00000000000000#!/usr/bin/env python3 import os.path from setuptools import setup, find_packages # Copyright 2017 Satellogic SA. # https://packaging.python.org/guides/single-sourcing-package-version/ version = {} with open(os.path.join("orbit_predictor", "version.py")) as fp: exec(fp.read(), version) setup( name='orbit-predictor', version=version["__version__"], author='Satellogic SA', author_email='oss@satellogic.com', description='Python library to propagate satellite orbits.', long_description=open('README.rst').read(), packages=find_packages(exclude=["tests"]), license="MIT", url='https://github.com/satellogic/orbit-predictor', # Keywords to get found easily on PyPI results,etc. keywords="orbit, sgp4, TLE, space, satellites", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Science/Research', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Utilities', 'Programming Language :: Python :: 3', ], install_requires=[ 'numpy>=1.8.2', 'sgp4', 'requests', ], extras_require={ "fast": [ "numba>=0.38", "scipy>=0.16", ], "dev": [ "hypothesis", "flake8", "hypothesis[datetime]", "mock", "logassert", "pytest", "pytest-cov", "pytest-benchmark", "pytz", ], }, python_requires=">=3.4", )