├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── clean.sh ├── docs ├── Makefile ├── api.rst ├── conf.py ├── index.rst ├── make.bat └── requirements.txt ├── setup.cfg ├── setup.py ├── test.sh ├── tests ├── conftest.py └── test_tle.py └── tletools ├── __init__.py ├── pandas.py ├── tle.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.7 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: pip 25 | path: . 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Federico Stra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLE-tools 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/TLE-tools) 4 | ![PyPI - License](https://img.shields.io/pypi/l/TLE-tools) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/TLE-tools) 6 | ![PyPI - Wheel](https://img.shields.io/pypi/wheel/TLE-tools) 7 | 8 | `TLE-tools` is a small library to work with [two-line element 9 | set](https://en.wikipedia.org/wiki/Two-line_element_set) files. 10 | 11 | ## Purpose 12 | 13 | The purpose of the library is to parse TLE sets into convenient `TLE` objects, 14 | load entire TLE set files into `pandas.DataFrame`'s, convert `TLE` objects into 15 | `poliastro.twobody.Orbit`'s, and more. 16 | 17 | From [Wikipedia](https://en.wikipedia.org/wiki/Two-line_element_set): 18 | 19 | > A two-line element set (TLE) is a data format encoding a list of orbital 20 | elements of an Earth-orbiting object for a given point in time, the epoch. 21 | The TLE data representation is specific to the [simplified perturbations 22 | models](https://en.wikipedia.org/wiki/Simplified_perturbations_models) (SGP, 23 | SGP4, SDP4, SGP8 and SDP8), so any algorithm using a TLE as a data source must 24 | implement one of the SGP models to correctly compute the state at a time of 25 | interest. TLEs can describe the trajectories only of Earth-orbiting objects. 26 | 27 | Example: 28 | 29 | ``` 30 | ISS (ZARYA) 31 | 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 32 | 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805 33 | ``` 34 | 35 | Here is a minimal example on how to load the previous TLE: 36 | 37 | ```python 38 | from tletools import TLE 39 | 40 | tle_string = """ 41 | ISS (ZARYA) 42 | 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 43 | 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805 44 | """ 45 | 46 | tle_lines = tle_string.strip().splitlines() 47 | 48 | tle = TLE.from_lines(*tle_lines) 49 | ``` 50 | 51 | Then `tle` is: 52 | 53 | ```python 54 | TLE(name='ISS (ZARYA)', norad='25544', classification='U', int_desig='98067A', 55 | epoch_year=2019, epoch_day=249.04864348, dn_o2=1.909e-05, ddn_o6=0.0, bstar=4.0858e-05, 56 | set_num=999, inc=51.6464, raan=320.1755, ecc=0.0007999, argp=10.9066, M=53.2893, 57 | n=15.50437522, rev_num=18780) 58 | ``` 59 | 60 | and you can then access its attributes like `t.argp`, `t.epoch`... 61 | 62 | ### TLE format specification 63 | 64 | Some more or less complete TLE format specifications can be found on the following websites: 65 | 66 | - [Wikipedia](https://en.wikipedia.org/wiki/Two-line_element_set#Format) 67 | - [NASA](https://spaceflight.nasa.gov/realdata/sightings/SSapplications/Post/JavaSSOP/SSOP_Help/tle_def.html) 68 | - [CelesTrak](https://celestrak.com/columns/v04n03/) 69 | - [Space-Track](https://www.space-track.org/documentation#tle) 70 | 71 | ## Installation 72 | 73 | Install and update using [pip](https://pip.pypa.io/en/stable/): 74 | ```bash 75 | pip install -U TLE-tools 76 | ``` 77 | 78 | ## Links 79 | 80 | - Website: https://federicostra.github.io/tletools 81 | - Documentation: https://tletools.readthedocs.io 82 | - Releases: https://pypi.org/project/TLE-tools 83 | - Code: https://github.com/FedericoStra/tletools 84 | - Issue tracker: https://github.com/FedericoStra/tletools/issues 85 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -r .pytest_cache/ TLE_tools.egg-info/ build/ docs/_build 4 | find . -name __pycache__ -exec rm -r {} + 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Documentation 4 | ***************** 5 | 6 | This part of the documentation covers all the interfaces of :mod:`tletools`. 7 | For guides on how to use them, pleas consult the tutorials. 8 | 9 | TLE Classes 10 | =========== 11 | 12 | .. automodule:: tletools.tle 13 | 14 | Interoperability 15 | ================ 16 | 17 | Pandas 18 | ------ 19 | 20 | .. automodule:: tletools.pandas 21 | :members: 22 | 23 | Poliastro 24 | --------- 25 | 26 | Use the :meth:`.TLE.to_orbit` method like this:: 27 | 28 | >>> tle_string = """ISS (ZARYA) 29 | ... 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 30 | ... 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805""" 31 | >>> tle = TLE.from_lines(*tle_string.splitlines()) 32 | >>> tle.to_orbit() 33 | 6788 x 6799 km x 51.6 deg (GCRS) orbit around Earth (♁) at epoch 2019-09-06T01:10:02.796672000 (UTC) 34 | 35 | Utils 36 | ===== 37 | 38 | .. automodule:: tletools.utils 39 | :members: 40 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'TLE-tools' 21 | copyright = '2019, Federico Stra' 22 | author = 'Federico Stra' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.2.4' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | master_doc = 'index' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.intersphinx', 38 | 'sphinx.ext.viewcode', 39 | # 'sphinx.ext.napoleon', 40 | # 'numpydoc', 41 | ] 42 | 43 | # Order of the members: 'alphabetical', 'bysource', 'groupwise' 44 | autodoc_member_order = 'groupwise' 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 53 | 54 | intersphinx_mapping = { 55 | 'python': ('https://docs.python.org/3', None), 56 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 57 | 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), 58 | 'astropy': ('https://docs.astropy.org/en/stable/', None), 59 | 'poliastro': ('https://docs.poliastro.space/en/stable/', None), 60 | } 61 | 62 | # -- Options for HTML output ------------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | # 67 | # html_theme = 'alabaster' 68 | html_theme = 'sphinx_rtd_theme' 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | html_static_path = ['_static'] 74 | 75 | # Theme options are theme-specific and customize the look and feel of a theme 76 | # further. For a list of options available for each theme, see the 77 | # documentation. 78 | html_theme_options = { 79 | "show_powered_by": False, 80 | "github_user": "FedericoStra", 81 | "github_repo": "tletools", 82 | "github_banner": True, 83 | } 84 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. TLE-tools documentation master file, created by 2 | sphinx-quickstart on Fri Sep 6 16:54:20 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to TLE-tools's documentation! 7 | ===================================== 8 | 9 | **TLE-tools** is a small library to work with `two-line element set`_ files. 10 | 11 | .. _`two-line element set`: https://en.wikipedia.org/wiki/Two-line_element_set 12 | 13 | Purpose 14 | ------- 15 | 16 | The purpose of the library is to parse TLE sets into convenient 17 | :class:`TLE ` objects, load entire TLE set files into 18 | :class:`pandas.DataFrame`'s, convert :class:`TLE ` objects 19 | into :class:`poliastro.twobody.orbit.Orbit`'s, and more. 20 | 21 | From Wikipedia_: 22 | 23 | A two-line element set (TLE) is a data format encoding a list of orbital 24 | elements of an Earth-orbiting object for a given point in time, the epoch. 25 | The TLE data representation is specific to the 26 | `simplified perturbations models`_ (SGP, SGP4, SDP4, SGP8 and SDP8), 27 | so any algorithm using a TLE as a data source must implement one of the SGP 28 | models to correctly compute the state at a time of interest. TLEs can 29 | describe the trajectories only of Earth-orbiting objects. 30 | 31 | .. _Wikipedia: https://en.wikipedia.org/wiki/Two-line_element_set 32 | .. _simplified perturbations models: https://en.wikipedia.org/wiki/Simplified_perturbations_models 33 | 34 | Here is an example TLE:: 35 | 36 | ISS (ZARYA) 37 | 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 38 | 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805 39 | 40 | Here is a minimal example on how to load the previous TLE:: 41 | 42 | from tletools import TLE 43 | 44 | tle_string = """ 45 | ISS (ZARYA) 46 | 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 47 | 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805 48 | """ 49 | 50 | tle_lines = tle_string.strip().splitlines() 51 | 52 | t = TLE.from_lines(*tle_lines) 53 | 54 | Then ``t`` is:: 55 | 56 | TLE(name='ISS (ZARYA)', norad='25544', classification='U', int_desig='98067A', 57 | epoch_year=2019, epoch_day=249.04864348, dn_o2=1.909e-05, ddn_o6=0.0, bstar=4.0858e-05, 58 | set_num=999, inc=51.6464, raan=320.1755, ecc=0.0007999, argp=10.9066, M=53.2893, 59 | n=15.50437522, rev_num=18780) 60 | 61 | and you can then access its attributes like ``t.argp``, ``t.epoch``... 62 | 63 | Installation 64 | ------------ 65 | 66 | Install and update using pip_:: 67 | 68 | pip install -U TLE-tools 69 | 70 | .. _pip: https://pip.pypa.io/en/stable/ 71 | 72 | Links 73 | ----- 74 | 75 | - Website: https://federicostra.github.io/tletools 76 | - Documentation: https://tletools.readthedocs.io 77 | - Releases: https://pypi.org/project/TLE-tools 78 | - Code: https://github.com/FedericoStra/tletools 79 | - Issue tracker: https://github.com/FedericoStra/tletools/issues 80 | 81 | 82 | Indices and tables 83 | ================== 84 | 85 | * :ref:`genindex` 86 | * :ref:`modindex` 87 | * :ref:`search` 88 | 89 | .. toctree:: 90 | :maxdepth: 2 91 | :caption: Contents: 92 | 93 | API Documentation 94 | ----------------- 95 | 96 | If you are looking for information on a specific function, class, or method, 97 | this part of the documentation is for you. 98 | 99 | .. toctree:: 100 | :maxdepth: 2 101 | 102 | api 103 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=2.2.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.4 3 | 4 | [bdist_wheel] 5 | universal = true 6 | 7 | [metadata] 8 | license_file = LICENSE 9 | 10 | [flake8] 11 | select = B, E, F, W, C90 12 | max-line-length = 95 13 | 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | 5 | with open("README.md") as fp: 6 | long_description = fp.read() 7 | 8 | setup( 9 | name='TLE-tools', 10 | version='0.2.4', 11 | description='Library to work with two-line element set files', 12 | license='MIT', 13 | author='Federico Stra', 14 | author_email='stra.federico@gmail.com', 15 | url='https://federicostra.github.io/tletools', 16 | project_urls={ 17 | 'Documentation': 'https://tletools.readthedocs.io', 18 | 'Code': 'https://github.com/FedericoStra/tletools', 19 | 'Issue tracker': 'https://github.com/FedericoStra/tletools/issues', 20 | }, 21 | packages=find_packages(include='tletools.*'), 22 | python_requires='>=3.4', 23 | install_requires=[ 24 | 'attrs>=19.0.0', 25 | 'numpy>=1.16.0', 26 | 'pandas>=0.24.0', 27 | 'astropy>=3.2.0', 28 | 'poliastro>=0.14.0', 29 | ], 30 | tests_require=[ 31 | 'flake8>=3.7.0', 32 | 'pytest>=5.0.0', 33 | ], 34 | extras_require={ 35 | 'dev': [ 36 | 'pytest', 37 | 'coverage', 38 | 'tox', 39 | 'sphinx', 40 | 'pallets-sphinx-themes', 41 | 'sphinxcontrib-log-cabinet', 42 | 'sphinx-issues', 43 | ], 44 | 'docs': [ 45 | 'sphinx', 46 | 'sphinx-rtd-theme', 47 | ], 48 | }, 49 | long_description=long_description, 50 | long_description_content_type='text/markdown', 51 | classifiers=[ 52 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 53 | 'Development Status :: 3 - Alpha', 54 | 'Framework :: IPython', 55 | 'Framework :: Jupyter', 56 | 'Intended Audience :: Developers', 57 | 'Intended Audience :: Education', 58 | 'Intended Audience :: Science/Research', 59 | 'License :: OSI Approved :: MIT License', 60 | 'Natural Language :: English', 61 | 'Operating System :: Unix', 62 | 'Operating System :: POSIX', 63 | 'Operating System :: Microsoft :: Windows', 64 | 'Programming Language :: Python', 65 | 'Programming Language :: Python :: 3', 66 | 'Programming Language :: Python :: 3.4', 67 | 'Programming Language :: Python :: 3.5', 68 | 'Programming Language :: Python :: 3.6', 69 | 'Programming Language :: Python :: 3.7', 70 | 'Programming Language :: Python :: Implementation :: CPython', 71 | 'Programming Language :: Python :: Implementation :: PyPy', 72 | 'Topic :: Scientific/Engineering', 73 | 'Topic :: Scientific/Engineering :: Visualization', 74 | ], 75 | ) 76 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest 4 | pytest --doctest-modules tletools 5 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tletools import TLE 4 | 5 | 6 | @pytest.fixture 7 | def tle_string(): 8 | return """ISS (ZARYA) 9 | 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 10 | 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805""" 11 | 12 | @pytest.fixture 13 | def tle_string2(): 14 | # This TLE tests high mean anomaly value. 15 | return """NOAA 18 16 | 1 28654U 05018A 20098.54037539 .00000075 00000-0 65128-4 0 9992 17 | 2 28654 99.0522 154.2797 0015184 73.2195 287.0641 14.12501077766909""" 18 | 19 | @pytest.fixture 20 | def tle_lines(tle_string): 21 | return tle_string.splitlines() 22 | 23 | @pytest.fixture 24 | def tle_lines2(tle_string2): 25 | return tle_string2.splitlines() 26 | 27 | @pytest.fixture 28 | def tle(): 29 | return TLE('ISS (ZARYA)', '25544', 'U', '98067A', 30 | # year, day 31 | 2019, 249.04864348, 32 | # dn_o2, ddn_o6, bstar, set_num 33 | 1.909e-05, 0.0, 4.0858e-05, 999, 34 | # inc, raan, ecc, argp 35 | 51.6464, 320.1755, 0.0007999, 10.9066, 36 | # M, n, rev_num 37 | 53.2893, 15.50437522, 18780) 38 | -------------------------------------------------------------------------------- /tests/test_tle.py: -------------------------------------------------------------------------------- 1 | from tletools import TLE 2 | from tletools.tle import TLEu 3 | 4 | import astropy.units as u 5 | 6 | def test_from_lines(tle_lines): 7 | t = TLE.from_lines(*tle_lines) 8 | assert isinstance(t, TLE) 9 | 10 | def test_from_lines_high_M(tle_lines2): 11 | t = TLE.from_lines(*tle_lines2) 12 | assert isinstance(t, TLE) 13 | 14 | def test_from_lines_with_units(tle_lines): 15 | t = TLEu.from_lines(*tle_lines) 16 | assert isinstance(t, TLEu) 17 | 18 | 19 | def test_to_orbit(tle): 20 | assert tle.to_orbit().ecc == tle.ecc 21 | assert float(tle.to_orbit().inc / u.deg) == tle.inc 22 | 23 | 24 | def test_asdict(tle): 25 | assert type(tle)(**tle.asdict()) == tle 26 | 27 | 28 | def test_astuple(tle): 29 | assert type(tle)(*tle.astuple()) == tle 30 | -------------------------------------------------------------------------------- /tletools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | **TLE-tools** is a small library to work with `two-line element set`_ files. 3 | 4 | .. _`two-line element set`: https://en.wikipedia.org/wiki/Two-line_element_set 5 | """ 6 | 7 | from .tle import TLE 8 | from .pandas import load_dataframe, add_epoch 9 | -------------------------------------------------------------------------------- /tletools/pandas.py: -------------------------------------------------------------------------------- 1 | """ 2 | The module :mod:`tletools.pandas` provides convenience functions to load 3 | two-line element set files into :class:`pandas.DataFrame`'s.' 4 | 5 | Given a file ``oneweb.txt`` with the following contents:: 6 | 7 | ONEWEB-0012 8 | 1 44057U 19010A 19290.71624163 .00000233 00000-0 58803-3 0 9997 9 | 2 44057 87.9055 22.9851 0002022 94.9226 265.2135 13.15296315 30734 10 | ONEWEB-0010 11 | 1 44058U 19010B 19290.71785289 .00000190 00000-0 47250-3 0 9991 12 | 2 44058 87.9054 22.9846 0002035 97.1333 263.0028 13.15294565 30783 13 | ONEWEB-0008 14 | 1 44059U 19010C 19290.86676214 -.00000034 00000-0 -12353-3 0 9990 15 | 2 44059 87.9055 22.9563 0001967 95.9628 264.1726 13.15300216 30897 16 | ONEWEB-0007 17 | 1 44060U 19010D 19290.87154896 .00000182 00000-0 45173-3 0 9998 18 | 2 44060 87.9067 22.9618 0001714 97.9802 262.1523 13.15299021 30927 19 | ONEWEB-0006 20 | 1 44061U 19010E 19290.72095254 .00000179 00000-0 44426-3 0 9991 21 | 2 44061 87.9066 22.9905 0001931 95.0539 265.0811 13.15294588 30940 22 | ONEWEB-0011 23 | 1 44062U 19010F 19291.17894923 .00000202 00000-0 50450-3 0 9993 24 | 2 44062 87.9056 22.8943 0002147 94.8298 265.3077 13.15300820 31002 25 | 26 | you can load the TLEs into a :class:`pandas.DataFrame` by using 27 | 28 | >>> load_dataframe("oneweb.txt") # doctest: +SKIP 29 | name norad classification int_desig epoch_year epoch_day dn_o2 ddn_o6 bstar set_num inc raan ecc argp M n rev_num epoch 30 | 0 ONEWEB-0012 44057 U 19010A 2019 290.716242 2.330000e-06 0.0 0.000588 999 87.9055 22.9851 0.000202 94.9226 265.2135 13.152963 3073 2019-10-17 17:11:23.276832 31 | 1 ONEWEB-0010 44058 U 19010B 2019 290.717853 1.900000e-06 0.0 0.000472 999 87.9054 22.9846 0.000204 97.1333 263.0028 13.152946 3078 2019-10-17 17:13:42.489696 32 | 2 ONEWEB-0008 44059 U 19010C 2019 290.866762 -3.400000e-07 0.0 -0.000124 999 87.9055 22.9563 0.000197 95.9628 264.1726 13.153002 3089 2019-10-17 20:48:08.248896 33 | 3 ONEWEB-0007 44060 U 19010D 2019 290.871549 1.820000e-06 0.0 0.000452 999 87.9067 22.9618 0.000171 97.9802 262.1523 13.152990 3092 2019-10-17 20:55:01.830144 34 | 4 ONEWEB-0006 44061 U 19010E 2019 290.720953 1.790000e-06 0.0 0.000444 999 87.9066 22.9905 0.000193 95.0539 265.0811 13.152946 3094 2019-10-17 17:18:10.299456 35 | 5 ONEWEB-0011 44062 U 19010F 2019 291.178949 2.020000e-06 0.0 0.000504 999 87.9056 22.8943 0.000215 94.8298 265.3077 13.153008 3100 2019-10-18 04:17:41.213472 36 | 37 | You can also load multiple files into a single :class:`pandas.DataFrame` with 38 | 39 | >>> from glob import glob 40 | >>> load_dataframe(glob("*.txt")) # doctest: +SKIP 41 | """ 42 | 43 | import pandas as pd 44 | 45 | from .tle import TLE 46 | from .utils import partition, dt_dt64_Y, dt_td64_us 47 | 48 | 49 | def load_dataframe(filename, *, computed=False, epoch=True): 50 | """Load multiple TLEs from one or more files and return a :class:`pandas.DataFrame`. 51 | 52 | :param filename: A single filename (:class:`str`) or an iterable producing filenames. 53 | :type filename: str or iterable 54 | :returns: A :class:`pandas.DataFrame` with all the loaded TLEs. 55 | 56 | **Examples** 57 | 58 | >>> load_dataframe("oneweb.txt") # doctest: +SKIP 59 | 60 | >>> load_dataframe(["oneweb.txt", "starlink.txt"]) # doctest: +SKIP 61 | 62 | >>> from glob import glob 63 | >>> load_dataframe(glob("*.txt")) # doctest: +SKIP 64 | """ 65 | if isinstance(filename, str): 66 | with open(filename) as fp: 67 | df = pd.DataFrame(TLE.from_lines(*l012).asdict(computed=computed) 68 | for l012 in partition(fp, 3)) 69 | if epoch: 70 | add_epoch(df) 71 | return df 72 | else: 73 | df = pd.concat( 74 | [load_dataframe(fn, computed=computed, epoch=False) for fn in filename], 75 | ignore_index=True, join='inner', copy=False) 76 | df.drop_duplicates(inplace=True) 77 | df.reset_index(drop=True, inplace=True) 78 | add_epoch(df) 79 | return df 80 | 81 | 82 | def add_epoch(df): 83 | """Add a column ``'epoch'`` to a dataframe. 84 | 85 | `df` must have columns ``'epoch_year'`` and ``'epoch_day'``, from which the 86 | column ``'epoch'`` is computed. 87 | 88 | :param pandas.DataFrame df: :class:`pandas.DataFrame` instance to modify. 89 | 90 | **Example** 91 | 92 | >>> from pandas import DataFrame 93 | >>> df = DataFrame([[2018, 31.2931], [2019, 279.3781]], 94 | ... columns=['epoch_year', 'epoch_day']) 95 | >>> add_epoch(df) 96 | >>> df 97 | epoch_year epoch_day epoch 98 | 0 2018 31.2931 2018-01-31 07:02:03.840 99 | 1 2019 279.3781 2019-10-06 09:04:27.840 100 | """ 101 | years = (df.epoch_year.values - 1970).astype(dt_dt64_Y) 102 | days = ((df.epoch_day.values - 1) * 86400 * 1000000).astype(dt_td64_us) 103 | df['epoch'] = years + days 104 | -------------------------------------------------------------------------------- /tletools/tle.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The module :mod:`tletools.tle` defines the classes :class:`TLE` and :class:`TLEu`. 3 | 4 | The library offers two classes to represent a single TLE. 5 | There is the unitless version :class:`TLE`, whose attributes are expressed in the same units 6 | that are used in the TLE format, and there is the unitful version :class:`TLEu`, 7 | whose attributes are quantities (:class:`astropy.units.Quantity`), a type able to represent 8 | a value with an associated unit taken from :mod:`astropy.units`. 9 | Here is a short example of how you can use them: 10 | 11 | >>> tle_string = """ 12 | ... ISS (ZARYA) 13 | ... 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 14 | ... 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805 15 | ... """ 16 | >>> tle_lines = tle_string.strip().splitlines() 17 | >>> TLE.from_lines(*tle_lines) 18 | TLE(name='ISS (ZARYA)', norad='25544', ..., n=15.50437522, rev_num=18780) 19 | 20 | .. autoclass:: TLE 21 | :members: 22 | .. autoclass:: TLEu 23 | ''' 24 | 25 | import attr 26 | 27 | import numpy as np 28 | import astropy.units as u 29 | from astropy.time import Time 30 | 31 | # Maybe remove them from here? 32 | from poliastro.twobody import Orbit as _Orbit 33 | from poliastro.bodies import Earth as _Earth 34 | 35 | from .utils import partition, rev as u_rev, M_to_nu as _M_to_nu 36 | 37 | 38 | DEG2RAD = np.pi / 180. 39 | RAD2DEG = 180. / np.pi 40 | 41 | 42 | def _conv_year(s): 43 | """Interpret a two-digit year string.""" 44 | if isinstance(s, int): 45 | return s 46 | y = int(s) 47 | return y + (1900 if y >= 57 else 2000) 48 | 49 | 50 | def _parse_decimal(s): 51 | """Parse a floating point with implicit leading dot. 52 | 53 | >>> _parse_decimal('378') 54 | 0.378 55 | """ 56 | return float('.' + s) 57 | 58 | 59 | def _parse_float(s): 60 | """Parse a floating point with implicit dot and exponential notation. 61 | 62 | >>> _parse_float(' 12345-3') 63 | 0.00012345 64 | >>> _parse_float('+12345-3') 65 | 0.00012345 66 | >>> _parse_float('-12345-3') 67 | -0.00012345 68 | """ 69 | return float(s[0] + '.' + s[1:6] + 'e' + s[6:8]) 70 | 71 | 72 | @attr.s 73 | class TLE: 74 | """Data class representing a single TLE. 75 | 76 | A two-line element set (TLE) is a data format encoding a list of orbital 77 | elements of an Earth-orbiting object for a given point in time, the epoch. 78 | 79 | All the attributes parsed from the TLE are expressed in the same units that 80 | are used in the TLE format. 81 | 82 | :ivar str name: 83 | Name of the satellite. 84 | :ivar str norad: 85 | NORAD catalog number (https://en.wikipedia.org/wiki/Satellite_Catalog_Number). 86 | :ivar str classification: 87 | 'U', 'C', 'S' for unclassified, classified, secret. 88 | :ivar str int_desig: 89 | International designator (https://en.wikipedia.org/wiki/International_Designator), 90 | :ivar int epoch_year: 91 | Year of the epoch. 92 | :ivar float epoch_day: 93 | Day of the year plus fraction of the day. 94 | :ivar float dn_o2: 95 | First time derivative of the mean motion divided by 2. 96 | :ivar float ddn_o6: 97 | Second time derivative of the mean motion divided by 6. 98 | :ivar float bstar: 99 | BSTAR coefficient (https://en.wikipedia.org/wiki/BSTAR). 100 | :ivar int set_num: 101 | Element set number. 102 | :ivar float inc: 103 | Inclination. 104 | :ivar float raan: 105 | Right ascension of the ascending node. 106 | :ivar float ecc: 107 | Eccentricity. 108 | :ivar float argp: 109 | Argument of perigee. 110 | :ivar float M: 111 | Mean anomaly. 112 | :ivar float n: 113 | Mean motion. 114 | :ivar int rev_num: 115 | Revolution number. 116 | """ 117 | 118 | # name of the satellite 119 | name = attr.ib(converter=str.strip) 120 | # NORAD catalog number (https://en.wikipedia.org/wiki/Satellite_Catalog_Number) 121 | norad = attr.ib(converter=str.strip) 122 | classification = attr.ib() 123 | int_desig = attr.ib(converter=str.strip) 124 | epoch_year = attr.ib(converter=_conv_year) 125 | epoch_day = attr.ib() 126 | dn_o2 = attr.ib() 127 | ddn_o6 = attr.ib() 128 | bstar = attr.ib() 129 | set_num = attr.ib(converter=int) 130 | inc = attr.ib() 131 | raan = attr.ib() 132 | ecc = attr.ib() 133 | argp = attr.ib() 134 | M = attr.ib() 135 | n = attr.ib() 136 | rev_num = attr.ib(converter=int) 137 | 138 | def __attrs_post_init__(self): 139 | self._epoch = None 140 | self._a = None 141 | self._nu = None 142 | 143 | @property 144 | def epoch(self): 145 | """Epoch of the TLE, as an :class:`astropy.time.Time` object.""" 146 | if self._epoch is None: 147 | year = np.datetime64(self.epoch_year - 1970, 'Y') 148 | day = np.timedelta64(int((self.epoch_day - 1) * 86400 * 10**6), 'us') 149 | self._epoch = Time(year + day, format='datetime64', scale='utc') 150 | return self._epoch 151 | 152 | @property 153 | def a(self): 154 | """Semi-major axis.""" 155 | if self._a is None: 156 | self._a = (_Earth.k.value / (self.n * np.pi / 43200) ** 2) ** (1/3) / 1000 157 | return self._a 158 | 159 | @property 160 | def nu(self): 161 | """True anomaly.""" 162 | if self._nu is None: 163 | # Make sure the mean anomaly is between -pi and pi 164 | M = ((self.M + 180) % 360 - 180) * DEG2RAD 165 | self._nu = _M_to_nu(M, self.ecc) * RAD2DEG 166 | return self._nu 167 | 168 | @classmethod 169 | def from_lines(cls, name, line1, line2): 170 | """Parse a TLE from its constituent lines. 171 | 172 | All the attributes parsed from the TLE are expressed in the same units that 173 | are used in the TLE format. 174 | """ 175 | return cls( 176 | name=name, 177 | norad=line1[2:7], 178 | classification=line1[7], 179 | int_desig=line1[9:17], 180 | epoch_year=line1[18:20], 181 | epoch_day=float(line1[20:32]), 182 | dn_o2=float(line1[33:43]), 183 | ddn_o6=_parse_float(line1[44:52]), 184 | bstar=_parse_float(line1[53:61]), 185 | set_num=line1[64:68], 186 | inc=float(line2[8:16]), 187 | raan=float(line2[17:25]), 188 | ecc=_parse_decimal(line2[26:33]), 189 | argp=float(line2[34:42]), 190 | M=float(line2[43:51]), 191 | n=float(line2[52:63]), 192 | rev_num=line2[63:68]) 193 | 194 | @classmethod 195 | def load(cls, filename): 196 | """Load multiple TLEs from a file.""" 197 | if isinstance(filename, str): 198 | with open(filename) as fp: 199 | return [cls.from_lines(*l012) 200 | for l012 in partition(fp, 3)] 201 | else: 202 | return [tle for fn in filename for tle in cls.load(fn)] 203 | 204 | @classmethod 205 | def loads(cls, string): 206 | """Load multiple TLEs from a string.""" 207 | return [cls.from_lines(*l012) for l012 in partition(string.split('\n'), 3)] 208 | 209 | def to_orbit(self, attractor=_Earth): 210 | '''Convert to a :class:`poliastro.twobody.orbit.Orbit` around the attractor. 211 | 212 | >>> tle_string = """ISS (ZARYA) 213 | ... 1 25544U 98067A 19249.04864348 .00001909 00000-0 40858-4 0 9990 214 | ... 2 25544 51.6464 320.1755 0007999 10.9066 53.2893 15.50437522187805""" 215 | >>> tle = TLE.from_lines(*tle_string.splitlines()) 216 | >>> tle.to_orbit() 217 | 6788 x 6799 km x 51.6 deg (GCRS) orbit around Earth (♁) at epoch 2019-09-06T01:10:02.796672000 (UTC) 218 | ''' 219 | return _Orbit.from_classical( 220 | attractor=attractor, 221 | a=u.Quantity(self.a, u.km), 222 | ecc=u.Quantity(self.ecc, u.one), 223 | inc=u.Quantity(self.inc, u.deg), 224 | raan=u.Quantity(self.raan, u.deg), 225 | argp=u.Quantity(self.argp, u.deg), 226 | nu=u.Quantity(self.nu, u.deg), 227 | epoch=self.epoch) 228 | 229 | def astuple(self): 230 | """Return a tuple of the attributes.""" 231 | return attr.astuple(self) 232 | 233 | def asdict(self, computed=False, epoch=False): 234 | """Return a dict of the attributes.""" 235 | d = attr.asdict(self) 236 | if computed: 237 | d.update(a=self.a, nu=self.nu) 238 | if epoch: 239 | d.update(epoch=self.epoch) 240 | return d 241 | 242 | 243 | @attr.s 244 | class TLEu(TLE): 245 | """Unitful data class representing a single TLE. 246 | 247 | This is a subclass of :class:`TLE`, so refer to that class for a description 248 | of the attributes, properties and methods. 249 | 250 | The only difference here is that all the attributes are quantities 251 | (:class:`astropy.units.Quantity`), a type able to represent a value with 252 | an associated unit taken from :mod:`astropy.units`. 253 | """ 254 | 255 | @property 256 | def a(self): 257 | """Semi-major axis.""" 258 | if self._a is None: 259 | self._a = (_Earth.k.value / self.n.to_value(u.rad/u.s) ** 2) ** (1/3) * u.m 260 | return self._a 261 | 262 | @property 263 | def nu(self): 264 | """True anomaly.""" 265 | if self._nu is None: 266 | # Make sure the mean anomaly is between -pi and pi 267 | M = ((self.M.to_value(u.rad) + 180) % 360 - 180) * DEG2RAD 268 | ecc = self.ecc.to_value(u.one) 269 | nu_rad = _M_to_nu(M, ecc) 270 | self._nu = nu_rad * RAD2DEG * u.deg 271 | return self._nu 272 | 273 | @classmethod 274 | def from_lines(cls, name, line1, line2): 275 | """Parse a TLE from its constituent lines.""" 276 | return cls( 277 | name=name, 278 | norad=line1[2:7], 279 | classification=line1[7], 280 | int_desig=line1[9:17], 281 | epoch_year=line1[18:20], 282 | epoch_day=float(line1[20:32]), 283 | dn_o2=u.Quantity(float(line1[33:43]), u_rev / u.day**2), 284 | ddn_o6=u.Quantity(_parse_float(line1[44:52]), u_rev / u.day**3), 285 | bstar=u.Quantity(_parse_float(line1[53:61]), 1 / u.earthRad), 286 | set_num=line1[64:68], 287 | inc=u.Quantity(float(line2[8:16]), u.deg), 288 | raan=u.Quantity(float(line2[17:25]), u.deg), 289 | ecc=u.Quantity(_parse_decimal(line2[26:33]), u.one), 290 | argp=u.Quantity(float(line2[34:42]), u.deg), 291 | M=u.Quantity(float(line2[43:51]), u.deg), 292 | n=u.Quantity(float(line2[52:63]), u_rev / u.day), 293 | rev_num=line2[63:68]) 294 | 295 | def to_orbit(self, attractor=_Earth): 296 | """Convert to an orbit around the attractor.""" 297 | return _Orbit.from_classical( 298 | attractor=attractor, 299 | a=self.a, 300 | ecc=self.ecc, 301 | inc=self.inc, 302 | raan=self.raan, 303 | argp=self.argp, 304 | nu=self.nu, 305 | epoch=self.epoch) 306 | -------------------------------------------------------------------------------- /tletools/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import astropy.units as u 3 | from poliastro.core.angles import M_to_E as _M_to_E, E_to_nu as _E_to_nu 4 | 5 | #: :class:`numpy.dtype` for a date expressed as a year. 6 | dt_dt64_Y = np.dtype('datetime64[Y]') 7 | #: :class:`numpy.dtype` for a timedelta expressed in microseconds. 8 | dt_td64_us = np.dtype('timedelta64[us]') 9 | 10 | #: :class:`astropy.units.Unit` of angular measure: a full turn or rotation. 11 | #: It is equivalent to :const:`astropy.units.cycle`. 12 | rev = u.def_unit( 13 | ['rev', 'revolution'], 14 | 2.0 * np.pi * u.rad, 15 | prefixes=False, 16 | doc="revolution: angular measurement, a full turn or rotation") 17 | 18 | u.add_enabled_units(rev) 19 | 20 | 21 | def M_to_nu(M, ecc): 22 | """True anomaly from mean anomaly. 23 | 24 | .. versionadded:: 0.2.3 25 | 26 | :param float M: Mean anomaly in radians. 27 | :param float ecc: Eccentricity. 28 | :returns: `nu`, the true anomaly, between -π and π radians. 29 | 30 | **Warning** 31 | 32 | The mean anomaly must be between -π and π radians. 33 | The eccentricity must be less than 1. 34 | 35 | **Examples** 36 | 37 | >>> M_to_nu(0.25, 0.) 38 | 0.25 39 | 40 | >>> M_to_nu(0.25, 0.5) 41 | 0.804298286591367 42 | 43 | >>> M_to_nu(5., 0.) 44 | Traceback (most recent call last): 45 | ... 46 | AssertionError 47 | """ 48 | return _E_to_nu(_M_to_E(M, ecc), ecc) 49 | 50 | 51 | def partition(iterable, n, rest=False): 52 | """Partition an iterable into tuples. 53 | 54 | The iterable `iterable` is progressively consumed `n` items at a time in order to 55 | produce tuples of length `n`. 56 | 57 | :param iterable iterable: The iterable to partition. 58 | :param int n: Length of the desired tuples. 59 | :param bool rest: Whether to return a possibly incomplete tuple at the end. 60 | :returns: A generator which yields subsequent `n`-uples from the original iterable. 61 | 62 | **Examples** 63 | 64 | By default, any remaining items which are not sufficient to form 65 | a new tuple of length `n` are discarded. 66 | 67 | >>> list(partition(range(8), 3)) 68 | [(0, 1, 2), (3, 4, 5)] 69 | 70 | You can ask to return the remaining items at the end by setting the flag `rest` 71 | to ``True``. 72 | 73 | >>> list(partition(range(8), 3, rest=True)) 74 | [(0, 1, 2), (3, 4, 5), (6, 7)] 75 | """ 76 | it = iter(iterable) 77 | while True: 78 | res = [] 79 | try: 80 | for _ in range(n): 81 | res.append(next(it)) 82 | except StopIteration: 83 | if rest: 84 | yield tuple(res) 85 | return 86 | yield tuple(res) 87 | 88 | 89 | def _partition(iterable, n): 90 | """Partition an iterable into tuples.""" 91 | it = iter(iterable) 92 | z = (it,) * n 93 | return zip(*z) 94 | --------------------------------------------------------------------------------