././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1608379368.8700297 orbit-predictor-1.14.2/0000775000175000017500000000000000000000000015530 5ustar00juanlujuanlu00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1608379368.8700297 orbit-predictor-1.14.2/PKG-INFO0000664000175000017500000001123700000000000016631 0ustar00juanlujuanlu00000000000000Metadata-Version: 2.1 Name: orbit-predictor Version: 1.14.2 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://github.com/satellogic/orbit-predictor/workflows/Python%20package/badge.svg :target: https://github.com/satellogic/orbit-predictor/actions .. 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) `_ All 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: fast Provides-Extra: dev ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1607692485.0 orbit-predictor-1.14.2/README.rst0000664000175000017500000000624600000000000017227 0ustar00juanlujuanlu00000000000000Orbit Predictor =============== .. image:: https://github.com/satellogic/orbit-predictor/workflows/Python%20package/badge.svg :target: https://github.com/satellogic/orbit-predictor/actions .. 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) `_ All 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) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1608379368.8700297 orbit-predictor-1.14.2/orbit_predictor/0000775000175000017500000000000000000000000020722 5ustar00juanlujuanlu00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577191523.0 orbit-predictor-1.14.2/orbit_predictor/__init__.py0000644000175000017500000000007400000000000023032 0ustar00juanlujuanlu00000000000000from .version import __version__ __all__ = ["__version__"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577191523.0 orbit-predictor-1.14.2/orbit_predictor/angles.py0000644000175000017500000000735100000000000022551 0ustar00juanlujuanlu00000000000000# 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608292261.0 orbit-predictor-1.14.2/orbit_predictor/constants.py0000664000175000017500000000075000000000000023312 0ustar00juanlujuanlu00000000000000from math import pi, radians from sgp4.earth_gravity import wgs84 AU = 149597870.700 # km LIGHT_SPEED_KMS = 299792.458 # km / s OMEGA = 2 * pi / (86400 * 365.2421897) # rad / s MU_E = wgs84.mu # km3 / s2 R_E_KM = wgs84.radiusearthkm # km R_E_MEAN_KM = 6371.0087714 # km F_E = 1 / 298.257223560 J2 = wgs84.j2 OMEGA_E = 7.292115e-5 # rad / s ALPHA_UMB = radians(0.264121687) # rad - from Vallado, section 5.3 ALPHA_PEN = radians(0.269007205) # rad - from Vallado, section 5.3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577191553.0 orbit-predictor-1.14.2/orbit_predictor/coordinate_systems.py0000644000175000017500000001250600000000000025214 0ustar00juanlujuanlu00000000000000# 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 _euclidean_distance(*components): # TODO: Remove code duplication with utils return sqrt(sum(c**2 for c in components)) 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 eci_to_radec(eci_coords): xequat, yequat, zequat = eci_coords # convert equatorial rectangular coordinates to RA and Decl: r = _euclidean_distance(xequat, yequat, zequat) RA = atan2(yequat, xequat) DEC = asin(zequat/r) return RA, DEC, r def radec_to_eci(radec_coords): raise NotImplementedError 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] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577191523.0 orbit-predictor-1.14.2/orbit_predictor/exceptions.py0000644000175000017500000000246400000000000023461 0ustar00juanlujuanlu00000000000000# 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""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577191523.0 orbit-predictor-1.14.2/orbit_predictor/groundtrack.py0000644000175000017500000000141100000000000023612 0ustar00juanlujuanlu00000000000000class BaseElevationAPI: def get_elevation(self, *, longitude, latitude): raise NotImplementedError def get_ground_point(self, position): latitude, longitude, _ = position.position_llh return ( longitude, latitude, self.get_elevation(longitude=longitude, latitude=latitude), ) def get_groundtrack(self, positions): return [self.get_ground_point(position) for position in positions] class ZeroElevation(BaseElevationAPI): def get_elevation(self, *, longitude, latitude): return 0.0 def compute_groundtrack(predictor, times, elevation_api=ZeroElevation()): positions = [predictor.get_position(time) for time in times] return elevation_api.get_groundtrack(positions) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577191523.0 orbit-predictor-1.14.2/orbit_predictor/keplerian.py0000644000175000017500000001031700000000000023246 0ustar00juanlujuanlu00000000000000# 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577435851.0 orbit-predictor-1.14.2/orbit_predictor/locations.py0000644000175000017500000003261200000000000023271 0ustar00juanlujuanlu00000000000000# 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.constants import LIGHT_SPEED_KMS from orbit_predictor import coordinate_systems from orbit_predictor.utils import reify, sun_azimuth_elevation class Location: def __init__(self, name, latitude_deg, longitude_deg, elevation_m): """Location. Parameters ---------- latitude_deg : float Latitude in degrees. longitude_deg : float Longitude in degrees. elevation_m : float Elevation in meters. """ 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()) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1608379368.8700297 orbit-predictor-1.14.2/orbit_predictor/predictors/0000775000175000017500000000000000000000000023100 5ustar00juanlujuanlu00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1605803751.0 orbit-predictor-1.14.2/orbit_predictor/predictors/__init__.py0000664000175000017500000000037100000000000025212 0ustar00juanlujuanlu00000000000000from 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"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1607692485.0 orbit-predictor-1.14.2/orbit_predictor/predictors/_minimize.py0000664000175000017500000000577300000000000025446 0ustar00juanlujuanlu00000000000000from math import sqrt, copysign, isnan from scipy.optimize.optimize import OptimizeResult, _status_message inf = float("inf") def minimize_scalar_bounded_alt(func, bounds, xatol=1e-5, maxiter=500, **extra): # Adapted from # https://github.com/scipy/scipy/blob/v1.5.2/scipy/optimize/optimize.py maxfun = maxiter x1, x2 = bounds assert x1 <= x2 flag = 0 sqrt_eps = sqrt(2.2e-16) golden_mean = 0.5 * (3.0 - sqrt(5.0)) a, b = x1, x2 fulc = a + golden_mean * (b - a) nfc, xf = fulc, fulc rat = e = 0.0 x = xf fx = func(x) num = 1 fu = inf ffulc = fnfc = fx xm = 0.5 * (a + b) tol1 = sqrt_eps * abs(xf) + xatol / 3.0 tol2 = 2.0 * tol1 while abs(xf - xm) > (tol2 - 0.5 * (b - a)): golden = 1 # Check for parabolic fit if abs(e) > tol1: golden = 0 r = (xf - nfc) * (fx - ffulc) q = (xf - fulc) * (fx - fnfc) p = (xf - fulc) * q - (xf - nfc) * r q = 2.0 * (q - r) if q > 0.0: p = -p q = abs(q) r = e e = rat # Check for acceptability of parabola if ((abs(p) < abs(0.5*q*r)) and (p > q*(a - xf)) and (p < q * (b - xf))): rat = (p + 0.0) / q x = xf + rat if ((x - a) < tol2) or ((b - x) < tol2): si = copysign(1, xm - xf) + ((xm - xf) == 0) rat = tol1 * si else: # do a golden-section step golden = 1 if golden: # do a golden-section step if xf >= xm: e = a - xf else: e = b - xf rat = golden_mean*e si = copysign(1, rat) + (rat == 0) x = xf + si * max(abs(rat), tol1) fu = func(x) num += 1 if fu <= fx: if x >= xf: a = xf else: b = xf fulc, ffulc = nfc, fnfc nfc, fnfc = xf, fx xf, fx = x, fu else: if x < xf: a = x else: b = x if (fu <= fnfc) or (nfc == xf): fulc, ffulc = nfc, fnfc nfc, fnfc = x, fu elif (fu <= ffulc) or (fulc == xf) or (fulc == nfc): fulc, ffulc = x, fu xm = 0.5 * (a + b) tol1 = sqrt_eps * abs(xf) + xatol / 3.0 tol2 = 2.0 * tol1 if num >= maxfun: flag = 1 break if isnan(xf) or isnan(fx) or isnan(fu): flag = 2 fval = fx result = OptimizeResult(fun=fval, status=flag, success=(flag == 0), message={0: 'Solution found.', 1: 'Maximum number of function calls ' 'reached.', 2: _status_message['nan']}.get(flag, ''), x=xf, nfev=num) return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379114.0 orbit-predictor-1.14.2/orbit_predictor/predictors/accurate.py0000664000175000017500000001134600000000000025246 0ustar00juanlujuanlu00000000000000# 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.api import Satrec, SGP4_ERRORS from sgp4.earth_gravity import wgs84 from sgp4.model import WGS84 from sgp4.propagation import gstime from orbit_predictor import coordinate_systems from ..exceptions import PropagationError from ..utils import reify, jday_from_datetime, unkozai from .base import CartesianPredictor # 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""" def __init__(self, sate_id, source): self._sate_id = sate_id self._source = source self.tle = self._source.get_tle(self.sate_id, dt.datetime.utcnow()) self._propagator = self._get_propagator() def _get_propagator(self): tle_line_1, tle_line_2 = self.tle.lines return Satrec.twoline2rv(tle_line_1, tle_line_2, WGS84) def __getstate__(self): # See https://docs.python.org/3/library/pickle.html#handling-stateful-objects state = self.__dict__.copy() del state["_propagator"] return state def __setstate__(self, state): # See https://docs.python.org/3/library/pickle.html#handling-stateful-objects self.__dict__.update(state) self._propagator = self._get_propagator() @property def sate_id(self): return self._sate_id @property def source(self): return self._source @reify def mean_motion(self): """Mean motion, in radians per minute""" return unkozai( self._propagator.no_kozai, self._propagator.ecco, self._propagator.inclo, wgs84 ) @lru_cache(maxsize=3600 * 24 * 7) # Max cache, a week def _propagate_only_position_ecef(self, when_utc): """Return position in the given date using ECEF coordinate system.""" jd, fr = jday_from_datetime(when_utc) status, position_eci, _ = self._propagator.sgp4(jd, fr) if status != 0: raise PropagationError(SGP4_ERRORS[status]) gmst = gstime(jd + fr) return coordinate_systems.eci_to_ecef(position_eci, gmst) def propagate_eci(self, when_utc=None): if when_utc is None: when_utc = dt.datetime.utcnow() jd, fr = jday_from_datetime(when_utc) status, position_eci, velocity_eci = self._propagator.sgp4(jd, fr) if status != 0: raise PropagationError(SGP4_ERRORS[status]) return position_eci, velocity_eci def get_only_position(self, when_utc=None): """Return a tuple in ECEF coordinate system Code is optimized, dont complain too much! """ if when_utc is None: when_utc = dt.datetime.utcnow() return self._propagate_only_position_ecef(when_utc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1607692485.0 orbit-predictor-1.14.2/orbit_predictor/predictors/base.py0000664000175000017500000002643600000000000024377 0ustar00juanlujuanlu00000000000000# 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, degrees import numpy as np try: from scipy.optimize import brentq, minimize_scalar except ImportError: warnings.warn('scipy module was not found, some features may not work properly.', ImportWarning) from orbit_predictor.constants import MU_E from orbit_predictor.exceptions import NotReachable from orbit_predictor import coordinate_systems from orbit_predictor.keplerian import rv2coe from orbit_predictor.utils import ( angle_between, reify, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals, ) from .pass_iterators import ( # noqa: F401 LocationPredictor, PredictedPass, ) 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): """Latitude (deg), longitude (deg), altitude (km).""" return coordinate_systems.ecef_to_llh(self.position_ecef) @reify def osculating_elements(self): """Osculating Keplerian orbital elements. Semimajor axis (km), eccentricity, inclination (deg), right ascension of the ascending node or RAAN (deg), argument of perigee (deg), true anomaly (deg). """ gmst = gstime_from_datetime(self.when_utc) position_eci = coordinate_systems.ecef_to_eci(self.position_ecef, gmst) velocity_eci = coordinate_systems.ecef_to_eci(self.velocity_ecef, gmst) # Convert position to Keplerian osculating elements p, ecc, inc, raan, argp, ta = rv2coe( MU_E, np.array(position_eci), np.array(velocity_eci) ) # Transform to more familiar semimajor axis sma = p / (1 - ecc ** 2) # NOTE: rv2coe already does % (2 * np.pi) # but under some circumstances this might require another pass, # see https://github.com/satellogic/orbit-predictor/pull/106#issuecomment-730177598 return sma, ecc, degrees(inc), degrees(raan), degrees(argp), degrees(ta) % 360 class Predictor: @property def sate_id(self): raise NotImplementedError def propagate_eci(self, when_utc=None): raise NotImplementedError def get_position(self, when_utc=None): raise NotImplementedError("You have to implement it!") def get_shadow(self, when_utc=None): """Gives illumination at given time (2 for illuminated, 1 for penumbra, 0 for umbra).""" if when_utc is None: when_utc = dt.datetime.utcnow() return get_shadow( self.get_position(when_utc).position_ecef, when_utc ) def get_normal_vector(self, when_utc=None): """Gets unitary normal vector (orthogonal to orbital plane) at given time.""" if when_utc is None: when_utc = dt.datetime.utcnow() position, velocity = self.propagate_eci(when_utc) orbital_plane_normal = np.cross(position, velocity) return orbital_plane_normal / vector_norm(orbital_plane_normal) def get_beta(self, when_utc=None): """Gets angle between orbital plane and Sun direction (beta) at given time, in degrees.""" if when_utc is None: when_utc = dt.datetime.utcnow() # Here we calculate the complementary angle of beta, # because we use the normal vector of the orbital plane beta_comp = angle_between( get_sun(when_utc), self.get_normal_vector(when_utc) ) # We subtract from 90 degrees to return the real beta angle return 90 - beta_comp class CartesianPredictor(Predictor): def _propagate_ecef(self, when_utc=None): """Return position and velocity in the given date using ECEF coordinate system.""" if when_utc is None: when_utc = dt.datetime.utcnow() 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): """Mean motion, in radians per minute""" raise NotImplementedError @reify def period(self): """Orbital period, in minutes""" return 2 * pi / self.mean_motion 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=None): """Return a tuple in ECEF coordinate system""" return self.get_position(when_utc).position_ecef def get_eclipse_duration(self, when_utc=None, tolerance=1e-1): """Gets eclipse duration at given time, in minutes""" ecc = self.get_position(when_utc).osculating_elements[1] if ecc > tolerance: raise NotImplementedError("Non circular orbits are not supported") beta = self.get_beta(when_utc) return eclipse_duration(beta, self.period) def passes_over(self, location, when_utc, limit_date=None, max_elevation_gt=0, aos_at_dg=0, location_predictor_class=LocationPredictor, tolerance_s=1.0): return location_predictor_class(location, self, when_utc, limit_date, max_elevation_gt, aos_at_dg, tolerance_s=tolerance_s) def get_next_pass(self, location, when_utc=None, max_elevation_gt=5, aos_at_dg=0, limit_date=None, location_predictor_class=LocationPredictor, tolerance_s=1.0): """Return a PredictedPass instance with the data of the next pass over the given location location_llh: point on Earth we want to see from the satellite. when_utc: datetime UTC after which the pass is calculated, default to now. max_elevation_gt: filter passes with max_elevation under it. aos_at_dg: This is if we want to start the pass at a specific elevation. The next pass with a LOS strictly after when_utc will be returned, possibly the current pass. """ 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, location_predictor_class=location_predictor_class, tolerance_s=tolerance_s): return pass_ else: raise NotReachable('Propagation limit date exceeded') def eclipses_since(self, when_utc=None, limit_date=None): """ An iterator that yields all eclipses start and end times between when_utc and limit_date. The next eclipse with a end strictly after when_utc will be returned, possibly the current eclipse. The last eclipse returned starts before limit_date, but it can end strictly after limit_date. No circular orbits are not supported, and will raise NotImplementedError. """ def _get_illumination(t): my_start = start + dt.timedelta(seconds=t) result = get_satellite_minus_penumbra_verticals( self.get_only_position(my_start), my_start ) return result if when_utc is None: when_utc = dt.datetime.utcnow() orbital_period_s = self.period * 60 # A third of the orbit period is used as the base window of the search. # This window ensures the function get_satellite_minus_penumbra_verticals # will not have more than one local minimum (one in the illuminated phase and # the other in penumbra). base_search_window_s = orbital_period_s / 3 start = when_utc while limit_date is None or start < limit_date: # a minimum negative value is aproximatelly the middle point of the eclipse minimum_illumination = minimize_scalar( _get_illumination, bounds=(0, base_search_window_s), method="bounded", options={"xatol": 1e-2}, ) eclipse_center_candidate_delta_s = minimum_illumination.x # If found a minimum that is not illuminated, there is an eclipse here if _get_illumination(eclipse_center_candidate_delta_s) < 0: # Search now both zeros to get the start and end of the eclipse # We know that in (0, base_search_window_s) there is a minimum with negative value, # and also on the opposite side of the eclipse we expect sunlight, # therefore we already have two robust bracketing intervals eclipse_start_delta_s = brentq( _get_illumination, eclipse_center_candidate_delta_s - orbital_period_s / 2, eclipse_center_candidate_delta_s, xtol=1e-2, full_output=False, ) eclipse_end_delta_s = brentq( _get_illumination, eclipse_center_candidate_delta_s, eclipse_center_candidate_delta_s + orbital_period_s / 2, xtol=1e-2, full_output=False, ) eclipse_start = start + dt.timedelta(seconds=eclipse_start_delta_s) eclipse_end = start + dt.timedelta(seconds=eclipse_end_delta_s) yield eclipse_start, eclipse_end start = eclipse_end + dt.timedelta(seconds=base_search_window_s) else: start += dt.timedelta(seconds=base_search_window_s) class GPSPredictor(Predictor): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584957333.0 orbit-predictor-1.14.2/orbit_predictor/predictors/keplerian.py0000644000175000017500000001132100000000000025420 0ustar00juanlujuanlu00000000000000# 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 radians, sqrt from orbit_predictor.angles import ta_to_M, M_to_ta from orbit_predictor.constants import MU_E from orbit_predictor.keplerian import coe2rv from orbit_predictor.predictors import TLEPredictor from orbit_predictor.predictors.base import CartesianPredictor from orbit_predictor.utils import mean_motion 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(MU_E / 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): """Mean motion, in radians per minute""" return mean_motion(self._sma) * 60 @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() # Retrieve TLE position at given date as starting point pos = TLEPredictor(sate_id, source).get_position(date) return cls(*pos.osculating_elements, epoch=date) def propagate_eci(self, when_utc=None): """Return position and velocity in the given date using ECI coordinate system. """ if when_utc is None: when_utc = dt.datetime.utcnow() # 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1577435851.0 orbit-predictor-1.14.2/orbit_predictor/predictors/numerical.py0000644000175000017500000002264200000000000025435 0ustar00juanlujuanlu00000000000000# 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 try: from math import isclose except ImportError: def isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0): return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) import numpy as np from orbit_predictor.constants import OMEGA, MU_E, R_E_KM, J2, OMEGA_E 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, mean_motion def is_sun_synchronous(predictor, rtol=1e-3, epoch=None): """Check if predictor corresponds to Sun-synchronous orbit within tolerance. """ if epoch is None: epoch = dt.datetime.now() sma_km, ecc, inc_deg, *_ = predictor.get_position(epoch).osculating_elements p = sma_km * (1 - ecc ** 2) n = mean_motion(sma_km) raan_dot_sec = - 3 * n * R_E_KM ** 2 * J2 / (2 * p ** 2) * cos(radians(inc_deg)) return isclose(raan_dot_sec, OMEGA, rel_tol=rtol) 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 ) def repeating_ground_track_sma(orbits, days=1, *, ecc, inc_deg=0, tolerance=1e-8): """Computes semimajor axis for repeating ground track orbit. Parameters ---------- orbits : int Number of orbits in a given period. days : int, optional Number of days to cover the given orbits, default to 1. ecc : float Eccentricity. inc_deg : float, optional Inclination in degrees, default to 0 (equatorial). Returns ------- sma : float Semimajor axis. Notes ----- See Vallado "Fundamentals of Astrodynamics and Applications", 4th ed (2013) and Wertz et al. "Space Mission Engineering: The New SMAD" (2011). """ if not (isinstance(orbits, int) and isinstance(days, int)): raise ValueError("Number of orbits and number of days must be integer.") k = orbits / days n = k * OMEGA_E while True: sma_new = np.cbrt(MU_E * (1 / n) ** 2) p = sma_new * (1 - ecc ** 2) node_dot = - 3 * n * J2 / 2 * (R_E_KM / p) ** 2 * np.cos(np.radians(inc_deg)) argp_dot = 3 * n * J2 / 4 * (R_E_KM / p) ** 2 * (4 - 5 * np.sin(np.radians(inc_deg)) ** 2) M0_dot = ( 3 * n * J2 / 4 * (R_E_KM / p) ** 2 * np.sqrt(1 - ecc ** 2) * (2 - 3 * np.sin(np.radians(inc_deg)) ** 2) ) n = k * (OMEGA_E - node_dot) - (M0_dot + argp_dot) sma = np.cbrt(MU_E * (1 / n) ** 2) if np.isclose(sma, sma_new, rtol=tolerance): break return sma @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. Notes ----- See Vallado "Fundamentals of Astrodynamics and Applications", 4th ed (2013) section 11.4.1. """ 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 as e: raise InvalidOrbitError( "Cannot find Sun-synchronous orbit with given parameters" ) from e # 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) @classmethod def repeating_ground_track( cls, *, orbits, days=1, ecc=0.0, inc_deg=0, raan_deg=0, argp_deg=0, ta_deg=0 ): sma = repeating_ground_track_sma(orbits, days, ecc=ecc, inc_deg=inc_deg) return cls(sma, ecc, inc_deg, raan_deg, argp_deg, ta_deg, dt.datetime.now()) def propagate_eci(self, when_utc=None): """Return position and velocity in the given date using ECI coordinate system. """ if when_utc is None: when_utc = dt.datetime.utcnow() # 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608316542.0 orbit-predictor-1.14.2/orbit_predictor/predictors/pass_iterators.py0000664000175000017500000003230500000000000026517 0ustar00juanlujuanlu00000000000000import datetime as dt import logging from math import pi, acos, degrees, radians import warnings import numpy as np try: from scipy.signal import find_peaks from scipy.optimize import root_scalar, minimize_scalar except ImportError: warnings.warn( "scipy module was not found, some features may not work properly", ImportWarning, stacklevel=2, ) from orbit_predictor.exceptions import PropagationError from orbit_predictor.utils import ( cross_product, dot_product, reify, vector_diff, vector_norm, orbital_period, ) ONE_SECOND = dt.timedelta(seconds=1) logger = logging.getLogger(__name__) def round_datetime(dt_): return dt_ class BaseLocationPredictor: def __init__(self, location, predictor, start_date, limit_date=None, max_elevation_gt=0, aos_at_dg=0, tolerance_s=1.0): self.location = location self.predictor = predictor 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) self.tolerance_s = tolerance_s self.tolerance = dt.timedelta(seconds=tolerance_s) def __iter__(self): yield from self.iter_passes() def iter_passes(self): """Yields passes""" raise NotImplementedError class LocationPredictor(BaseLocationPredictor): """Predicts passes over a given location. Exposes an iterable interface. Notice that this algorithm is not fully exhaustive, see https://github.com/satellogic/orbit-predictor/issues/99 for details. """ def iter_passes(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.predictor.get_position(accuratepass.tca) return PredictedPass(self.location, self.predictor.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.predictor.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.predictor.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): return end - start <= self.tolerance @staticmethod def midpoint(start, end): """Returns the midpoint between two dates""" return start + (end - start) / 2 def _elevation_at(self, when_utc): position = self.predictor.get_only_position(when_utc) return self.location.elevation_for(position) 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 + self.tolerance) 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.predictor.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 SmartLocationPredictor(BaseLocationPredictor): """Predicts passes over a given location using a different algorithm. This uses a sampling interval of 3 minutes, which seems like a good compromise for Low-Earth Orbits. However, this means that, under certain circumstances, passes shorter than this duration could theoretically be missed. """ def iter_passes(self): # Explore all values of t every 3 minutes t_values = np.arange(0, (self.limit_date - self.start_date).total_seconds(), 180) elev_values = np.array([self._elevation(delta_seconds) for delta_seconds in t_values]) peaks_idx, _ = find_peaks(elev_values) for peak_idx in peaks_idx: if elev_values[peak_idx] < self.aos_at: continue else: t_approximate_tca = t_values[peak_idx] period_s = orbital_period(self.predictor.mean_motion) * 60 aos, tca, los, max_elevation = self._refine_pass(t_approximate_tca, period_s) yield PredictedPass( self.location, self.predictor.sate_id, max_elevation_deg=degrees(max_elevation), aos=aos, los=los, duration_s=(los - aos).total_seconds(), max_elevation_position=self.predictor.get_position(tca), max_elevation_date=tca, ) def _elevation(self, delta_seconds): when_utc = self.start_date + dt.timedelta(seconds=delta_seconds) position = self.predictor.get_only_position(when_utc) return self.location.elevation_for(position) def _refine_pass(self, t_approximate_tca, period_s): # AOS must be between half the previous period and the approximate TCA t_aos = root_scalar( lambda t: self._elevation(t) - self.aos_at, bracket=(t_approximate_tca - period_s / 2, t_approximate_tca), xtol=self.tolerance_s, method="brentq", ).root aos = self.start_date + dt.timedelta(seconds=t_aos) # LOS must be between the approximate TCA and half the next period t_los = root_scalar( lambda t: self._elevation(t) - self.aos_at, bracket=(t_approximate_tca, t_approximate_tca + period_s / 2), xtol=self.tolerance_s, method="brentq", ).root los = self.start_date + dt.timedelta(seconds=t_los) # Find date for maximum elevation between AOS and LOS # NOTE: If the tolerance is too loose, wrong results might be returned! res_tca = minimize_scalar( lambda t: -self._elevation(t), bracket=(t_aos, (t_los + t_aos) / 2, t_los), method="brent", ) t_tca = res_tca.x max_elevation = -res_tca.fun tca = self.start_date + dt.timedelta(seconds=t_tca) return aos, tca, los, max_elevation 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 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1605803751.0 orbit-predictor-1.14.2/orbit_predictor/predictors/tle.py0000664000175000017500000000016200000000000024235 0ustar00juanlujuanlu00000000000000from .accurate import HighAccuracyTLEPredictor # Backwards compatibility TLEPredictor = HighAccuracyTLEPredictor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608293420.0 orbit-predictor-1.14.2/orbit_predictor/sources.py0000664000175000017500000001512400000000000022762 0ustar00juanlujuanlu00000000000000# 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 requests from urllib import parse as urlparse from urllib.parse import urlencode from sgp4.api import Satrec from orbit_predictor.predictors import TLEPredictor from orbit_predictor.utils import datetime_from_jday 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): """Return a Predictor instance using the current storage.""" 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 line in tle: fd.write(line + "\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 found") 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-%dT%H:%M:%S") 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 = Satrec.twoline2rv(tle_lines[0], tle_lines[1]) db.add_tle( sgp4_sat.satnum, tuple(tle_lines), datetime_from_jday(sgp4_sat.jdsatepoch, sgp4_sat.jdsatepochF), ) predictor = TLEPredictor(sgp4_sat.satnum, db) return predictor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608293420.0 orbit-predictor-1.14.2/orbit_predictor/utils.py0000664000175000017500000003760000000000000022442 0ustar00juanlujuanlu00000000000000# 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 ( acos, asin, atan2, cos, degrees, floor, radians, sin, sqrt, tan, modf, pi ) import numpy as np from sgp4.api import jday as jday_jd_fr from sgp4.ext import jday, invjday from sgp4.propagation import gstime from .constants import AU, R_E_MEAN_KM, MU_E, ALPHA_UMB, ALPHA_PEN from .coordinate_systems import eci_to_radec, ecef_to_eci # 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 angle_between(a, b): """ Computes angle between two vectors in degrees. Notes ----- Naïve algorithm, see https://scicomp.stackexchange.com/q/27689/782. """ return degrees(np.arccos(dot_product(a, b) / (vector_norm(a) * vector_norm(b)))) 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: return np.dot(rot.T, vec.astype(rot.dtype)) else: 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): sun_eci = get_sun(when) # convert equatorial rectangular coordinates to RA and Decl: RA, _, _ = eci_to_radec(sun_eci) RA = degrees(RA) # 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 ltan_from_raan(when, raan=0): sun_eci = get_sun(when) # convert equatorial rectangular coordinates to RA and Decl: RA, _, _ = eci_to_radec(sun_eci) RA = degrees(RA) ltan = ((raan - RA) / 15.0 + 12.0) % 24 return ltan 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() jd = juliandate(timetuple_from_dt(when)) date = jd - DECEMBER_31TH_1999_MIDNIGHT_JD w, M, L, eccentricity, oblecl = _sun_mean_ecliptic_elements(date) sun_eci = _sun_eci(w, M, L, eccentricity, oblecl) # convert equatorial rectangular coordinates to RA and Decl: RA, DEC, r = eci_to_radec(sun_eci) RA = degrees(RA) DEC = degrees(DEC) # 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(DEC) y = sin_d(HA) * cos_d(DEC) z = sin_d(DEC) # 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 _sun_mean_ecliptic_elements(t_ut1): w = 282.9404 + 4.70935e-5 * t_ut1 # longitude of perihelion degrees eccentricity = 0.016709 - 1.151e-9 * t_ut1 # eccentricity M = (356.0470 + 0.9856002585 * t_ut1) % 360 # mean anomaly degrees L = w + M # Sun's mean longitude degrees oblecl = 23.4393 - 3.563e-7 * t_ut1 # Sun's obliquity of the ecliptic return w, M, L, eccentricity, oblecl def _sun_eci(w, M, L, eccentricity, oblecl): # 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 perihelion) 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 true 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) zequat = yeclip * sin_d(23.4406) + zeclip * cos_d(oblecl) return [xequat, yequat, zequat] def get_sun(when): """ Returns inertial position of the Sun, in au. """ jd = juliandate(timetuple_from_dt(when)) date = jd - DECEMBER_31TH_1999_MIDNIGHT_JD w, M, L, eccentricity, oblecl = _sun_mean_ecliptic_elements(date) sun_eci = _sun_eci(w, M, L, eccentricity, oblecl) return np.array(sun_eci) def get_shadow(r, when_utc): """ Gives illumination of Earth satellite (2 for illuminated, 1 for penumbra, 0 for umbra). Parameters ---------- r : numpy.ndarray or list ECEF vector pointing to the satellite in km. when_utc : datetime.datetime Time of calculation. """ gmst = gstime_from_datetime(when_utc) r_sun = get_sun(when_utc) * AU return shadow(r_sun, ecef_to_eci(r, gmst)) def shadow(r_sun, r, r_p=R_E_MEAN_KM): """ Gives illumination of Earth satellite (2 for illuminated, 1 for penumbra, 0 for umbra). Parameters ---------- r_sun : numpy.ndarray or list Vector pointing to the Sun in km. r : numpy.ndarray or list Vector pointing to the satellite in km. r_p : float, optional Radius of the planet, default to Earth WGS84. Notes ----- Algorithm 34 from Vallado, section 5.3. """ shadow_result = 2 if dot_product(r_sun, r) < 0: angle = angle_between(-r_sun, r) sat_horiz = vector_norm(r) * cos_d(angle) sat_vert = vector_norm(r) * sin_d(angle) x = r_p / sin(ALPHA_PEN) pen_vert = tan(ALPHA_PEN) * (x + sat_horiz) if sat_vert <= pen_vert: y = r_p / sin(ALPHA_UMB) umb_vert = tan(ALPHA_UMB) * (y - sat_horiz) if sat_vert <= umb_vert: shadow_result = 0 else: shadow_result = 1 return shadow_result def get_satellite_minus_penumbra_verticals(r, when_utc, r_p=R_E_MEAN_KM): """ Returns the continuous value of the difference between the satellite vertical and the penumbra vertical if the dot product of r_sun and r is negative, otherwise it returns a positive value in a continuous way. Parameters ---------- r : numpy.ndarray or list ECEF vector pointing to the satellite in km. when_utc : datetime.datetime Time of calculation. Notes ----- It is a rather artificial continuous function with positive values in illuminated phase, and negative values with penumbra or umbra. The zeros of the function are only in the transitions from illuminated to penumbra (when going from positive to negative) and from penumbra to illuminated (when going from negative to positive). BEWARE: it can have local minimuns with positive values. Works for highly elliptical orbits too. The internals are the same as shadow function based on Algorithm 34 from Vallado, section 5.3. """ gmst = gstime_from_datetime(when_utc) r_sun = get_sun(when_utc) * AU r = ecef_to_eci(r, gmst) if dot_product(r_sun, r) >= 0: # The result of simplifying the sat_vert - pen_vert calculation # in the case of dot_product(r_sun, r) == 0, i.e., angle == pi / 2. return (vector_norm(np.array(r)) - r_p / cos(ALPHA_PEN)) angle = angle_between(-r_sun, r) sat_horiz = vector_norm(r) * cos_d(angle) sat_vert = vector_norm(r) * sin_d(angle) x = r_p / sin(ALPHA_PEN) pen_vert = tan(ALPHA_PEN) * (x + sat_horiz) return sat_vert - pen_vert def eclipse_duration(beta, period, r_p=R_E_MEAN_KM): """Eclipse duration, in minutes""" # Based on Vallado 4th ed., pp. 305 # Circular orbital radius corresponding to given period r = np.cbrt(MU_E / (4 * pi ** 2) * (period * 60) ** 2) # We clip the argument of acos between -1 and 1 # to return a eclipse duration of 0 when it is out of range return acos( np.clip(sqrt(1 - (r_p / r) ** 2) / cos(radians(beta)), -1, 1) ) * period / pi 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 sidereal time GMST0 = ((sun_lon + 180) % 360) / 15 return GMST0 + UTH + local_lon / 15 def gstime_from_datetime(when_utc): timetuple = timetuple_from_dt(when_utc) return gstime(jday(*timetuple)) def jday_from_datetime(when_utc): return jday_jd_fr( when_utc.year, when_utc.month, when_utc.day, when_utc.hour, when_utc.minute, when_utc.second + when_utc.microsecond * 1e-6 ) def datetime_from_jday(jd, fr): year, mon, day, hr, minute, sec_float = invjday(jd + fr) sec = int(sec_float) microsec = int((sec_float - sec) * 1e6) return dt.datetime(year, mon, day, hr, minute, sec, microsec) 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 timetuple_from_dt(when_utc): 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 timetuple def mean_motion(sma_km): """Mean motion, in radians per second""" return sqrt(MU_E / sma_km ** 3) # rad / s def orbital_period(mean_motion): """Orbital period, in minutes""" return 1 / mean_motion * 2 * pi def unkozai(no_kozai, ecco, inclo, whichconst): """Undo Kozai transformation.""" _, _, _, xke, j2, _, _, _ = whichconst ak = pow(xke / no_kozai, 2.0 / 3.0) d1 = 0.75 * j2 * (3.0 * cos(inclo)**2 - 1.0) / (1.0 - ecco**2)**(3/2) del_ = d1 / ak ** 2 adel = ak * (1.0 - del_ * del_ - del_ * (1.0 / 3.0 + 134.0 * del_ * del_ / 81.0)) return no_kozai / (1.0 + d1/adel**2) 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379348.0 orbit-predictor-1.14.2/orbit_predictor/version.py0000664000175000017500000000010300000000000022753 0ustar00juanlujuanlu00000000000000# https://www.python.org/dev/peps/pep-0440/ __version__ = '1.14.2' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1608379368.8700297 orbit-predictor-1.14.2/orbit_predictor.egg-info/0000775000175000017500000000000000000000000022414 5ustar00juanlujuanlu00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379368.0 orbit-predictor-1.14.2/orbit_predictor.egg-info/PKG-INFO0000644000175000017500000001123700000000000023513 0ustar00juanlujuanlu00000000000000Metadata-Version: 2.1 Name: orbit-predictor Version: 1.14.2 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://github.com/satellogic/orbit-predictor/workflows/Python%20package/badge.svg :target: https://github.com/satellogic/orbit-predictor/actions .. 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) `_ All 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: fast Provides-Extra: dev ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379368.0 orbit-predictor-1.14.2/orbit_predictor.egg-info/SOURCES.txt0000644000175000017500000000152600000000000024302 0ustar00juanlujuanlu00000000000000README.rst setup.cfg setup.py orbit_predictor/__init__.py orbit_predictor/angles.py orbit_predictor/constants.py orbit_predictor/coordinate_systems.py orbit_predictor/exceptions.py orbit_predictor/groundtrack.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/_minimize.py orbit_predictor/predictors/accurate.py orbit_predictor/predictors/base.py orbit_predictor/predictors/keplerian.py orbit_predictor/predictors/numerical.py orbit_predictor/predictors/pass_iterators.py orbit_predictor/predictors/tle.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379368.0 orbit-predictor-1.14.2/orbit_predictor.egg-info/dependency_links.txt0000644000175000017500000000000100000000000026460 0ustar00juanlujuanlu00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379368.0 orbit-predictor-1.14.2/orbit_predictor.egg-info/requires.txt0000644000175000017500000000024500000000000025013 0ustar00juanlujuanlu00000000000000numpy>=1.8.2 sgp4>=2.5 requests [dev] hypothesis flake8 hypothesis[datetime] mock logassert pytest pytest-cov pytest-benchmark pytz [fast] numba>=0.38 scipy>=0.16 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608379368.0 orbit-predictor-1.14.2/orbit_predictor.egg-info/top_level.txt0000644000175000017500000000002000000000000025134 0ustar00juanlujuanlu00000000000000orbit_predictor ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1608379368.8700297 orbit-predictor-1.14.2/setup.cfg0000644000175000017500000000025400000000000017350 0ustar00juanlujuanlu00000000000000[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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608293420.0 orbit-predictor-1.14.2/setup.py0000775000175000017500000000321500000000000017246 0ustar00juanlujuanlu00000000000000#!/usr/bin/env python3 import os.path from setuptools import setup, find_packages # Copyright 2017-2020 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>=2.5', '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", )