poliastro-0.13.1/000755 001750 001750 00000000000 13577132207 014455 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/AUTHORS000644 001750 001750 00000000316 13525576227 015534 0ustar00juanlujuanlu000000 000000 * Juan Luis Cano Rodríguez Project creator and current developer * Alberto Lorenzo Márquez Contributed complete Continuous Integration for all platforms poliastro-0.13.1/COPYING000644 001750 001750 00000002154 13525576227 015521 0ustar00juanlujuanlu000000 000000 The MIT License (MIT) Copyright (c) 2012-2019 Juan Luis Cano Rodríguez and the poliastro development team 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. poliastro-0.13.1/MANIFEST.in000644 001750 001750 00000000264 13525576227 016224 0ustar00juanlujuanlu000000 000000 include COPYING include AUTHORS recursive-include src/poliastro/tests *.py *.html prune docs/source/examples/.ipynb_checkpoints global-exclude *.py[cod] __pycache__ *.so *.dylib poliastro-0.13.1/PKG-INFO000644 001750 001750 00000026105 13577132207 015556 0ustar00juanlujuanlu000000 000000 Metadata-Version: 2.1 Name: poliastro Version: 0.13.1 Summary: Python package for Orbital Mechanics Home-page: https://blog.poliastro.space/ Author: Juan Luis Cano Author-email: hello@juanlu.space License: MIT Download-URL: https://github.com/poliastro/poliastro Project-URL: Source, https://github.com/poliastro/poliastro Project-URL: Tracker, https://github.com/poliastro/poliastro/issues Description: .. poliastro .. image:: http://poliastro.github.io/images/logo_text.png :target: http://poliastro.github.io/ :alt: poliastro logo :width: 675px :align: center .. |orcid| image:: https://img.shields.io/badge/id-0000--0002--2187--161X-a6ce39.svg :target: http://orcid.org/0000-0002-2187-161X :Name: poliastro :Website: https://poliastro.github.io/ :Author: Juan Luis Cano Rodríguez |orcid| :Version: 0.13.1 .. |circleci| image:: https://img.shields.io/circleci/project/github/poliastro/poliastro/0.13.x.svg?style=flat-square&logo=circleci :target: https://circleci.com/gh/poliastro/poliastro .. |appveyor| image:: https://img.shields.io/appveyor/ci/Juanlu001/poliastro/0.13.x.svg?style=flat-square&logo=appveyor :target: https://ci.appveyor.com/project/Juanlu001/poliastro/branch/0.13.x .. |codecov| image:: https://img.shields.io/codecov/c/github/poliastro/poliastro.svg?style=flat-square :target: https://codecov.io/github/poliastro/poliastro?branch=0.13.x .. |codeclimate| image:: https://api.codeclimate.com/v1/badges/fd2aa5bf8c4b7984d11b/maintainability :target: https://codeclimate.com/github/poliastro/poliastro/maintainability .. |docs| image:: https://img.shields.io/badge/docs-v0.13.1-brightgreen.svg?style=flat-square :target: https://docs.poliastro.space/en/v0.13.1/?badge=v0.13.1 .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square :target: https://github.com/poliastro/poliastro/raw/0.13.x/COPYING .. |doi| image:: https://zenodo.org/badge/11178845.svg?style=flat-square :target: https://zenodo.org/badge/latestdoi/11178845 .. |astropy| image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat-square :target: http://www.astropy.org/ .. |mailing| image:: https://img.shields.io/badge/mailing%20list-groups.io-8cbcd1.svg?style=flat-square :target: https://groups.io/g/poliastro-dev .. |matrix| image:: https://img.shields.io/matrix/poliastro:matrix.org.svg?style=flat-square :alt: Join the chat at https://chat.openastronomy.org/#/room/#poliastro:matrix.org :target: https://chat.openastronomy.org/#/room/#poliastro:matrix.org |circleci| |appveyor| |codecov| |codeclimate| |docs| |license| |doi| |astropy| |mailing| |matrix| poliastro is an open source pure Python package dedicated to problems arising in Astrodynamics and Orbital Mechanics, such as orbit propagation, solution of the Lambert's problem, conversion between position and velocity vectors and classical orbital elements and orbit plotting, focusing on interplanetary applications. It is released under the MIT license. .. code-block:: python from poliastro.examples import molniya molniya.plot() .. image:: https://github.com/poliastro/poliastro/raw/0.13.x/docs/source/examples/molniya.png :align: center Documentation ============= |docs| Complete documentation, including a user guide and an API reference, can be read on the wonderful `Read the Docs`_. https://docs.poliastro.space/ .. _`Read the Docs`: https://readthedocs.org/ Examples ======== .. |mybinder| image:: https://img.shields.io/badge/launch-binder-e66581.svg?style=flat-square :target: https://beta.mybinder.org/v2/gh/poliastro/poliastro/0.13.x?filepath=index.ipynb |mybinder| In the examples directory you can find several Jupyter notebooks with specific applications of poliastro. You can launch a cloud Jupyter server using `binder`_ to edit the notebooks without installing anything. Try it out! https://beta.mybinder.org/v2/gh/poliastro/poliastro/0.13.x?filepath=index.ipynb .. _binder: https://beta.mybinder.org/ Requirements ============ poliastro requires the following Python packages: * NumPy, for basic numerical routines * Astropy, for physical units and time handling * numba (optional), for accelerating the code * jplephem, for the planetary ephemerides using SPICE kernels * matplotlib, for orbit plotting * plotly, for 2D and 3D interactive orbit plotting * SciPy, for root finding and numerical propagation poliastro is usually tested on Linux and Windows on Python 3.6 and 3.7 against latest NumPy. It should work on OS X without problems. ============== ============ =================== Platform Site Status ============== ============ =================== Linux CircleCI |circleci| Windows x64 Appveyor |appveyor| ============== ============ =================== Installation ============ The easiest and fastest way to get the package up and running is to install poliastro using `conda `_:: $ conda install poliastro --channel conda-forge Please check out the `documentation for alternative installation methods`_. .. _`documentation for alternative installation methods`: https://docs.poliastro.space/en/v0.13.1/getting_started.html#alternative-installation-methods Testing ======= |codecov| If installed correctly, the tests can be run using pytest:: $ python -c "import poliastro.testing; poliastro.testing.test()" ===================================== test session starts ===================================== platform linux -- Python 3.7.1, pytest-4.2.0, py-1.7.0, pluggy-0.8.1 rootdir: /home/juanlu/.miniconda36/envs/_test37/lib/python3.7/site-packages/poliastro, inifile: collected 747 items [...] ========= 738 passed, 3 skipped, 5 xfailed, 1 xpassed, 13 warnings in 392.12 seconds ========== $ Problems ======== If the installation fails or you find something that doesn't work as expected, please open an issue in the `issue tracker`_. .. _`issue tracker`: https://github.com/poliastro/poliastro/issues Contributing ============ .. image:: https://img.shields.io/waffle/label/poliastro/poliastro/1%20-%20Ready.svg?style=flat-square :target: https://waffle.io/poliastro/poliastro :alt: 'Stories in Ready' poliastro is a community project, hence all contributions are more than welcome! For more information, head to `CONTRIBUTING.rst`_. .. _`CONTRIBUTING.rst`: https://github.com/poliastro/poliastro/blob/0.13.x/CONTRIBUTING.rst Support ======= |mailing| |matrix| Release announcements and general discussion take place on our `Mailing List`_ . For further clarifications and discussions, feel free to join Poliastro `Chat Room`_. .. _`Chat Room`: https://chat.openastronomy.org/#/room/#poliastro:matrix.org .. _`Mailing List`: https://groups.io/g/poliastro-dev Citing ====== If you use poliastro on your project, please `drop me a line `_. You can also use the DOI to cite it in your publications. This is the latest one: |doi| And this is an example citation format:: Juan Luis Cano Rodríguez et al.. (2015). poliastro: poliastro 0.4.0. Zenodo. 10.5281/zenodo.17462 License ======= |license| poliastro is released under the MIT license, hence allowing commercial use of the library. Please refer to the COPYING file. FAQ === What's up with the name? ------------------------ poliastro comes from Polimi, which is the shortened name of the Politecnico di Milano, the Italian university where I was studying while writing this software. It's my tiny tribute to a place I came to love. *Grazie mille!* Can I do with poliastro? ----------------------------------------------- poliastro is focused on interplanetary applications. This has two consequences: * It tries to be more general than other Flight Dynamics core libraries more focused on Earth satellites (see `Related software`_ for a brief list), allowing the algorithms to work also for orbits around non-Earth bodies. * It leaves out certain features that would be too Earth-specific, such as TLE reading, SGP4 propagation, groundtrack plotting and others. .. _`Related software`: https://docs.poliastro.space/en/v0.13.1/about.html#related-software What's the future of the project? --------------------------------- poliastro is actively maintained and receiving an influx of new contributors thanks to the generous sponsorship of Google and the European Space Agency. The best way to get an idea of the roadmap is to see the `Milestones`_ of the project. .. _`Milestones`: https://github.com/poliastro/poliastro/milestones Keywords: aero,aerospace,engineering,astrodynamics,orbits,kepler,orbital mechanics Platform: any Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Scientific/Engineering :: Physics Classifier: Topic :: Scientific/Engineering :: Astronomy Requires-Python: <3.8,>=3.6 Provides-Extra: jupyter Provides-Extra: cesium Provides-Extra: dev poliastro-0.13.1/README.rst000644 001750 001750 00000020013 13577131033 016134 0ustar00juanlujuanlu000000 000000 .. poliastro .. image:: http://poliastro.github.io/images/logo_text.png :target: http://poliastro.github.io/ :alt: poliastro logo :width: 675px :align: center .. |orcid| image:: https://img.shields.io/badge/id-0000--0002--2187--161X-a6ce39.svg :target: http://orcid.org/0000-0002-2187-161X :Name: poliastro :Website: https://poliastro.github.io/ :Author: Juan Luis Cano Rodríguez |orcid| :Version: 0.13.1 .. |circleci| image:: https://img.shields.io/circleci/project/github/poliastro/poliastro/0.13.x.svg?style=flat-square&logo=circleci :target: https://circleci.com/gh/poliastro/poliastro .. |appveyor| image:: https://img.shields.io/appveyor/ci/Juanlu001/poliastro/0.13.x.svg?style=flat-square&logo=appveyor :target: https://ci.appveyor.com/project/Juanlu001/poliastro/branch/0.13.x .. |codecov| image:: https://img.shields.io/codecov/c/github/poliastro/poliastro.svg?style=flat-square :target: https://codecov.io/github/poliastro/poliastro?branch=0.13.x .. |codeclimate| image:: https://api.codeclimate.com/v1/badges/fd2aa5bf8c4b7984d11b/maintainability :target: https://codeclimate.com/github/poliastro/poliastro/maintainability .. |docs| image:: https://img.shields.io/badge/docs-v0.13.1-brightgreen.svg?style=flat-square :target: https://docs.poliastro.space/en/v0.13.1/?badge=v0.13.1 .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square :target: https://github.com/poliastro/poliastro/raw/0.13.x/COPYING .. |doi| image:: https://zenodo.org/badge/11178845.svg?style=flat-square :target: https://zenodo.org/badge/latestdoi/11178845 .. |astropy| image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat-square :target: http://www.astropy.org/ .. |mailing| image:: https://img.shields.io/badge/mailing%20list-groups.io-8cbcd1.svg?style=flat-square :target: https://groups.io/g/poliastro-dev .. |matrix| image:: https://img.shields.io/matrix/poliastro:matrix.org.svg?style=flat-square :alt: Join the chat at https://chat.openastronomy.org/#/room/#poliastro:matrix.org :target: https://chat.openastronomy.org/#/room/#poliastro:matrix.org |circleci| |appveyor| |codecov| |codeclimate| |docs| |license| |doi| |astropy| |mailing| |matrix| poliastro is an open source pure Python package dedicated to problems arising in Astrodynamics and Orbital Mechanics, such as orbit propagation, solution of the Lambert's problem, conversion between position and velocity vectors and classical orbital elements and orbit plotting, focusing on interplanetary applications. It is released under the MIT license. .. code-block:: python from poliastro.examples import molniya molniya.plot() .. image:: https://github.com/poliastro/poliastro/raw/0.13.x/docs/source/examples/molniya.png :align: center Documentation ============= |docs| Complete documentation, including a user guide and an API reference, can be read on the wonderful `Read the Docs`_. https://docs.poliastro.space/ .. _`Read the Docs`: https://readthedocs.org/ Examples ======== .. |mybinder| image:: https://img.shields.io/badge/launch-binder-e66581.svg?style=flat-square :target: https://beta.mybinder.org/v2/gh/poliastro/poliastro/0.13.x?filepath=index.ipynb |mybinder| In the examples directory you can find several Jupyter notebooks with specific applications of poliastro. You can launch a cloud Jupyter server using `binder`_ to edit the notebooks without installing anything. Try it out! https://beta.mybinder.org/v2/gh/poliastro/poliastro/0.13.x?filepath=index.ipynb .. _binder: https://beta.mybinder.org/ Requirements ============ poliastro requires the following Python packages: * NumPy, for basic numerical routines * Astropy, for physical units and time handling * numba (optional), for accelerating the code * jplephem, for the planetary ephemerides using SPICE kernels * matplotlib, for orbit plotting * plotly, for 2D and 3D interactive orbit plotting * SciPy, for root finding and numerical propagation poliastro is usually tested on Linux and Windows on Python 3.6 and 3.7 against latest NumPy. It should work on OS X without problems. ============== ============ =================== Platform Site Status ============== ============ =================== Linux CircleCI |circleci| Windows x64 Appveyor |appveyor| ============== ============ =================== Installation ============ The easiest and fastest way to get the package up and running is to install poliastro using `conda `_:: $ conda install poliastro --channel conda-forge Please check out the `documentation for alternative installation methods`_. .. _`documentation for alternative installation methods`: https://docs.poliastro.space/en/v0.13.1/getting_started.html#alternative-installation-methods Testing ======= |codecov| If installed correctly, the tests can be run using pytest:: $ python -c "import poliastro.testing; poliastro.testing.test()" ===================================== test session starts ===================================== platform linux -- Python 3.7.1, pytest-4.2.0, py-1.7.0, pluggy-0.8.1 rootdir: /home/juanlu/.miniconda36/envs/_test37/lib/python3.7/site-packages/poliastro, inifile: collected 747 items [...] ========= 738 passed, 3 skipped, 5 xfailed, 1 xpassed, 13 warnings in 392.12 seconds ========== $ Problems ======== If the installation fails or you find something that doesn't work as expected, please open an issue in the `issue tracker`_. .. _`issue tracker`: https://github.com/poliastro/poliastro/issues Contributing ============ .. image:: https://img.shields.io/waffle/label/poliastro/poliastro/1%20-%20Ready.svg?style=flat-square :target: https://waffle.io/poliastro/poliastro :alt: 'Stories in Ready' poliastro is a community project, hence all contributions are more than welcome! For more information, head to `CONTRIBUTING.rst`_. .. _`CONTRIBUTING.rst`: https://github.com/poliastro/poliastro/blob/0.13.x/CONTRIBUTING.rst Support ======= |mailing| |matrix| Release announcements and general discussion take place on our `Mailing List`_ . For further clarifications and discussions, feel free to join Poliastro `Chat Room`_. .. _`Chat Room`: https://chat.openastronomy.org/#/room/#poliastro:matrix.org .. _`Mailing List`: https://groups.io/g/poliastro-dev Citing ====== If you use poliastro on your project, please `drop me a line `_. You can also use the DOI to cite it in your publications. This is the latest one: |doi| And this is an example citation format:: Juan Luis Cano Rodríguez et al.. (2015). poliastro: poliastro 0.4.0. Zenodo. 10.5281/zenodo.17462 License ======= |license| poliastro is released under the MIT license, hence allowing commercial use of the library. Please refer to the COPYING file. FAQ === What's up with the name? ------------------------ poliastro comes from Polimi, which is the shortened name of the Politecnico di Milano, the Italian university where I was studying while writing this software. It's my tiny tribute to a place I came to love. *Grazie mille!* Can I do with poliastro? ----------------------------------------------- poliastro is focused on interplanetary applications. This has two consequences: * It tries to be more general than other Flight Dynamics core libraries more focused on Earth satellites (see `Related software`_ for a brief list), allowing the algorithms to work also for orbits around non-Earth bodies. * It leaves out certain features that would be too Earth-specific, such as TLE reading, SGP4 propagation, groundtrack plotting and others. .. _`Related software`: https://docs.poliastro.space/en/v0.13.1/about.html#related-software What's the future of the project? --------------------------------- poliastro is actively maintained and receiving an influx of new contributors thanks to the generous sponsorship of Google and the European Space Agency. The best way to get an idea of the roadmap is to see the `Milestones`_ of the project. .. _`Milestones`: https://github.com/poliastro/poliastro/milestones poliastro-0.13.1/setup.cfg000644 001750 001750 00000004222 13577134570 016303 0ustar00juanlujuanlu000000 000000 [metadata] name = poliastro description = Python package for Orbital Mechanics long_description = file: README.rst version = attr: poliastro.__version__ license = MIT author = Juan Luis Cano author_email = hello@juanlu.space keywords = aero aerospace engineering astrodynamics orbits kepler orbital mechanics url = https://blog.poliastro.space/ download_url = https://github.com/poliastro/poliastro project_urls = Source=https://github.com/poliastro/poliastro Tracker=https://github.com/poliastro/poliastro/issues platforms = any classifiers = Development Status :: 4 - Beta Intended Audience :: Education Intended Audience :: Science/Research License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: Implementation :: CPython Topic :: Scientific/Engineering Topic :: Scientific/Engineering :: Physics Topic :: Scientific/Engineering :: Astronomy [options] package_dir = =src packages = find: zip_safe = False python_requires = >=3.6,<3.8 install_requires = astropy~=3.2 astroquery>=0.3.9 jplephem matplotlib>=2.0,!=3.0.1 numba>=0.39 ; implementation_name=='cpython' numpy pandas plotly~=4.0 scipy>=1.0 include_package_data = True [options.packages.find] where = src [options.entry_points] console_scripts = poliastro = poliastro.cli:main [options.extras_require] jupyter = notebook cesium = czml3~=0.1.2 dev = black coverage ipykernel ipython>=5.0 ipywidgets>=7.0 isort jupyter-client nbsphinx>=0.5.0 pycodestyle pytest>=3.2 pytest-cov<2.6.0 pytest-doctestplus pytest-mpl sphinx sphinx_rtd_theme # FIXME: https://github.com/readthedocs/sphinx_rtd_theme/issues/788 sphinx-notfound-page tox [tool:pytest] norecursedirs = .git .tox dist env build venv python_files = test_*.py doctest_plus = disabled addopts = --strict markers = slow remote_data filterwarnings mpl_image_compare [flake8] ignore = E203, E266, E501, W503 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 [egg_info] tag_build = tag_date = 0 poliastro-0.13.1/setup.py000644 001750 001750 00000000273 13577127432 016175 0ustar00juanlujuanlu000000 000000 #!/usr/bin/env python from setuptools import setup # https://packaging.python.org/guides/single-sourcing-package-version/ # http://blog.ionelmc.ro/2014/05/25/python-packaging/ setup() poliastro-0.13.1/src/000755 001750 001750 00000000000 13577132207 015244 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/000755 001750 001750 00000000000 13577132207 017260 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/__init__.py000644 001750 001750 00000000164 13577131033 021366 0ustar00juanlujuanlu000000 000000 """ ========= poliastro ========= Utilities and Python wrappers for Orbital Mechanics """ __version__ = "0.13.1" poliastro-0.13.1/src/poliastro/bodies.py000644 001750 001750 00000032673 13577127436 021122 0ustar00juanlujuanlu000000 000000 """Bodies of the Solar System. Contains some predefined bodies of the Solar System: * Sun (☉) * Earth (♁) * Moon (☾) * Mercury (☿) * Venus (♀) * Mars (♂) * Jupiter (♃) * Saturn (♄) * Uranus (⛢) * Neptune (♆) * Pluto (♇) and a way to define new bodies (:py:class:`~Body` class). Data references can be found in :py:mod:`~poliastro.constants` """ import math from astropy import units as u from astropy.constants import G from poliastro import constants class _Body(object): name: str symbol: str def __str__(self): return f"{self.name} ({self.symbol})" def __repr__(self): return self.__str__() def rot_elements_at_epoch(self, epoch): """Provides rotational elements at epoch. Provides north pole of body and angle to prime meridian Parameters ---------- epoch : ~astropy.time.Time, optional Epoch, default to J2000. Returns ------- ra, dec, W: tuple (~astropy.units.Quantity) Right ascension and declination of north pole, and angle of the prime meridian. """ T = (epoch.tdb - constants.J2000).to("day").value / 36525 d = (epoch.tdb - constants.J2000).to("day").value return self._rot_elements_at_epoch(T, d) @staticmethod def _rot_elements_at_epoch(T, d): raise NotImplementedError("Function only defined for some Solar System bodies") class Body(_Body): """Class to represent a generic body. """ def __init__( self, parent, k, name, symbol=None, R=0 * u.km, R_polar=0 * u.km, R_mean=0 * u.km, **kwargs, ): """Constructor. Parameters ---------- parent : Body Central body. k : ~astropy.units.Quantity Standard gravitational parameter. name : str Name of the body. symbol : str, optional Symbol for the body. R : ~astropy.units.Quantity, optional Equatorial radius of the body. R_polar: ~astropy.units.Quantity, optional Polar radius of the body R_mean: ~astropy.units.Quantity, optional Mean radius of the body """ self.parent = parent self.k = k self.name = name self.symbol = symbol self.R = R self.R_polar = R_polar self.R_mean = R_mean self.kwargs = kwargs @classmethod @u.quantity_input(k=u.km ** 3 / u.s ** 2, R=u.km) def from_parameters(cls, parent, k, name, symbol, R, **kwargs): return cls(parent, k, name, symbol, R, **kwargs) @classmethod def from_relative( cls, reference, parent=None, k=None, name=None, symbol=None, R=None, **kwargs ): k = k * reference.k R = R * reference.R return cls(parent, k, name, symbol, R, **kwargs) class _Sun(_Body): parent = None k = constants.GM_sun name = "Sun" symbol = "\u2609" R = constants.R_sun mass = k / G J2 = constants.J2_sun Wdivc = constants.Wdivc_sun @staticmethod def _rot_elements_at_epoch(T, d): ra = 286.13 * u.deg dec = 63.87 * u.deg W = (84.176 + 14.1844000 * d) * u.deg return ra, dec, W Sun = _Sun() class _Mercury(_Body): parent = Sun k = constants.GM_mercury name = "Mercury" symbol = "\u263F" R = constants.R_mercury R_mean = constants.R_mean_mercury R_polar = constants.R_polar_mercury mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): M1 = (174.7910857 + 4.092335 * d) * u.deg M2 = (349.5821714 + 8.184670 * d) * u.deg M3 = (164.3732571 + 12.277005 * d) * u.deg M4 = (339.1643429 + 16.369340 * d) * u.deg M5 = (153.9554286 + 20.461675 * d) * u.deg ra = (281.0103 - 0.0328 * T) * u.deg dec = (61.45 - 0.005 * T) * u.deg W = (329.5988 + 6.1385108 * d) * u.deg + ( 0.01067257 * math.sin(M1.to("rad").value) - 0.00112309 * math.sin(M2.to("rad").value) - 0.00011040 * math.sin(M3.to("rad").value) - 0.00002539 * math.sin(M4.to("rad").value) - 0.00000571 * math.sin(M5.to("rad").value) ) * u.deg return ra, dec, W class _Venus(_Body): parent = Sun k = constants.GM_venus name = "Venus" symbol = "\u2640" R = constants.R_venus R_mean = constants.R_mean_venus R_polar = constants.R_polar_venus mass = k / G J2 = constants.J2_venus J3 = constants.J3_venus ecc = 0.007 * u.one @staticmethod def _rot_elements_at_epoch(T, d): ra = 272.76 * u.deg dec = 67.16 * u.deg W = (160.20 - 1.4813688 * d) * u.deg return ra, dec, W class _Earth(_Body): parent = Sun k = constants.GM_earth name = "Earth" symbol = "\u2641" R = constants.R_earth R_mean = constants.R_mean_earth R_polar = constants.R_polar_earth mass = k / G J2 = constants.J2_earth J3 = constants.J3_earth H0 = constants.H0_earth rho0 = constants.rho0_earth @staticmethod def _rot_elements_at_epoch(T, d): ra = (0.00 - 0.641 * T) * u.deg dec = (90.00 - 0.557 * T) * u.deg W = (190.147 + 360.9856235 * d) * u.deg return ra, dec, W class _Mars(_Body): parent = Sun k = constants.GM_mars name = "Mars" symbol = "\u2642" R = constants.R_mars R_mean = constants.R_mean_mars R_polar = constants.R_polar_mars mass = k / G J2 = constants.J2_mars J3 = constants.J3_mars @staticmethod def _rot_elements_at_epoch(T, d): M1 = (198.991226 + 19139.4819985 * T) * u.deg M2 = (226.292679 + 38280.8511281 * T) * u.deg M3 = (249.663391 + 57420.7251593 * T) * u.deg M4 = (266.183510 + 76560.6367950 * T) * u.deg M5 = (79.398797 + 0.5042615 * T) * u.deg ra = ( 317.269202 - 0.10927547 * T + 0.000068 * math.sin(M1.to("rad").value) + 0.000238 * math.sin(M2.to("rad").value) + 0.000052 * math.sin(M3.to("rad").value) + 0.000009 * math.sin(M4.to("rad").value) + 0.419057 * math.sin(M5.to("rad").value) ) * u.deg K1 = (122.433576 + 19139.9407476 * T) * u.deg K2 = (43.058401 + 38280.8753272 * T) * u.deg K3 = (57.663379 + 57420.7517205 * T) * u.deg K4 = (79.476401 + 76560.6495004 * T) * u.deg K5 = (166.325722 + 0.5042615 * T) * u.deg dec = ( 54.432516 - 0.05827105 * T + 0.000051 * math.cos(K1.to("rad").value) + 0.000141 * math.cos(K2.to("rad").value) + 0.000031 * math.cos(K3.to("rad").value) + 0.000005 * math.cos(K4.to("rad").value) + 1.591274 * math.cos(K5.to("rad").value) ) * u.deg J1 = (129.071773 + 19140.0328244 * T) * u.deg J2 = (36.352167 + 38281.0473591 * T) * u.deg J3 = (56.668646 + 57420.9295360 * T) * u.deg J4 = (67.364003 + 76560.2552215 * T) * u.deg J5 = (104.792680 + 95700.4387578 * T) * u.deg J6 = (95.391654 + 0.5042615 * T) * u.deg W = ( 176.049863 + 350.891982443297 * d + 0.000145 * math.sin(J1.to("rad").value) + 0.000157 * math.sin(J2.to("rad").value) + 0.000040 * math.sin(J3.to("rad").value) + 0.000001 * math.sin(J4.to("rad").value) + 0.000001 * math.sin(J5.to("rad").value) + 0.584542 * math.sin(J6.to("rad").value) ) * u.deg return ra, dec, W class _Jupiter(_Body): parent = Sun k = constants.GM_jupiter name = "Jupiter" symbol = "\u2643" R = constants.R_jupiter R_mean = constants.R_mean_jupiter R_polar = constants.R_polar_jupiter mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): Ja = (99.360714 + 4850.4046 * T) * u.deg Jb = (175.895369 + 1191.9605 * T) * u.deg Jc = (300.323162 + 262.5475 * T) * u.deg Jd = (114.012305 + 6070.2476 * T) * u.deg Je = (49.511251 + 64.3000 * T) * u.deg ra = ( 268.056595 - 0.006499 * T + 0.000117 * math.sin(Ja.to("rad").value) + 0.000938 * math.sin(Jb.to("rad").value) + 0.001432 * math.sin(Jc.to("rad").value) + 0.000030 * math.sin(Jd.to("rad").value) + 0.002150 * math.sin(Je.to("rad").value) ) * u.deg dec = ( 64.495303 + 0.002413 * T + 0.000050 * math.cos(Ja.to("rad").value) + 0.000404 * math.cos(Jb.to("rad").value) + 0.000617 * math.cos(Jc.to("rad").value) - 0.000013 * math.cos(Jd.to("rad").value) + 0.000926 * math.cos(Je.to("rad").value) ) * u.deg W = (284.95 + 870.536 * d) * u.deg return ra, dec, W class _Saturn(_Body): parent = Sun k = constants.GM_saturn name = "Saturn" symbol = "\u2644" R = constants.R_saturn R_mean = constants.R_mean_saturn R_polar = constants.R_polar_saturn mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): ra = (40.589 - 0.036 * T) * u.deg dec = (83.537 - 0.004 * T) * u.deg W = (38.90 + 810.7939024 * d) * u.deg return ra, dec, W class _Uranus(_Body): parent = Sun k = constants.GM_uranus name = "Uranus" symbol = "\u26E2" R = constants.R_uranus R_mean = constants.R_mean_uranus R_polar = constants.R_polar_uranus mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): ra = 257.311 * u.deg dec = -15.175 * u.deg W = (203.81 - 501.1600928 * d) * u.deg return ra, dec, W class _Neptune(_Body): parent = Sun k = constants.GM_neptune name = "Neptune" symbol = "\u2646" R = constants.R_neptune R_mean = constants.R_mean_neptune R_polar = constants.R_polar_neptune mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): N = (357.85 + 52.316 * T) * u.deg ra = (299.36 + 0.70 * math.sin(N.to("rad").value)) * u.deg dec = (43.46 - 0.51 * math.cos(N.to("rad").value)) * u.deg W = (249.978 + 541.1397757 * d - 0.48 * math.sin(N.to("rad").value)) * u.deg return ra, dec, W class _Pluto(_Body): parent = Sun k = constants.GM_pluto name = "Pluto" symbol = "\u2647" R = constants.R_pluto R_mean = constants.R_mean_pluto R_polar = constants.R_polar_pluto mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): ra = 132.993 * u.deg dec = -6.163 * u.deg W = (302.695 + 56.3625225 * d) * u.deg return ra, dec, W Mercury = _Mercury() Venus = _Venus() Earth = _Earth() Mars = _Mars() Jupiter = _Jupiter() Saturn = _Saturn() Uranus = _Uranus() Neptune = _Neptune() Pluto = _Pluto() class _Moon(_Body): parent = Earth k = constants.GM_moon name = "Moon" symbol = "\u263E" R = constants.R_moon R_mean = constants.R_mean_moon R_polar = constants.R_polar_moon mass = k / G @staticmethod def _rot_elements_at_epoch(T, d): E1 = (125.045 - 0.0529921 * d) * u.deg E2 = (250.089 - 0.1059842 * d) * u.deg E3 = (260.008 + 13.0120009 * d) * u.deg E4 = (176.625 + 13.3407154 * d) * u.deg E5 = (357.529 + 0.9856003 * d) * u.deg E6 = (311.589 + 26.4057084 * d) * u.deg E7 = (134.963 + 13.0649930 * d) * u.deg E8 = (276.617 + 0.3287146 * d) * u.deg E9 = (34.226 + 1.7484877 * d) * u.deg E10 = (15.134 - 0.1589763 * d) * u.deg E11 = (119.743 + 0.0036096 * d) * u.deg E12 = (239.961 + 0.1643573 * d) * u.deg E13 = (25.053 + 12.9590088 * d) * u.deg ra = ( 269.9949 + 0.0031 * T - 3.8787 * math.sin(E1.to("rad").value) - 0.1204 * math.sin(E2.to("rad").value) + 0.0700 * math.sin(E3.to("rad").value) - 0.0172 * math.sin(E4.to("rad").value) + 0.0072 * math.sin(E6.to("rad").value) - 0.0052 * math.sin(E10.to("rad").value) + 0.0043 * math.sin(E13.to("rad").value) ) * u.deg dec = ( 66.5392 + 0.0130 * T + 1.5419 * math.cos(E1.to("rad").value) + 0.0239 * math.cos(E2.to("rad").value) - 0.0278 * math.cos(E3.to("rad").value) + 0.0068 * math.cos(E4.to("rad").value) - 0.0029 * math.cos(E6.to("rad").value) + 0.0009 * math.cos(E7.to("rad").value) + 0.0008 * math.cos(E10.to("rad").value) - 0.0009 * math.cos(E13.to("rad").value) ) * u.deg W = ( 38.321 + 13.17635815 * d - 1.4e-12 * d ** 2 + 3.5610 * math.sin(E1.to("rad").value) + 0.1208 * math.sin(E2.to("rad").value) - 0.0642 * math.sin(E3.to("rad").value) + 0.0158 * math.sin(E4.to("rad").value) + 0.0252 * math.sin(E5.to("rad").value) - 0.0066 * math.sin(E6.to("rad").value) - 0.0047 * math.sin(E7.to("rad").value) - 0.0046 * math.sin(E8.to("rad").value) + 0.0028 * math.sin(E9.to("rad").value) + 0.0052 * math.sin(E10.to("rad").value) + 0.0040 * math.sin(E11.to("rad").value) + 0.0019 * math.sin(E12.to("rad").value) - 0.0044 * math.sin(E13.to("rad").value) ) * u.deg return ra, dec, W Moon = _Moon() poliastro-0.13.1/src/poliastro/cli.py000644 001750 001750 00000001146 13525576227 020412 0ustar00juanlujuanlu000000 000000 """Command line functions. """ import argparse import poliastro from poliastro.neos.dastcom5 import download_dastcom5 def main(): parser = argparse.ArgumentParser( prog="poliastro", description="Command line tools for the poliastro Python library.", ) parser.add_argument("--version", action="version", version=poliastro.__version__) parser.add_argument( "--download-dastcom5", action="store_true", help="Downloads DASTCOM5 database" ) args = parser.parse_args() if args.download_dastcom5: download_dastcom5() else: parser.print_help() poliastro-0.13.1/src/poliastro/constants.py000644 001750 001750 00000032764 13577127436 021672 0ustar00juanlujuanlu000000 000000 """Astronomical and physics constants. This module complements constants defined in `astropy.constants`, with gravitational paremeters and radii. Note that `GM_jupiter` and `GM_neptune` are both referred to the whole planetary system gravitational parameter. Unless otherwise specified, gravitational and mass parameters were obtained from: * Luzum, Brian et al. “The IAU 2009 System of Astronomical Constants: The Report of the IAU Working Group on Numerical Standards for Fundamental Astronomy.” Celestial Mechanics and Dynamical Astronomy 110.4 (2011): 293–304. Crossref. Web. `DOI: 10.1007/s10569-011-9352-4`_ radii were obtained from: * Archinal, B. A. et al. “Report of the IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009.” Celestial Mechanics and Dynamical Astronomy 109.2 (2010): 101–135. Crossref. Web. `DOI: 10.1007/s10569-010-9320-4`_ .. _`DOI: 10.1007/s10569-011-9352-4`: http://dx.doi.org/10.1007/s10569-011-9352-4 .. _`DOI: 10.1007/s10569-010-9320-4`: http://dx.doi.org/10.1007/s10569-010-9320-4 J2 for the Sun was obtained from: * https://hal.archives-ouvertes.fr/hal-00433235/document (New values of gravitational moments J2 and J4 deduced from helioseismology, Redouane Mecheri et al) """ from astropy import time from astropy.constants import Constant # See for example USNO Circular 179 J2000_TT = time.Time("J2000", scale="tt") J2000_TDB = time.Time("J2000", scale="tdb") J2000 = J2000_TT GM_sun = Constant( "GM_sun", "Heliocentric gravitational constant", 1.32712442099e20, "m3 / (s2)", 0.0000000001e20, "IAU 2009 system of astronomical constants", system="si", ) GM_earth = Constant( "GM_earth", "Geocentric gravitational constant", 3.986004418e14, "m3 / (s2)", 0.000000008e14, "IAU 2009 system of astronomical constants", system="si", ) # Anderson, John D. et al. “The Mass, Gravity Field, and Ephemeris of Mercury.” Icarus 71.3 (1987): 337–349. # Crossref. Web. DOI: 10.1016/0019-1035(87)90033-9 GM_mercury = Constant( "GM_mercury", "Mercury gravitational constant", 2.203209e13, "m3 / (s2)", 0.91, "IAU 2009 system of astronomical constants", system="si", ) # Konopliv, A.S., W.B. Banerdt, and W.L. Sjogren. “Venus Gravity: 180th Degree and Order Model.” # Icarus 139.1 (1999): 3–18. Crossref. Web. DOI: 10.1006/icar.1999.6086 GM_venus = Constant( "GM_venus", "Venus gravitational constant", 3.24858592e14, "m3 / (s2)", 0.006, "IAU 2009 system of astronomical constants", system="si", ) # Konopliv, Alex S. et al. “A Global Solution for the Mars Static and Seasonal Gravity, Mars Orientation, Phobos and # Deimos Masses, and Mars Ephemeris.” Icarus 182.1 (2006): 23–50. # Crossref. Web. DOI: 10.1016/j.icarus.2005.12.025 GM_mars = Constant( "GM_mars", "Mars gravitational constant", 4.282837440e13, "m3 / (s2)", 0.00028, "IAU 2009 system of astronomical constants", system="si", ) # Jacobson, R. A. et al. “A comprehensive orbit reconstruction for the galileo prime mission in the JS200 system.” # The Journal of the Astronautical Sciences 48.4 (2000): 495–516. # Crossref. Web. GM_jupiter = Constant( "GM_jupiter", "Jovian system gravitational constant", 1.2671276253e17, "m3 / (s2)", 2.00, "IAU 2009 system of astronomical constants", system="si", ) # Jacobson, R. A. et al. “The Gravity Field of the Saturnian System from Satellite Observations and Spacecraft # Tracking Data.” The Astronomical Journal 132.6 (2006): 2520–2526. # Crossref. Web. DOI: 10.1086/508812 GM_saturn = Constant( "GM_saturn", "Saturn gravitational constant", 3.79312077e16, "m3 / (s2)", 1.1, "IAU 2009 system of astronomical constants", system="si", ) # Jacobson, R. A. et al. “The Masses of Uranus and Its Major Satellites from Voyager Tracking Data and Earth-Based # Uranian Satellite Data.” The Astronomical Journal 103 (1992): 2068. # Crossref. Web. DOI: 10.1086/116211 GM_uranus = Constant( "GM_uranus", "Uranus gravitational constant", 5.7939393e15, "m3 / (s2)", 13.0, "IAU 2009 system of astronomical constants", system="si", ) # Jacobson, R. A. “THE ORBITS OF THE NEPTUNIAN SATELLITES AND THE ORIENTATION OF THE POLE OF NEPTUNE.” # The Astronomical Journal 137.5 (2009): 4322–4329. Crossref. Web. DOI: # 10.1088/0004-6256/137/5/4322 GM_neptune = Constant( "GM_neptune", "Neptunian system gravitational constant", 6.836527100580397e15, "m3 / (s2)", 10.0, "IAU 2009 system of astronomical constants", system="si", ) # Tholen, David J. et al. “MASSES OF NIX AND HYDRA.” The Astronomical Journal 135.3 (2008): 777–784. Crossref. Web. # DOI: 10.1088/0004-6256/135/3/777 GM_pluto = Constant( "GM_pluto", "Pluto gravitational constant", 8.703e11, "m3 / (s2)", 3.7, "IAU 2009 system of astronomical constants", system="si", ) # Lemoine, Frank G. et al. “High-Degree Gravity Models from GRAIL Primary Mission Data.” # Journal of Geophysical Research: Planets 118.8 (2013): 1676–1698. # Crossref. Web. DOI: 10.1002/jgre.20118 GM_moon = Constant( "GM_moon", "Moon gravitational constant", 4.90279981e12, "m3 / (s2)", 0.00000774, "Journal of Geophysical Research: Planets 118.8 (2013)", system="si", ) # Archinal, B. A., Acton, C. H., A’Hearn, M. F., Conrad, A., Consolmagno, # G. J., Duxbury, T., … Williams, I. P. (2018). Report of the IAU Working # Group on Cartographic Coordinates and Rotational Elements: 2015. Celestial # Mechanics and Dynamical Astronomy, 130(3). doi:10.1007/s10569-017-9805-5 R_mean_earth = Constant( "R_mean_earth", "Earth mean radius", 6.3710084e6, "m", 0.1, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_mercury = Constant( "R_mean_mercury", "Mercury mean radius", 2.4394e6, "m", 100, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_venus = Constant( "R_mean_venus", "Venus mean radius", 6.0518e6, "m", 1000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_mars = Constant( "R_mean_mars", "Mars mean radius", 3.38950e6, "m", 2000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_jupiter = Constant( "R_mean_jupiter", "Jupiter mean radius", 6.9911e7, "m", 6000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009", system="si", ) R_mean_saturn = Constant( "R_mean_saturn", "Saturn mean radius", 5.8232e7, "m", 6000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_uranus = Constant( "R_mean_uranus", "Uranus mean radius", 2.5362e7, "m", 7000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_neptune = Constant( "R_mean_neptune", "Neptune mean radius", 2.4622e7, "m", 19000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_pluto = Constant( "R_mean_pluto", "Pluto mean radius", 1.188e6, "m", 1600, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mean_moon = Constant( "R_mean_moon", "Moon mean radius", 1.7374e6, "m", 0, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_sun = Constant( "R_sun", "Sun equatorial radius", 6.95700e8, "m", 0, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_earth = Constant( "R_earth", "Earth equatorial radius", 6.3781366e6, "m", 0.1, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mercury = Constant( "R_mercury", "Mercury equatorial radius", 2.44053e6, "m", 40, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_venus = Constant( "R_venus", "Venus equatorial radius", 6.0518e6, "m", 1000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_mars = Constant( "R_mars", "Mars equatorial radius", 3.39619e6, "m", 100, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_jupiter = Constant( "R_jupiter", "Jupiter equatorial radius", 7.1492e7, "m", 4000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009", system="si", ) R_saturn = Constant( "R_saturn", "Saturn equatorial radius", 6.0268e7, "m", 4000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_uranus = Constant( "R_uranus", "Uranus equatorial radius", 2.5559e7, "m", 4000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_neptune = Constant( "R_neptune", "Neptune equatorial radius", 2.4764e7, "m", 15000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_pluto = Constant( "R_pluto", "Pluto equatorial radius", 1.1883e6, "m", 1600, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_moon = Constant( "R_moon", "Moon equatorial radius", 1.7374e6, "m", 0, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_earth = Constant( "R_polar_earth", "Earth polar radius", 6.3567519e6, "m", 0.1, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_mercury = Constant( "R_polar_mercury", "Mercury polar radius", 2.43826e6, "m", 40, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_venus = Constant( "R_polar_venus", "Venus polar radius", 6.0518e6, "m", 1000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_mars = Constant( "R_polar_mars", "Mars polar radius", 3.376220e6, "m", 100, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_jupiter = Constant( "R_polar_jupiter", "Jupiter polar radius", 6.6854, "m", 10000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009", system="si", ) R_polar_saturn = Constant( "R_polar_saturn", "Saturn polar radius", 5.4364e7, "m", 10000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_uranus = Constant( "R_polar_uranus", "Uranus polar radius", 2.4973e7, "m", 20000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_neptune = Constant( "R_polar_neptune", "Neptune polar radius", 2.4341e7, "m", 30000, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_pluto = Constant( "R_polar_pluto", "Pluto polar radius", 1.1883e6, "m", 1600, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) R_polar_moon = Constant( "R_polar_moon", "Moon polar radius", 1.7374e6, "m", 0, "IAU Working Group on Cartographic Coordinates and Rotational Elements: 2015", system="si", ) J2_sun = Constant( "J2_sun", "Sun J2 oblateness coefficient", 2.20e-7, "", 0.01e-7, "HAL archives", system="si", ) J2_earth = Constant( "J2_earth", "Earth J2 oblateness coefficient", 0.00108263, "", 1, "HAL archives", system="si", ) J3_earth = Constant( "J3_earth", "Earth J3 asymmetry between the northern and southern hemispheres", -2.5326613168e-6, "", 1, "HAL archives", system="si", ) J2_mars = Constant( "J2_mars", "Mars J2 oblateness coefficient", 0.0019555, "", 1, "HAL archives", system="si", ) J3_mars = Constant( "J3_mars", "Mars J3 asymmetry between the northern and southern hemispheres", 3.1450e-5, "", 1, "HAL archives", system="si", ) J2_venus = Constant( "J2_venus", "Venus J2 oblateness coefficient", 4.4044e-6, "", 1, "HAL archives", system="si", ) J3_venus = Constant( "J3_venus", "Venus J3 asymmetry between the northern and southern hemispheres", -2.1082e-6, "", 1, "HAL archives", system="si", ) H0_earth = Constant( "H0_earth", "Earth H0 atmospheric scale height", 8_500, "m", 1, "de Pater and Lissauer 2010", system="si", ) rho0_earth = Constant( "rho0_earth", "Earth rho0 atmospheric density prefactor", 1.3, "kg / (m3)", 1, "de Pater and Lissauer 2010", system="si", ) Wdivc_sun = Constant( "Wdivc_sun", "total radiation power of Sun divided by the speed of light", 1.0203759306204136e14, "kg km / (s2)", 1, "Howard Curtis", system="si", ) poliastro-0.13.1/src/poliastro/coordinates.py000644 001750 001750 00000012134 13577127432 022151 0ustar00juanlujuanlu000000 000000 """Functions related to coordinate systems and transformations. This module complements :py:mod:`astropy.coordinates`. """ from math import cos, sin, sqrt import astropy.units as u import numpy as np from astropy.coordinates import ( CartesianDifferential, CartesianRepresentation, get_body_barycentric_posvel, ) from poliastro.constants import J2000 from poliastro.core.elements import rv2coe from poliastro.twobody.orbit import Orbit from poliastro.util import transform as transform_vector def body_centered_to_icrs(r, v, source_body, epoch=J2000, rotate_meridian=False): """Converts position and velocity body-centered frame to ICRS. Parameters ---------- r : ~astropy.units.Quantity Position vector in a body-centered reference frame. v : ~astropy.units.Quantity Velocity vector in a body-centered reference frame. source_body : Body Source body. epoch : ~astropy.time.Time, optional Epoch, default to J2000. rotate_meridian : bool, optional Whether to apply the rotation of the meridian too, default to False. Returns ------- r, v : tuple (~astropy.units.Quantity) Position and velocity vectors in ICRS. """ ra, dec, W = source_body.rot_elements_at_epoch(epoch) if rotate_meridian: r = transform_vector(r, -W, "z") v = transform_vector(v, -W, "z") r_trans1 = transform_vector(r, -(90 * u.deg - dec), "x") r_trans2 = transform_vector(r_trans1, -(90 * u.deg + ra), "z") v_trans1 = transform_vector(v, -(90 * u.deg - dec), "x") v_trans2 = transform_vector(v_trans1, -(90 * u.deg + ra), "z") icrs_frame_pos_coord, icrs_frame_vel_coord = get_body_barycentric_posvel( source_body.name, time=epoch ) r_f = icrs_frame_pos_coord.xyz + r_trans2 v_f = icrs_frame_vel_coord.xyz + v_trans2 return r_f.to(r.unit), v_f.to(v.unit) def icrs_to_body_centered(r, v, target_body, epoch=J2000, rotate_meridian=False): """Converts position and velocity in ICRS to body-centered frame. Parameters ---------- r : ~astropy.units.Quantity Position vector in ICRS. v : ~astropy.units.Quantity Velocity vector in ICRS. target_body : Body Target body. epoch : ~astropy.time.Time, optional Epoch, default to J2000. rotate_meridian : bool, optional Whether to apply the rotation of the meridian too, default to False. Returns ------- r, v : tuple (~astropy.units.Quantity) Position and velocity vectors in a body-centered reference frame. """ ra, dec, W = target_body.rot_elements_at_epoch(epoch) icrs_frame_pos_coord, icrs_frame_vel_coord = get_body_barycentric_posvel( target_body.name, time=epoch ) r_trans1 = r - icrs_frame_pos_coord.xyz r_trans2 = transform_vector(r_trans1, (90 * u.deg + ra), "z") r_f = transform_vector(r_trans2, (90 * u.deg - dec), "x") v_trans1 = v - icrs_frame_vel_coord.xyz v_trans2 = transform_vector(v_trans1, (90 * u.deg + ra), "z") v_f = transform_vector(v_trans2, (90 * u.deg - dec), "x") if rotate_meridian: r_f = transform_vector(r_f, W, "z") v_f = transform_vector(v_f, W, "z") return r_f.to(r.unit), v_f.to(v.unit) def inertial_body_centered_to_pqw(r, v, source_body): """Converts position and velocity from inertial body-centered frame to perifocal frame. Parameters ---------- r : ~astropy.units.Quantity Position vector in a inertial body-centered reference frame. v : ~astropy.units.Quantity Velocity vector in a inertial body-centered reference frame. source_body : Body Source body. Returns ------- r_pqw, v_pqw : tuple (~astropy.units.Quantity) Position and velocity vectors in ICRS. """ r = r.to("km").value v = v.to("km/s").value k = source_body.k.to("km^3 / s^2").value p, ecc, inc, _, _, nu = rv2coe(k, r, v) r_pqw = (np.array([cos(nu), sin(nu), 0 * nu]) * p / (1 + ecc * cos(nu))).T * u.km v_pqw = (np.array([-sin(nu), (ecc + cos(nu)), 0]) * sqrt(k / p)).T * u.km / u.s return r_pqw, v_pqw def transform(orbit, frame_orig, frame_dest): """Transforms Orbit from one frame to another. Parameters ---------- orbit : ~poliastro.bodies.Orbit Orbit to transform frame_orig : ~astropy.coordinates.BaseCoordinateFrame Initial frame frame_dest : ~astropy.coordinates.BaseCoordinateFrame Final frame Returns ------- orbit: ~poliastro.bodies.Orbit Orbit in the new frame """ orbit_orig = frame_orig( x=orbit.r[0], y=orbit.r[1], z=orbit.r[2], v_x=orbit.v[0], v_y=orbit.v[1], v_z=orbit.v[2], representation=CartesianRepresentation, differential_type=CartesianDifferential, ) orbit_dest = orbit_orig.transform_to(frame_dest(obstime=orbit.epoch)) orbit_dest.representation = CartesianRepresentation return Orbit.from_vectors( orbit.attractor, orbit_dest.data.xyz, orbit_dest.data.differentials["s"].d_xyz, epoch=orbit.epoch, ) poliastro-0.13.1/src/poliastro/core/000755 001750 001750 00000000000 13577132207 020210 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/core/__init__.py000644 001750 001750 00000000000 13525576227 022316 0ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/core/_jit.py000644 001750 001750 00000001252 13525576227 021516 0ustar00juanlujuanlu000000 000000 """Just-in-time compiler. Wraps numba if it is available as a module, uses an identity decorator instead. """ import inspect import warnings def ijit(first=None, *args, **kwargs): """Identity JIT, returns unchanged function. """ def _jit(f): return f if inspect.isfunction(first): return first else: return _jit try: import numba jit = numba.njit except ImportError: warnings.warn( "Could not import numba package. All poliastro " "functions will work properly but the CPU intensive " "algorithms will be slow. Consider installing numba to " "boost performance." ) jit = ijit poliastro-0.13.1/src/poliastro/core/angles.py000644 001750 001750 00000023430 13525576227 022044 0ustar00juanlujuanlu000000 000000 import numpy as np from ._jit import jit @jit def _kepler_equation(E, M, ecc): return E - ecc * np.sin(E) - M @jit def _kepler_equation_prime(E, M, ecc): return 1 - ecc * np.cos(E) @jit def _kepler_equation_hyper(F, M, ecc): return -F + ecc * np.sinh(F) - M @jit def _kepler_equation_prime_hyper(F, M, ecc): return ecc * np.cosh(F) - 1 @jit def _kepler_equation_parabolic(D, M, ecc): return M_parabolic(ecc, D) - M @jit def _kepler_equation_prime_parabolic(D, M, ecc): return M_parabolic_prime(ecc, D) @jit def M_parabolic(ecc, D, tolerance=1e-16): x = (ecc - 1.0) / (ecc + 1.0) * (D ** 2) small_term = False S = 0.0 k = 0 while not small_term: term = (ecc - 1.0 / (2.0 * k + 3.0)) * (x ** k) small_term = np.abs(term) < tolerance S += term k += 1 return ( np.sqrt(2.0 / (1.0 + ecc)) * D + np.sqrt(2.0 / (1.0 + ecc) ** 3) * (D ** 3) * S ) @jit def M_parabolic_prime(ecc, D, tolerance=1e-16): x = (ecc - 1.0) / (ecc + 1.0) * (D ** 2) small_term = False S_prime = 0.0 k = 0 while not small_term: term = (ecc - 1.0 / (2.0 * k + 3.0)) * (2 * k + 3.0) * (x ** k) small_term = np.abs(term) < tolerance S_prime += term k += 1 return ( np.sqrt(2.0 / (1.0 + ecc)) + np.sqrt(2.0 / (1.0 + ecc) ** 3) * (D ** 2) * S_prime ) @jit def newton(regime, x0, args=(), tol=1.48e-08, maxiter=50): p0 = 1.0 * x0 for iter in range(maxiter): if regime == "parabolic": fval = _kepler_equation_parabolic(p0, *args) fder = _kepler_equation_prime_parabolic(p0, *args) elif regime == "hyperbolic": fval = _kepler_equation_hyper(p0, *args) fder = _kepler_equation_prime_hyper(p0, *args) else: fval = _kepler_equation(p0, *args) fder = _kepler_equation_prime(p0, *args) newton_step = fval / fder p = p0 - newton_step if abs(p - p0) < tol: return p p0 = p return 1.0 @jit def D_to_nu(D): r"""True anomaly from parabolic eccentric anomaly. .. math:: \nu = 2 \cdot \arctan{(D)} Parameters ---------- D : ~astropy.units.Quantity Eccentric anomaly. Returns ------- nu : ~astropy.units.Quantity True anomaly. Note ---- Taken from Farnocchia, Davide, Davide Bracali Cioci, and Andrea Milani. "Robust resolution of Kepler’s equation in all eccentricity regimes." Celes """ return 2.0 * np.arctan(D) @jit def nu_to_D(nu): r"""Parabolic eccentric anomaly from true anomaly. .. math:: D = \tan{\frac{\nu}{2}} Parameters ---------- nu : ~astropy.units.Quantity True anomaly. Returns ------- D : ~astropy.units.Quantity Hyperbolic eccentric anomaly. Note ---- Taken from Farnocchia, Davide, Davide Bracali Cioci, and Andrea Milani. "Robust resolution of Kepler’s equation in all eccentricity regimes." Celestial Mechanics and Dynamical Astronomy 116, no. 1 (2013): 21-34. """ return np.tan(nu / 2.0) @jit def nu_to_E(nu, ecc): r"""Eccentric anomaly from true anomaly. .. versionadded:: 0.4.0 .. math:: E = 2\arctan{\sqrt{\frac{1-e}{1+e}}\tan{\frac{\nu}{2}}} Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- E : ~astropy.units.Quantity Eccentric anomaly. """ beta = ecc / (1 + np.sqrt(1 - (ecc ** 2))) E = nu - 2 * np.arctan(beta * np.sin(nu) / (1 + beta * np.cos(nu))) return E @jit def nu_to_F(nu, ecc): r"""Hyperbolic eccentric anomaly from true anomaly. .. math:: F = ln{\left ( \frac{\sin{(\nu)}\sqrt{e^{2}-1} + \cos{\nu} + e}{1+e\cos{(\nu)}} \right )} Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. Note ----- Taken from Curtis, H. (2013). *Orbital mechanics for engineering students*. 167 """ F = np.log( (np.sqrt(ecc + 1) + np.sqrt(ecc - 1) * np.tan(nu / 2)) / (np.sqrt(ecc + 1) - np.sqrt(ecc - 1) * np.tan(nu / 2)) ) return F @jit def E_to_nu(E, ecc): r"""True anomaly from eccentric anomaly. .. versionadded:: 0.4.0 .. math:: \nu = 2\arctan{\left ( \sqrt{\frac{1+e}{1-e}}\tan{\frac{E}{2}} \right )} Parameters ---------- E : ~astropy.units.Quantity Eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- nu : ~astropy.units.Quantity True anomaly. """ beta = ecc / (1 + np.sqrt((1 - ecc) * (1 + ecc))) nu = E + 2 * np.arctan(beta * np.sin(E) / (1 - beta * np.cos(E))) return nu @jit def F_to_nu(F, ecc): """True anomaly from hyperbolic eccentric anomaly. Parameters ---------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- nu : ~astropy.units.Quantity True anomaly. """ nu = 2 * np.arctan( (np.exp(F) * np.sqrt(ecc + 1) - np.sqrt(ecc + 1)) / (np.exp(F) * np.sqrt(ecc - 1) + np.sqrt(ecc - 1)) ) return nu @jit def M_to_E(M, ecc): """Eccentric anomaly from mean anomaly. .. versionadded:: 0.4.0 Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- E : ~astropy.units.Quantity Eccentric anomaly. """ E = newton("elliptic", M, args=(M, ecc)) return E @jit def M_to_F(M, ecc): """Hyperbolic eccentric anomaly from mean anomaly. Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. """ F = newton("hyperbolic", np.arcsinh(M / ecc), args=(M, ecc), maxiter=100) return F @jit def M_to_D(M, ecc): """Parabolic eccentric anomaly from mean anomaly. Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- D : ~astropy.units.Quantity Parabolic eccentric anomaly. """ B = 3.0 * M / 2.0 A = (B + (1.0 + B ** 2) ** 0.5) ** (2.0 / 3.0) guess = 2 * A * B / (1 + A + A ** 2) D = newton("parabolic", guess, args=(M, ecc), maxiter=100) return D @jit def E_to_M(E, ecc): """Mean anomaly from eccentric anomaly. .. versionadded:: 0.4.0 Parameters ---------- E : ~astropy.units.Quantity Eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ M = _kepler_equation(E, 0.0, ecc) return M @jit def F_to_M(F, ecc): """Mean anomaly from eccentric anomaly. Parameters ---------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ M = _kepler_equation_hyper(F, 0.0, ecc) return M @jit def D_to_M(D, ecc): """Mean anomaly from eccentric anomaly. Parameters ---------- D : ~astropy.units.Quantity Parabolic eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ M = _kepler_equation_parabolic(D, 0.0, ecc) return M @jit def M_to_nu(M, ecc, delta=1e-2): """True anomaly from mean anomaly. .. versionadded:: 0.4.0 Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity. delta : float (optional) threshold of near-parabolic regime definition (from Davide Farnocchia et al) Returns ------- nu : ~astropy.units.Quantity True anomaly. Examples -------- >>> from numpy import radians, degrees >>> degrees(M_to_nu(radians(30.0), 0.06)) 33.67328493021166 """ if ecc > 1 + delta: F = M_to_F(M, ecc) nu = F_to_nu(F, ecc) elif ecc < 1 - delta: E = M_to_E(M, ecc) nu = E_to_nu(E, ecc) else: D = M_to_D(M, ecc) nu = D_to_nu(D) return nu @jit def nu_to_M(nu, ecc, delta=1e-2): """Mean anomaly from true anomaly. .. versionadded:: 0.4.0 Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity. delta : float (optional) threshold of near-parabolic regime definition (from Davide Farnocchia et al) Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ if ecc > 1 + delta: F = nu_to_F(nu, ecc) M = F_to_M(F, ecc) elif ecc < 1 - delta: E = nu_to_E(nu, ecc) M = E_to_M(E, ecc) else: D = nu_to_D(nu) M = D_to_M(D, ecc) return M @jit def fp_angle(nu, ecc): r"""Returns the flight path angle. .. math:: \gamma = \arctan{\frac{e\sin{\theta}}{1 + e\cos{\theta}}} Parameters ---------- nu: ~astropy.units.Quantity True anomaly ecc: ~astropy.units.Quantity Eccentricity Returns fp_angle: ~astropy.units.Quantity Flight path angle Note ----- Algorithm taken from Vallado 2007, pp. 113. """ return np.arctan2(ecc * np.sin(nu), 1 + ecc * np.cos(nu)) poliastro-0.13.1/src/poliastro/core/elements.py000644 001750 001750 00000030311 13577127432 022400 0ustar00juanlujuanlu000000 000000 """This module contains a set of functions that can be used to convert between different elements that define the orbit of a body. """ import numpy as np from numpy.core.umath import cos, sin, sqrt from poliastro.core.angles import E_to_nu, F_to_nu from poliastro.core.util import cross, norm, rotation_matrix from ._jit import jit @jit def rv_pqw(k, p, ecc, nu): r"""Returns r and v vectors in perifocal frame. .. math:: \vec{r} = \frac{h^2}{\mu}\frac{1}{1 + e\cos(\theta)}\begin{bmatrix} \cos(\theta)\\ \sin(\theta)\\ 0 \end{bmatrix} \\\\\\ \vec{v} = \frac{h^2}{\mu}\begin{bmatrix} -\sin(\theta)\\ e+\cos(\theta)\\ 0 \end{bmatrix} Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2). p : float Semi-latus rectum or parameter (km). ecc : float Eccentricity. nu: float True anomaly (rad). Returns ------- r: ndarray Position. Dimension 3 vector v: ndarray Velocity. Dimension 3 vector Examples -------- >>> from poliastro.constants import GM_earth >>> k = GM_earth #Earth gravitational parameter >>> ecc = 0.3 #Eccentricity >>> h = 60000e6 #Angular momentum of the orbit [m^2]/[s] >>> nu = np.deg2rad(120) #True Anomaly [rad] >>> p = h**2 / k #Parameter of the orbit >>> r, v = rv_pqw(k, p, ecc, nu) >>> #Printing the results r = [-5312706.25105345 9201877.15251336 0] [m] v = [-5753.30180931 -1328.66813933 0] [m]/[s] Note ---- These formulas can be checked at Curtis 3rd. Edition, page 110. Also the example proposed is 2.11 of Curtis 3rd Edition book. """ r_pqw = (np.array([cos(nu), sin(nu), 0 * nu]) * p / (1 + ecc * cos(nu))).T v_pqw = (np.array([-sin(nu), (ecc + cos(nu)), 0]) * sqrt(k / p)).T return r_pqw, v_pqw @jit def coe_rotation_matrix(inc, raan, argp): """Create a rotation matrix for coe transformation """ r = rotation_matrix(raan, 2) r = r @ rotation_matrix(inc, 0) r = r @ rotation_matrix(argp, 2) return r @jit def coe2rv(k, p, ecc, inc, raan, argp, nu): r"""Converts from classical orbital to state vectors. Classical orbital elements are converted into position and velocity vectors by `rv_pqw` algorithm. A rotation matrix is applied to position and velocity vectors to get them expressed in terms of an IJK basis. .. math:: \begin{align} \vec{r}_{IJK} &= [ROT3(-\Omega)][ROT1(-i)][ROT3(-\omega)]\vec{r}_{PQW} = \left [ \frac{IJK}{PQW} \right ]\vec{r}_{PQW}\\ \vec{v}_{IJK} &= [ROT3(-\Omega)][ROT1(-i)][ROT3(-\omega)]\vec{v}_{PQW} = \left [ \frac{IJK}{PQW} \right ]\vec{v}_{PQW}\\ \end{align} Previous rotations (3-1-3) can be expressed in terms of a single rotation matrix: .. math:: \left [ \frac{IJK}{PQW} \right ] .. math:: \begin{bmatrix} \cos(\Omega)\cos(\omega) - \sin(\Omega)\sin(\omega)\cos(i) & -\cos(\Omega)\sin(\omega) - \sin(\Omega)\cos(\omega)\cos(i) & \sin(\Omega)\sin(i)\\ \sin(\Omega)\cos(\omega) + \cos(\Omega)\sin(\omega)\cos(i) & -\sin(\Omega)\sin(\omega) + \cos(\Omega)\cos(\omega)\cos(i) & -\cos(\Omega)\sin(i)\\ \sin(\omega)\sin(i) & \cos(\omega)\sin(i) & \cos(i) \end{bmatrix} 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). omega : float Longitude of ascending node (rad). argp : float Argument of perigee (rad). nu : float True anomaly (rad). Returns ------- r_ijk: np.array Position vector in basis ijk v_ijk: np.array Velocity vector in basis ijk """ r_pqw, v_pqw = rv_pqw(k, p, ecc, nu) rm = coe_rotation_matrix(inc, raan, argp) r_ijk = rm @ r_pqw v_ijk = rm @ v_pqw return r_ijk, v_ijk @jit def coe2mee(p, ecc, inc, raan, argp, nu): r"""Converts from classical orbital elements to modified equinoctial orbital elements. The definition of the modified equinoctial orbital elements is taken from [Walker, 1985]. The modified equinoctial orbital elements are a set of orbital elements that are useful for trajectory analysis and optimization. They are valid for circular, elliptic, and hyperbolic orbits. These direct modified equinoctial equations exhibit no singularity for zero eccentricity and orbital inclinations equal to 0 and 90 degrees. However, two of the components are singular for an orbital inclination of 180 degrees. .. math:: \begin{align} p &= a(1-e^2) \\ f &= e\cos(\omega + \Omega) \\ g &= e\sin(\omega + \Omega) \\ h &= \tan(\frac{i}{2})\cos(\Omega) \\ k &= \tan(\frac{i}{2})\sin(\Omega) \\ L &= \Omega + \omega + \theta \\ \end{align} 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). omega : float Longitude of ascending node (rad). argp : float Argument of perigee (rad). nu : float True anomaly (rad). Returns ------- p: float Semi-latus rectum or parameter f: float Equinoctial parameter f g: float Equinoctial parameter g h: float Equinoctial parameter h k: float Equinoctial parameter k L: float Longitude Note ----- The conversion equations are taken directly from the original paper. """ lonper = raan + argp f = ecc * np.cos(lonper) g = ecc * np.sin(lonper) # TODO: Check polar case (see [Walker, 1985]) h = np.tan(inc / 2) * np.cos(raan) k = np.tan(inc / 2) * np.sin(raan) L = lonper + nu return p, f, g, h, k, L @jit def rv2coe(k, r, v, tol=1e-8): r"""Converts from vectors to classical orbital elements. 1. First the angular momentum is computed: .. math:: \vec{h} = \vec{r} \times \vec{v} 2. With it the eccentricity can be solved: .. math:: \begin{align} \vec{e} &= \frac{1}{\mu}\left [ \left ( v^{2} - \frac{\mu}{r}\right ) \vec{r} - (\vec{r} \cdot \vec{v})\vec{v} \right ] \\ e &= \sqrt{\vec{e}\cdot\vec{e}} \\ \end{align} 3. The node vector line is solved: .. math:: \begin{align} \vec{N} &= \vec{k} \times \vec{h} \\ N &= \sqrt{\vec{N}\cdot\vec{N}} \end{align} 4. The rigth ascension node is computed: .. math:: \Omega = \left\{ \begin{array}{lcc} cos^{-1}{\left ( \frac{N_{x}}{N} \right )} & if & N_{y} \geq 0 \\ \\ 360^{o} -cos^{-1}{\left ( \frac{N_{x}}{N} \right )} & if & N_{y} < 0 \\ \end{array} \right. 5. The argument of perigee: .. math:: \omega = \left\{ \begin{array}{lcc} cos^{-1}{\left ( \frac{\vec{N}\vec{e}}{Ne} \right )} & if & e_{z} \geq 0 \\ \\ 360^{o} -cos^{-1}{\left ( \frac{\vec{N}\vec{e}}{Ne} \right )} & if & e_{z} < 0 \\ \end{array} \right. 6. And finally the true anomaly: .. math:: \nu = \left\{ \begin{array}{lcc} cos^{-1}{\left ( \frac{\vec{e}\vec{r}}{er} \right )} & if & v_{r} \geq 0 \\ \\ 360^{o} -cos^{-1}{\left ( \frac{\vec{e}\vec{r}}{er} \right )} & if & v_{r} < 0 \\ \end{array} \right. Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2) r : array Position vector (km) v : array Velocity vector (km / s) tol : float, optional Tolerance for eccentricity and inclination checks, default to 1e-8 Returns ------- p : float Semi-latus rectum of parameter (km) ecc: float Eccentricity inc: float Inclination (rad) raan: float Right ascension of the ascending nod (rad) argp: float Argument of Perigee (rad) nu: float True Anomaly (rad) Examples -------- >>> from poliastro.constants import GM_earth >>> from astropy import units as u >>> k = GM_earth.to(u.km ** 3 / u.s ** 2).value # Earth gravitational parameter >>> r = np.array([-6045., -3490., 2500.]) >>> v = np.array([-3.457, 6.618, 2.533]) >>> p, ecc, inc, raan, argp, nu = rv2coe(k, r, v) >>> print("p:", p, "[km]") p: 8530.47436396927 [km] >>> print("ecc:", ecc) ecc: 0.17121118195416898 >>> print("inc:", np.rad2deg(inc), "[deg]") inc: 153.2492285182475 [deg] >>> print("raan:", np.rad2deg(raan), "[deg]") raan: 255.27928533439618 [deg] >>> print("argp:", np.rad2deg(argp), "[deg]") argp: 20.068139973005366 [deg] >>> print("nu:", np.rad2deg(nu), "[deg]") nu: 28.445804984192122 [deg] Note ---- This example is a real exercise from Orbital Mechanics for Engineering students by Howard D.Curtis. This exercise is 4.3 of 3rd. Edition, page 200. """ h = cross(r, v) n = cross([0, 0, 1], h) e = ((v.dot(v) - k / (norm(r))) * r - r.dot(v) * v) / k ecc = norm(e) p = h.dot(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 nu = np.arctan2(h.dot(cross(e, r)) / norm(h), r.dot(e)) elif not equatorial and circular: raan = np.arctan2(n[1], n[0]) % (2 * np.pi) argp = 0 # Argument of latitude nu = np.arctan2(r.dot(cross(h, n)) / norm(h), r.dot(n)) elif equatorial and circular: raan = 0 argp = 0 nu = np.arctan2(r[1], r[0]) % (2 * np.pi) # True longitude else: a = p / (1 - (ecc ** 2)) ka = k * a if a > 0: e_se = r.dot(v) / sqrt(ka) e_ce = norm(r) * (norm(v) ** 2) / k - 1 nu = E_to_nu(np.arctan2(e_se, e_ce), ecc) else: e_sh = r.dot(v) / sqrt(-ka) e_ch = norm(r) * (norm(v) ** 2) / k - 1 nu = F_to_nu(np.log((e_ch + e_sh) / (e_ch - e_sh)) / 2, ecc) raan = np.arctan2(n[1], n[0]) % (2 * np.pi) px = r.dot(n) py = r.dot(cross(h, n)) / norm(h) argp = (np.arctan2(py, px) - nu) % (2 * np.pi) nu = (nu + np.pi) % (2 * np.pi) - np.pi return p, ecc, inc, raan, argp, nu @jit def mee2coe(p, f, g, h, k, L): r"""Converts from modified equinoctial orbital elements to classical orbital elements. The definition of the modified equinoctial orbital elements is taken from [Walker, 1985]. .. math:: \begin{align} p &= a(1 - e^{2})\\ e &= \sqrt{f^{2} + g^{2}}\\ i &= 2\arctan{(\sqrt{h^{2} + k^{2}})}\\ raan &= atan2(k, h) \pmod{2\pi}\\ argp &= (atan2(g, f) - raan) \pmod{2\pi}\\ nu &= (L - atan2(g, f)) \pmod{2\pi}\\ \end{align} Parameters ---------- p: float Semi-latus rectum f: float Equinoctial parameter p g: float Equinoctial parameter g h: float Equinoctial parameter h k: float Equinoctial parameter k L: float Longitude Returns ------- p: float Semi-latus rectum ecc: float Eccentricity of the orbit inc: float Inclination of the orbit raan: float RAAN of orbit argp: float Argument of the periapsis nu: float True anomaly Note ----- The conversion is always safe because arctan2 works also for 0, 0 arguments. """ ecc = np.sqrt(f ** 2 + g ** 2) inc = 2 * np.arctan(np.sqrt(h ** 2 + k ** 2)) lonper = np.arctan2(g, f) raan = np.arctan2(k, h) % (2 * np.pi) argp = (lonper - raan) % (2 * np.pi) nu = (L - lonper) % (2 * np.pi) return p, ecc, inc, raan, argp, nu poliastro-0.13.1/src/poliastro/core/hyper.py000644 001750 001750 00000001313 13525576227 021716 0ustar00juanlujuanlu000000 000000 """Utility hypergeometric functions. """ import numpy as np from ._jit import jit @jit def hyp2f1b(x): """Hypergeometric function 2F1(3, 1, 5/2, x), see [Battin]. .. todo:: Add more information about this function Note ---- More information about hypergeometric function can be checked at https://en.wikipedia.org/wiki/Hypergeometric_function """ if x >= 1.0: return np.inf else: res = 1.0 term = 1.0 ii = 0 while True: term = term * (3 + ii) * (1 + ii) / (5 / 2 + ii) * x / (ii + 1) res_old = res res += term if res_old == res: return res ii += 1 poliastro-0.13.1/src/poliastro/core/iod.py000644 001750 001750 00000030367 13577127432 021352 0ustar00juanlujuanlu000000 000000 import numpy as np from numpy import pi from poliastro.core.hyper import hyp2f1b from poliastro.core.stumpff import c2, c3 from poliastro.core.util import cross, norm from ._jit import jit @jit def vallado(k, r0, r, tof, short, numiter, rtol): r"""Solves the Lambert's problem. The algorithm returns the initial velocity vector and the final one, these are computed by the following expresions: .. math:: \vec{v_{o}} &= \frac{1}{g}(\vec{r} - f\vec{r_{0}}) \\ \vec{v} &= \frac{1}{g}(\dot{g}\vec{r} - \vec{r_{0}}) Therefore, the lagrange coefficients need to be computed. For the case of Lamber's problem, they can be expressed by terms of the initial and final vector: .. math:: \begin{align} f = 1 -\frac{y}{r_{o}} \\ g = A\sqrt{\frac{y}{\mu}} \\ \dot{g} = 1 - \frac{y}{r} \\ \end{align} Where y(z) is a function that depends on the :py:mod:`poliastro.core.stumpff` coefficients: .. math:: y = r_{o} + r + A\frac{zS(z)-1}{\sqrt{C(z)}} \\ A = \sin{(\Delta \nu)}\sqrt{\frac{rr_{o}}{1 - \cos{(\Delta \nu)}}} The value of z to evaluate the stump functions is solved by applying a Numerical method to the following equation: .. math:: z_{i+1} = z_{i} - \frac{F(z_{i})}{F{}'(z_{i})} Function F(z) to the expression: .. math:: F(z) = \left [\frac{y(z)}{C(z)} \right ]^{\frac{3}{2}}S(z) + A\sqrt{y(z)} - \sqrt{\mu}\Delta t Parameters ---------- k: float Gravitational Parameter r0: ~np.array Initial position vector r: ~np.array Final position vector tof: ~float Time of flight numiter: int Number of iterations to rtol: int Number of revolutions Returns ------- v0: ~np.array Initial velocity vector v: ~np.array Final velocity vector Examples -------- >>> from poliastro.core.iod import vallado >>> from astropy import units as u >>> import numpy as np >>> from poliastro.bodies import Earth >>> k = Earth.k.to(u.km**3 / u.s**2) >>> r1 = np.array([5000, 10000, 2100])*u.km #Initial position vector >>> r2 = np.array([-14600, 2500, 7000])*u.km #Final position vector >>> tof = 3600*u.s #Time of fligh >>> v1, v2 = vallado(k.value, r1.value, r2.value, tof.value, short=True, numiter=35, rtol=1e-8) >>> v1 = v1*u.km / u.s >>> v2 = v2*u.km / u.s >>> print(v1, v2) [-5.99249499 1.92536673 3.24563805] km / s [-3.31245847 -4.196619 -0.38528907] km / s Note ---- This procedure can be found in section 5.3 of Curtis, with all the theoretical description of the problem. Analytical example can be found in the same book under name Example 5.2. """ if short: t_m = +1 else: t_m = -1 norm_r0 = np.dot(r0, r0) ** 0.5 norm_r = np.dot(r, r) ** 0.5 norm_r0_times_norm_r = norm_r0 * norm_r norm_r0_plus_norm_r = norm_r0 + norm_r cos_dnu = np.dot(r0, r) / norm_r0_times_norm_r A = t_m * (norm_r * norm_r0 * (1 + cos_dnu)) ** 0.5 if A == 0.0: raise RuntimeError("Cannot compute orbit, phase angle is 180 degrees") psi = 0.0 psi_low = -4 * np.pi psi_up = 4 * np.pi count = 0 while count < numiter: y = norm_r0_plus_norm_r + A * (psi * c3(psi) - 1) / c2(psi) ** 0.5 if A > 0.0: # Readjust xi_low until y > 0.0 # Translated directly from Vallado while y < 0.0: psi_low = psi psi = ( 0.8 * (1.0 / c3(psi)) * (1.0 - norm_r0_times_norm_r * np.sqrt(c2(psi)) / A) ) y = norm_r0_plus_norm_r + A * (psi * c3(psi) - 1) / c2(psi) ** 0.5 xi = np.sqrt(y / c2(psi)) tof_new = (xi ** 3 * c3(psi) + A * np.sqrt(y)) / np.sqrt(k) # Convergence check if np.abs((tof_new - tof) / tof) < rtol: break count += 1 # Bisection check condition = tof_new <= tof psi_low = psi_low + (psi - psi_low) * condition psi_up = psi_up + (psi - psi_up) * (not condition) psi = (psi_up + psi_low) / 2 else: raise RuntimeError("Maximum number of iterations reached") f = 1 - y / norm_r0 g = A * np.sqrt(y / k) gdot = 1 - y / norm_r v0 = (r - f * r0) / g v = (gdot * r - r0) / g return v0, v @jit def izzo(k, r1, r2, tof, M, numiter, rtol): """ Aplies izzo algorithm to solve Lambert's problem. Parameters ---------- k: float Gravitational Constant r1: ~numpy.array Initial position vector r2: ~numpy.array Final position vector tof: float Time of flight between both positions M: int Number of revolutions numiter: int Numbert of iterations rtol: float Error tolerance Returns ------- v1: ~numpy.array Initial velocity vector v2: ~numpy.array Final velocity vector """ # Check preconditions assert tof > 0 assert k > 0 # Check collinearity of r1 and r2 if np.all(cross(r1, r2) == 0): raise ValueError("Lambert solution cannot be computed for collinear vectors") # Chord c = r2 - r1 c_norm, r1_norm, r2_norm = norm(c), norm(r1), norm(r2) # Semiperimeter s = (r1_norm + r2_norm + c_norm) * 0.5 # Versors i_r1, i_r2 = r1 / r1_norm, r2 / r2_norm i_h = cross(i_r1, i_r2) i_h = i_h / norm(i_h) # Fixed from paper # Geometry of the problem ll = np.sqrt(1 - min(1.0, c_norm / s)) if i_h[2] < 0: ll = -ll i_h = -i_h i_t1, i_t2 = cross(i_h, i_r1), cross(i_h, i_r2) # Fixed from paper # Non dimensional time of flight T = np.sqrt(2 * k / s ** 3) * tof # Find solutions xy = _find_xy(ll, T, M, numiter, rtol) # Reconstruct gamma = np.sqrt(k * s / 2) rho = (r1_norm - r2_norm) / c_norm sigma = np.sqrt(1 - rho ** 2) for x, y in xy: V_r1, V_r2, V_t1, V_t2 = _reconstruct( x, y, r1_norm, r2_norm, ll, gamma, rho, sigma ) v1 = V_r1 * i_r1 + V_t1 * i_t1 v2 = V_r2 * i_r2 + V_t2 * i_t2 yield v1, v2 @jit def _reconstruct(x, y, r1, r2, ll, gamma, rho, sigma): """Reconstruct solution velocity vectors. """ V_r1 = gamma * ((ll * y - x) - rho * (ll * y + x)) / r1 V_r2 = -gamma * ((ll * y - x) + rho * (ll * y + x)) / r2 V_t1 = gamma * sigma * (y + ll * x) / r1 V_t2 = gamma * sigma * (y + ll * x) / r2 return [V_r1, V_r2, V_t1, V_t2] @jit def _find_xy(ll, T, M, numiter, rtol): """Computes all x, y for given number of revolutions. """ # For abs(ll) == 1 the derivative is not continuous assert abs(ll) < 1 assert T > 0 # Mistake on original paper M_max = np.floor(T / pi) T_00 = np.arccos(ll) + ll * np.sqrt(1 - ll ** 2) # T_xM # Refine maximum number of revolutions if necessary if T < T_00 + M_max * pi and M_max > 0: _, T_min = _compute_T_min(ll, M_max, numiter, rtol) if T < T_min: M_max -= 1 # Check if a feasible solution exist for the given number of revolutions # This departs from the original paper in that we do not compute all solutions if M > M_max: raise ValueError("No feasible solution, try lower M") # Initial guess for x_0 in _initial_guess(T, ll, M): # Start Householder iterations from x_0 and find x, y x = _householder(x_0, T, ll, M, rtol, numiter) y = _compute_y(x, ll) yield x, y @jit def _compute_y(x, ll): """Computes y. """ return np.sqrt(1 - ll ** 2 * (1 - x ** 2)) @jit def _compute_psi(x, y, ll): """Computes psi. "The auxiliary angle psi is computed using Eq.(17) by the appropriate inverse function" """ if -1 <= x < 1: # Elliptic motion # Use arc cosine to avoid numerical errors return np.arccos(x * y + ll * (1 - x ** 2)) elif x > 1: # Hyperbolic motion # The hyperbolic sine is bijective return np.arcsinh((y - x * ll) * np.sqrt(x ** 2 - 1)) else: # Parabolic motion return 0.0 @jit def _tof_equation(x, T0, ll, M): """Time of flight equation. """ return _tof_equation_y(x, _compute_y(x, ll), T0, ll, M) @jit def _tof_equation_y(x, y, T0, ll, M): """Time of flight equation with externally computated y. """ if M == 0 and np.sqrt(0.6) < x < np.sqrt(1.4): eta = y - ll * x S_1 = (1 - ll - x * eta) * 0.5 Q = 4 / 3 * hyp2f1b(S_1) T_ = (eta ** 3 * Q + 4 * ll * eta) * 0.5 else: psi = _compute_psi(x, y, ll) T_ = np.divide( np.divide(psi + M * pi, np.sqrt(np.abs(1 - x ** 2))) - x + ll * y, (1 - x ** 2), ) return T_ - T0 @jit def _tof_equation_p(x, y, T, ll): # TODO: What about derivatives when x approaches 1? return (3 * T * x - 2 + 2 * ll ** 3 * x / y) / (1 - x ** 2) @jit def _tof_equation_p2(x, y, T, dT, ll): return (3 * T + 5 * x * dT + 2 * (1 - ll ** 2) * ll ** 3 / y ** 3) / (1 - x ** 2) @jit def _tof_equation_p3(x, y, _, dT, ddT, ll): return (7 * x * ddT + 8 * dT - 6 * (1 - ll ** 2) * ll ** 5 * x / y ** 5) / ( 1 - x ** 2 ) @jit def _compute_T_min(ll, M, numiter, rtol): """Compute minimum T. """ if ll == 1: x_T_min = 0.0 T_min = _tof_equation(x_T_min, 0.0, ll, M) else: if M == 0: x_T_min = np.inf T_min = 0.0 else: # Set x_i > 0 to avoid problems at ll = -1 x_i = 0.1 T_i = _tof_equation(x_i, 0.0, ll, M) x_T_min = _halley(x_i, T_i, ll, rtol, numiter) T_min = _tof_equation(x_T_min, 0.0, ll, M) return [x_T_min, T_min] @jit def _initial_guess(T, ll, M): """Initial guess. """ if M == 0: # Single revolution T_0 = np.arccos(ll) + ll * np.sqrt(1 - ll ** 2) + M * pi # Equation 19 T_1 = 2 * (1 - ll ** 3) / 3 # Equation 21 if T >= T_0: x_0 = (T_0 / T) ** (2 / 3) - 1 elif T < T_1: x_0 = 5 / 2 * T_1 / T * (T_1 - T) / (1 - ll ** 5) + 1 else: # This is the real condition, which is not exactly equivalent # elif T_1 < T < T_0 x_0 = (T_0 / T) ** (np.log2(T_1 / T_0)) - 1 return [x_0] else: # Multiple revolution x_0l = (((M * pi + pi) / (8 * T)) ** (2 / 3) - 1) / ( ((M * pi + pi) / (8 * T)) ** (2 / 3) + 1 ) x_0r = (((8 * T) / (M * pi)) ** (2 / 3) - 1) / ( ((8 * T) / (M * pi)) ** (2 / 3) + 1 ) return [x_0l, x_0r] @jit def _halley(p0, T0, ll, tol, maxiter): """Find a minimum of time of flight equation using the Halley method. Note ---- This function is private because it assumes a calling convention specific to this module and is not really reusable. """ for ii in range(maxiter): y = _compute_y(p0, ll) fder = _tof_equation_p(p0, y, T0, ll) fder2 = _tof_equation_p2(p0, y, T0, fder, ll) if fder2 == 0: raise RuntimeError("Derivative was zero") fder3 = _tof_equation_p3(p0, y, T0, fder, fder2, ll) # Halley step (cubic) p = p0 - 2 * fder * fder2 / (2 * fder2 ** 2 - fder * fder3) if abs(p - p0) < tol: return p p0 = p raise RuntimeError("Failed to converge") @jit def _householder(p0, T0, ll, M, tol, maxiter): """Find a zero of time of flight equation using the Householder method. Note ---- This function is private because it assumes a calling convention specific to this module and is not really reusable. """ for ii in range(maxiter): y = _compute_y(p0, ll) fval = _tof_equation_y(p0, y, T0, ll, M) T = fval + T0 fder = _tof_equation_p(p0, y, T, ll) fder2 = _tof_equation_p2(p0, y, T, fder, ll) fder3 = _tof_equation_p3(p0, y, T, fder, fder2, ll) # Householder step (quartic) p = p0 - fval * ( (fder ** 2 - fval * fder2 / 2) / (fder * (fder ** 2 - fval * fder2) + fder3 * fval ** 2 / 6) ) if abs(p - p0) < tol: return p p0 = p raise RuntimeError("Failed to converge") poliastro-0.13.1/src/poliastro/core/perturbations.py000644 001750 001750 00000014105 13577127432 023470 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.util import norm from ._jit import jit @jit def J2_perturbation(t0, state, k, J2, R): r"""Calculates J2_perturbation acceleration (km/s2) .. math:: \vec{p} = \frac{3}{2}\frac{J_{2}\mu R^{2}}{r^{4}}\left [\frac{x}{r}\left ( 5\frac{z^{2}}{r^{2}}-1 \right )\vec{i} + \frac{y}{r}\left ( 5\frac{z^{2}}{r^{2}}-1 \right )\vec{j} + \frac{z}{r}\left ( 5\frac{z^{2}}{r^{2}}-3 \right )\vec{k}\right] .. versionadded:: 0.9.0 Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float gravitational constant, (km^3/s^2) J2: float oblateness factor R: float attractor radius Note ---- The J2 accounts for the oblateness of the attractor. The formula is given in Howard Curtis, (12.30) """ r_vec = state[:3] r = norm(r_vec) factor = (3.0 / 2.0) * k * J2 * (R ** 2) / (r ** 5) a_x = 5.0 * r_vec[2] ** 2 / r ** 2 - 1 a_y = 5.0 * r_vec[2] ** 2 / r ** 2 - 1 a_z = 5.0 * r_vec[2] ** 2 / r ** 2 - 3 return np.array([a_x, a_y, a_z]) * r_vec * factor @jit def J3_perturbation(t0, state, k, J3, R): r"""Calculates J3_perturbation acceleration (km/s2) Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float gravitational constant, (km^3/s^2) J3: float oblateness factor R: float attractor radius Note ---- The J3 accounts for the oblateness of the attractor. The formula is given in Howard Curtis, problem 12.8 This perturbation has not been fully validated, see https://github.com/poliastro/poliastro/pull/398 """ r_vec = state[:3] r = norm(r_vec) factor = (1.0 / 2.0) * k * J3 * (R ** 3) / (r ** 5) cos_phi = r_vec[2] / r a_x = 5.0 * r_vec[0] / r * (7.0 * cos_phi ** 3 - 3.0 * cos_phi) a_y = 5.0 * r_vec[1] / r * (7.0 * cos_phi ** 3 - 3.0 * cos_phi) a_z = 3.0 * (35.0 / 3.0 * cos_phi ** 4 - 10.0 * cos_phi ** 2 + 1) return np.array([a_x, a_y, a_z]) * factor @jit def atmospheric_drag(t0, state, k, R, C_D, A, m, H0, rho0): r"""Calculates atmospheric drag acceleration (km/s2) .. math:: \vec{p} = -\frac{1}{2}\rho v_{rel}\left ( \frac{C_{d}A}{m} \right )\vec{v_{rel}} .. versionadded:: 0.9.0 Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float gravitational constant, (km^3/s^2) C_D: float dimensionless drag coefficient () A: float frontal area of the spacecraft (km^2) m: float mass of the spacecraft (kg) H0 : float atmospheric scale height, (km) rho0: float the exponent density pre-factor, (kg / m^3) Note ---- This function provides the acceleration due to atmospheric drag. We follow Howard Curtis, section 12.4 the atmospheric density model is rho(H) = rho0 x exp(-H / H0) """ H = norm(state[:3]) v_vec = state[3:] v = norm(v_vec) B = C_D * A / m rho = rho0 * np.exp(-(H - R) / H0) return -(1.0 / 2.0) * rho * B * v * v_vec @jit def shadow_function(r_sat, r_sun, R): r"""Determines whether the satellite is in attractor's shadow, uses algorithm 12.3 from Howard Curtis Parameters ---------- r_sat : numpy.ndarray position of the satellite in the frame of attractor (km) r_sun : numpy.ndarray position of star in the frame of attractor (km) R : float radius of body (attractor) that creates shadow (km) """ r_sat_norm = np.sqrt(np.sum(r_sat ** 2)) r_sun_norm = np.sqrt(np.sum(r_sun ** 2)) theta = np.arccos(np.dot(r_sat, r_sun) / r_sat_norm / r_sun_norm) theta_1 = np.arccos(R / r_sat_norm) theta_2 = np.arccos(R / r_sun_norm) return theta < theta_1 + theta_2 def third_body(t0, state, k, k_third, third_body): r"""Calculates 3rd body acceleration (km/s2) .. math:: \vec{p} = \mu_{m}\left ( \frac{\vec{r_{m/s}}}{r_{m/s}^3} - \frac{\vec{r_{m}}}{r_{m}^3} \right ) Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float gravitational constant, (km^3/s^2) third_body: a callable object returning the position of 3rd body third body that causes the perturbation Note ---- This formula is taken from Howard Curtis, section 12.10. As an example, a third body could be the gravity from the Moon acting on a small satellite. """ body_r = third_body(t0) delta_r = body_r - state[:3] return k_third * delta_r / norm(delta_r) ** 3 - k_third * body_r / norm(body_r) ** 3 def radiation_pressure(t0, state, k, R, C_R, A, m, Wdivc_s, star): r"""Calculates radiation pressure acceleration (km/s2) .. math:: \vec{p} = -\nu \frac{S}{c} \left ( \frac{C_{r}A}{m} \right )\frac{\vec{r}}{r} Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float gravitational constant, (km^3/s^2) R : float radius of the attractor C_R: float dimensionless radiation pressure coefficient, 1 < C_R < 2 () A: float effective spacecraft area (km^2) m: float mass of the spacecraft (kg) Wdivc_s : float total star emitted power divided by the speed of light (W * s / km) star: a callable object returning the position of star in attractor frame star position Note ---- This function provides the acceleration due to star light pressure. We follow Howard Curtis, section 12.9 """ r_star = star(t0) r_sat = state[:3] P_s = Wdivc_s / (norm(r_star) ** 2) nu = float(shadow_function(r_sat, r_star, R)) return -nu * P_s * (C_R * A / m) * r_star / norm(r_star) poliastro-0.13.1/src/poliastro/core/propagation.py000644 001750 001750 00000056607 13525576227 023132 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.angles import ( D_to_nu, E_to_nu, F_to_nu, M_to_nu, _kepler_equation, _kepler_equation_prime, nu_to_M, ) from poliastro.core.elements import coe2rv, rv2coe from poliastro.core.stumpff import c2, c3 from ._jit import jit def func_twobody(t0, u_, k, ad, ad_kwargs): """Differential equation for the initial value two body problem. This function follows Cowell's formulation. Parameters ---------- t0 : float Time. u_ : ~numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard gravitational parameter. ad : function(t0, u, k) Non Keplerian acceleration (km/s2). ad_kwargs : optional perturbation parameters passed to ad. """ ax, ay, az = ad(t0, u_, k, **ad_kwargs) x, y, z, vx, vy, vz = u_ r3 = (x ** 2 + y ** 2 + z ** 2) ** 1.5 du = np.array([vx, vy, vz, -k * x / r3 + ax, -k * y / r3 + ay, -k * z / r3 + az]) return du @jit def mean_motion(k, r0, v0, tof): r"""Propagates orbit using mean motion. This algorithm depends on the geometric shape of the orbit. For the case of the strong elliptic or strong hyperbolic orbits: .. math:: M = M_{0} + \frac{\mu^{2}}{h^{3}}\left ( 1 -e^{2}\right )^{\frac{3}{2}}t .. versionadded:: 0.9.0 Parameters ---------- k : float Standar Gravitational parameter r0 : ~astropy.units.Quantity Initial position vector wrt attractor center. v0 : ~astropy.units.Quantity Initial velocity vector. tof : float Time of flight (s). Note ---- This method takes initial :math:`\vec{r}, \vec{v}`, calculates classical orbit parameters, increases mean anomaly and performs inverse transformation to get final :math:`\vec{r}, \vec{v}` The logic is based on formulae (4), (6) and (7) from http://dx.doi.org/10.1007/s10569-013-9476-9 """ # get the initial true anomaly and orbit parameters that are constant over time p, ecc, inc, raan, argp, nu0 = rv2coe(k, r0, v0) # get the initial mean anomaly M0 = nu_to_M(nu0, ecc) # strong elliptic or strong hyperbolic orbits if np.abs(ecc - 1.0) > 1e-2: a = p / (1.0 - ecc ** 2) # given the initial mean anomaly, calculate mean anomaly # at the end, mean motion (n) equals sqrt(mu / |a^3|) M = M0 + tof * np.sqrt(k / np.abs(a ** 3)) nu = M_to_nu(M, ecc) # near-parabolic orbit else: q = p * np.abs(1.0 - ecc) / np.abs(1.0 - ecc ** 2) # mean motion n = sqrt(mu / 2 q^3) for parabolic orbit M = M0 + tof * np.sqrt(k / 2.0 / (q ** 3)) nu = M_to_nu(M, ecc) return coe2rv(k, p, ecc, inc, raan, argp, nu) @jit def kepler(k, r0, v0, tof, numiter): r"""Solves Kepler's Equation by applying a Newton-Raphson method. If the position of a body along its orbit wants to be computed for an specific time, it can be solved by terms of the Kepler's Equation: .. math:: E = M + e\sin{E} In this case, the equation is written in terms of the Universal Anomaly: .. math:: \sqrt{\mu}\Delta t = \frac{r_{o}v_{o}}{\sqrt{\mu}}\chi^{2}C(\alpha \chi^{2}) + (1 - \alpha r_{o})\chi^{3}S(\alpha \chi^{2}) + r_{0}\chi This equation is solved for the universal anomaly by applying a Newton-Raphson numerical method. Once it is solved, the Lagrange coefficients are returned: .. math:: \begin{align} f &= 1 \frac{\chi^{2}}{r_{o}}C(\alpha \chi^{2}) \\ g &= \Delta t - \frac{1}{\sqrt{\mu}}\chi^{3}S(\alpha \chi^{2}) \\ \dot{f} &= \frac{\sqrt{\mu}}{rr_{o}}(\alpha \chi^{3}S(\alpha \chi^{2}) - \chi) \\ \dot{g} &= 1 - \frac{\chi^{2}}{r}C(\alpha \chi^{2}) \\ \end{align} Lagrange coefficients can be related then with the position and velocity vectors: .. math:: \begin{align} \vec{r} &= f\vec{r_{o}} + g\vec{v_{o}} \\ \vec{v} &= \dot{f}\vec{r_{o}} + \dot{g}\vec{v_{o}} \\ \end{align} Parameters ---------- k: float Standard gravitational parameter r0: ~numpy.array Initial position vector v0: ~numpy.array Initial velocity vector numiter: int Number of iterations Returns ------- f: float First Lagrange coefficient g: float Second Lagrange coefficient fdot: float Derivative of the first coefficient gdot: float Derivative of the second coefficient Note ---- The theoretical procedure is explained in section 3.7 of Curtis in really deep detail. For analytical example, check in the same book for example 3.6. """ # Cache some results dot_r0v0 = np.dot(r0, v0) norm_r0 = np.dot(r0, r0) ** 0.5 sqrt_mu = k ** 0.5 alpha = -np.dot(v0, v0) / k + 2 / norm_r0 # First guess if alpha > 0: # Elliptic orbit xi_new = sqrt_mu * tof * alpha elif alpha < 0: # Hyperbolic orbit xi_new = ( np.sign(tof) * (-1 / alpha) ** 0.5 * np.log( (-2 * k * alpha * tof) / ( dot_r0v0 + np.sign(tof) * np.sqrt(-k / alpha) * (1 - norm_r0 * alpha) ) ) ) else: # Parabolic orbit # (Conservative initial guess) xi_new = sqrt_mu * tof / norm_r0 # Newton-Raphson iteration on the Kepler equation count = 0 while count < numiter: xi = xi_new psi = xi * xi * alpha c2_psi = c2(psi) c3_psi = c3(psi) norm_r = ( xi * xi * c2_psi + dot_r0v0 / sqrt_mu * xi * (1 - psi * c3_psi) + norm_r0 * (1 - psi * c2_psi) ) xi_new = ( xi + ( sqrt_mu * tof - xi * xi * xi * c3_psi - dot_r0v0 / sqrt_mu * xi * xi * c2_psi - norm_r0 * xi * (1 - psi * c3_psi) ) / norm_r ) if abs(xi_new - xi) < 1e-7: break else: count += 1 else: raise RuntimeError("Maximum number of iterations reached") # Compute Lagrange coefficients f = 1 - xi ** 2 / norm_r0 * c2_psi g = tof - xi ** 3 / sqrt_mu * c3_psi gdot = 1 - xi ** 2 / norm_r * c2_psi fdot = sqrt_mu / (norm_r * norm_r0) * xi * (psi * c3_psi - 1) return f, g, fdot, gdot @jit def mikkola(k, r0, v0, tof, rtol=None): """ Raw algorithm for Mikkola's Kepler solver. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol: float This method does not require of tolerance since it is non iterative. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Note ---- Original paper: https://doi.org/10.1007/BF01235850 """ # Solving for the classical elements p, ecc, inc, raan, argp, nu = rv2coe(k, r0, v0) M0 = nu_to_M(nu, ecc, delta=0) a = p / (1 - ecc ** 2) n = np.sqrt(k / np.abs(a) ** 3) M = M0 + n * tof # Solve for specific geometrical case if ecc < 1.0: # Equation (9a) alpha = (1 - ecc) / (4 * ecc + 1 / 2) else: alpha = (ecc - 1) / (4 * ecc + 1 / 2) beta = M / 2 / (4 * ecc + 1 / 2) # Equation (9b) if beta >= 0: z = (beta + np.sqrt(beta ** 2 + alpha ** 3)) ** (1 / 3) else: z = (beta - np.sqrt(beta ** 2 + alpha ** 3)) ** (1 / 3) s = z - alpha / z # Apply initial correction if ecc < 1.0: ds = -0.078 * s ** 5 / (1 + ecc) else: ds = 0.071 * s ** 5 / (1 + 0.45 * s ** 2) / (1 + 4 * s ** 2) / ecc s += ds # Solving for the true anomaly if ecc < 1.0: E = M + ecc * (3 * s - 4 * s ** 3) f = E - ecc * np.sin(E) - M f1 = 1.0 - ecc * np.cos(E) f2 = ecc * np.sin(E) f3 = ecc * np.cos(E) f4 = -f2 f5 = -f3 else: E = 3 * np.log(s + np.sqrt(1 + s ** 2)) f = -E + ecc * np.sinh(E) - M f1 = -1.0 + ecc * np.cosh(E) f2 = ecc * np.sinh(E) f3 = ecc * np.cosh(E) f4 = f2 f5 = f3 # Apply Taylor expansion u1 = -f / f1 u2 = -f / (f1 + 0.5 * f2 * u1) u3 = -f / (f1 + 0.5 * f2 * u2 + (1.0 / 6.0) * f3 * u2 ** 2) u4 = -f / ( f1 + 0.5 * f2 * u3 + (1.0 / 6.0) * f3 * u3 ** 2 + (1.0 / 24.0) * f4 * (u3 ** 3) ) u5 = -f / ( f1 + f2 * u4 / 2 + f3 * (u4 * u4) / 6.0 + f4 * (u4 * u4 * u4) / 24.0 + f5 * (u4 * u4 * u4 * u4) / 120.0 ) E += u5 if ecc < 1.0: nu = E_to_nu(E, ecc) else: if ecc == 1.0: # Parabolic nu = D_to_nu(E) else: # Hyperbolic nu = F_to_nu(E, ecc) return coe2rv(k, p, ecc, inc, raan, argp, nu) @jit def markley(k, r0, v0, tof): """ Solves the kepler problem by a non iterative method. Relative error is around 1e-18, only limited by machine double-precission errors. Parameters ---------- k : float Standar Gravitational parameter r0 : array Initial position vector wrt attractor center. v0 : array Initial velocity vector. tof : float Time of flight. Returns ------- rf: array Final position vector vf: array Final velocity vector Note ---- The following algorithm was taken from http://dx.doi.org/10.1007/BF00691917. """ # Solve first for eccentricity and mean anomaly p, ecc, inc, raan, argp, nu = rv2coe(k, r0, v0) M0 = nu_to_M(nu, ecc, delta=0) a = p / (1 - ecc ** 2) n = np.sqrt(k / a ** 3) M = M0 + n * tof # Range between -pi and pi M = M % (2 * np.pi) if M > np.pi: M = -(2 * np.pi - M) # Equation (20) alpha = (3 * np.pi ** 2 + 1.6 * (np.pi - np.abs(M)) / (1 + ecc)) / (np.pi ** 2 - 6) # Equation (5) d = 3 * (1 - ecc) + alpha * ecc # Equation (9) q = 2 * alpha * d * (1 - ecc) - M ** 2 # Equation (10) r = 3 * alpha * d * (d - 1 + ecc) * M + M ** 3 # Equation (14) w = (np.abs(r) + np.sqrt(q ** 3 + r ** 2)) ** (2 / 3) # Equation (15) E = (2 * r * w / (w ** 2 + w * q + q ** 2) + M) / d # Equation (26) f0 = _kepler_equation(E, M, ecc) f1 = _kepler_equation_prime(E, M, ecc) f2 = ecc * np.sin(E) f3 = ecc * np.cos(E) f4 = -f2 # Equation (22) delta3 = -f0 / (f1 - 0.5 * f0 * f2 / f1) delta4 = -f0 / (f1 + 0.5 * delta3 * f2 + 1 / 6 * delta3 ** 2 * f3) delta5 = -f0 / ( f1 + 0.5 * delta4 * f2 + 1 / 6 * delta4 ** 2 * f3 + 1 / 24 * delta4 ** 3 * f4 ) E += delta5 nu = E_to_nu(E, ecc) return coe2rv(k, p, ecc, inc, raan, argp, nu) @jit def pimienta(k, r0, v0, tof): """ Raw algorithm for Adonis' Pimienta and John L. Crassidis 15th order polynomial Kepler solver. Parameters ---------- k : float Standar Gravitational parameter r0 : array Initial position vector wrt attractor center. v0 : array Initial velocity vector. tof : float Time of flight. Returns ------- rf: array Final position vector vf: array Final velocity vector Note ---- This algorithm was drived from the original paper: http://hdl.handle.net/10477/50522 """ # TODO: implement hyperbolic case # Solve first for eccentricity and mean anomaly p, ecc, inc, raan, argp, nu = rv2coe(k, r0, v0) M0 = nu_to_M(nu, ecc, delta=0) semi_axis_a = p / (1 - ecc ** 2) n = np.sqrt(k / np.abs(semi_axis_a) ** 3) M = M0 + n * tof # Equation (32a), (32b), (32c) and (32d) c3 = 5 / 2 + 560 * ecc a = 15 * (1 - ecc) / c3 b = -M / c3 y = np.sqrt(b ** 2 / 4 + a ** 3 / 27) # Equation (33) x_bar = (-b / 2 + y) ** (1 / 3) - (b / 2 + y) ** (1 / 3) # Coefficients from equations (34a) and (34b) c15 = 3003 / 14336 + 16384 * ecc c13 = 3465 / 13312 - 61440 * ecc c11 = 945 / 2816 + 92160 * ecc c9 = 175 / 384 - 70400 * ecc c7 = 75 / 112 + 28800 * ecc c5 = 9 / 8 - 6048 * ecc # Precompute x_bar powers, equations (35a) to (35d) x_bar2 = x_bar ** 2 x_bar3 = x_bar2 * x_bar x_bar4 = x_bar3 * x_bar x_bar5 = x_bar4 * x_bar x_bar6 = x_bar5 * x_bar x_bar7 = x_bar6 * x_bar x_bar8 = x_bar7 * x_bar x_bar9 = x_bar8 * x_bar x_bar10 = x_bar9 * x_bar x_bar11 = x_bar10 * x_bar x_bar12 = x_bar11 * x_bar x_bar13 = x_bar12 * x_bar x_bar14 = x_bar13 * x_bar x_bar15 = x_bar14 * x_bar # Function f and its derivatives are given by all the (36) equation set f = ( c15 * x_bar15 + c13 * x_bar13 + c11 * x_bar11 + c9 * x_bar9 + c7 * x_bar7 + c5 * x_bar5 + c3 * x_bar3 + 15 * (1 - ecc) * x_bar - M ) f1 = ( 15 * c15 * x_bar14 + 13 * c13 * x_bar12 + 11 * c11 * x_bar10 + 9 * c9 * x_bar8 + 7 * c7 * x_bar6 + 5 * c5 * x_bar4 + 3 * c3 * x_bar2 + 15 * (1 - ecc) ) f2 = ( 210 * c15 * x_bar13 + 156 * c13 * x_bar11 + 110 * c11 * x_bar9 + 72 * c9 * x_bar7 + 42 * c7 * x_bar5 + 20 * c5 * x_bar3 + 6 * c3 * x_bar ) f3 = ( 2730 * c15 * x_bar12 + 1716 * c13 * x_bar10 + 990 * c11 * x_bar8 + 504 * c9 * x_bar6 + 210 * c7 * x_bar4 + 60 * c5 * x_bar2 + 6 * c3 ) f4 = ( 32760 * c15 * x_bar11 + 17160 * c13 * x_bar9 + 7920 * c11 * x_bar7 + 3024 * c9 * x_bar5 + 840 * c7 * x_bar3 + 120 * c5 * x_bar ) f5 = ( 360360 * c15 * x_bar10 + 154440 * c13 * x_bar8 + 55440 * c11 * x_bar6 + 15120 * c9 * x_bar4 + 2520 * c7 * x_bar2 + 120 * c5 ) f6 = ( 3603600 * c15 * x_bar9 + 1235520 * c13 * x_bar7 + 332640 * c11 * x_bar5 + 60480 * c9 * x_bar3 + 5040 * c7 * x_bar ) f7 = ( 32432400 * c15 * x_bar8 + 8648640 * c13 * x_bar6 + 1663200 * c11 * x_bar4 + 181440 * c9 * x_bar2 + 5040 * c7 ) f8 = ( 259459200 * c15 * x_bar7 + 51891840 * c13 * x_bar5 + 6652800 * c11 * x_bar3 + 362880 * c9 * x_bar ) f9 = ( 1.8162144e9 * c15 * x_bar6 + 259459200 * c13 * x_bar4 + 19958400 * c11 * x_bar2 + 362880 * c9 ) f10 = ( 1.08972864e10 * c15 * x_bar5 + 1.0378368e9 * c13 * x_bar3 + 39916800 * c11 * x_bar ) f11 = 5.4486432e10 * c15 * x_bar4 + 3.1135104e9 * c13 * x_bar2 + 39916800 * c11 f12 = 2.17945728e11 * c15 * x_bar3 + 6.2270208e9 * c13 * x_bar f13 = 6.53837184 * c15 * x_bar2 + 6.2270208e9 * c13 f14 = 1.307674368e13 * c15 * x_bar f15 = 1.307674368e13 * c15 # Solving g parameters defined by equations (37a), (37b), (37c) and (37d) g1 = 1 / 2 g2 = 1 / 6 g3 = 1 / 24 g4 = 1 / 120 g5 = 1 / 720 g6 = 1 / 5040 g7 = 1 / 40320 g8 = 1 / 362880 g9 = 1 / 3628800 g10 = 1 / 39916800 g11 = 1 / 479001600 g12 = 1 / 6.2270208e9 g13 = 1 / 8.71782912e10 g14 = 1 / 1.307674368e12 # Solving for the u_{i} and h_{i} variables defined by equation (38) u1 = -f / f1 h2 = f1 + g1 * u1 * f2 u2 = -f / h2 h3 = f1 + g1 * u2 * f2 + g2 * u2 ** 2 * f3 u3 = -f / h3 h4 = f1 + g1 * u3 * f2 + g2 * u3 ** 2 * f3 + g3 * u3 ** 3 * f4 u4 = -f / h4 h5 = f1 + g1 * u4 * f2 + g2 * u4 ** 2 * f3 + g3 * u4 ** 3 * f4 + g4 * u4 ** 4 * f5 u5 = -f / h5 h6 = ( f1 + g1 * u5 * f2 + g2 * u5 ** 2 * f3 + g3 * u5 ** 3 * f4 + g4 * u5 ** 4 * f5 + g5 * u5 ** 5 * f6 ) u6 = -f / h6 h7 = ( f1 + g1 * u6 * f2 + g2 * u6 ** 2 * f3 + g3 * u6 ** 3 * f4 + g4 * u6 ** 4 * f5 + g5 * u6 ** 5 * f6 + g6 * u6 ** 6 * f7 ) u7 = -f / h7 h8 = ( f1 + g1 * u7 * f2 + g2 * u7 ** 2 * f3 + g3 * u7 ** 3 * f4 + g4 * u7 ** 4 * f5 + g5 * u7 ** 5 * f6 + g6 * u7 ** 6 * f7 + g7 * u7 ** 7 * f8 ) u8 = -f / h8 h9 = ( f1 + g1 * u8 * f2 + g2 * u8 ** 2 * f3 + g3 * u8 ** 3 * f4 + g4 * u8 ** 4 * f5 + g5 * u8 ** 5 * f6 + g6 * u8 ** 6 * f7 + g7 * u8 ** 7 * f8 + g8 * u8 ** 8 * f9 ) u9 = -f / h9 h10 = ( f1 + g1 * u9 * f2 + g2 * u9 ** 2 * f3 + g3 * u9 ** 3 * f4 + g4 * u9 ** 4 * f5 + g5 * u9 ** 5 * f6 + g6 * u9 ** 6 * f7 + g7 * u9 ** 7 * f8 + g8 * u9 ** 8 * f9 + g9 * u9 ** 9 * f10 ) u10 = -f / h10 h11 = ( f1 + g1 * u10 * f2 + g2 * u10 ** 2 * f3 + g3 * u10 ** 3 * f4 + g4 * u10 ** 4 * f5 + g5 * u10 ** 5 * f6 + g6 * u10 ** 6 * f7 + g7 * u10 ** 7 * f8 + g8 * u10 ** 8 * f9 + g9 * u10 ** 9 * f10 + g10 * u10 ** 10 * f11 ) u11 = -f / h11 h12 = ( f1 + g1 * u11 * f2 + g2 * u11 ** 2 * f3 + g3 * u11 ** 3 * f4 + g4 * u11 ** 4 * f5 + g5 * u11 ** 5 * f6 + g6 * u11 ** 6 * f7 + g7 * u11 ** 7 * f8 + g8 * u11 ** 8 * f9 + g9 * u11 ** 9 * f10 + g10 * u11 ** 10 * f11 + g11 * u11 ** 11 * f12 ) u12 = -f / h12 h13 = ( f1 + g1 * u12 * f2 + g2 * u12 ** 2 * f3 + g3 * u12 ** 3 * f4 + g4 * u12 ** 4 * f5 + g5 * u12 ** 5 * f6 + g6 * u12 ** 6 * f7 + g7 * u12 ** 7 * f8 + g8 * u12 ** 8 * f9 + g9 * u12 ** 9 * f10 + g10 * u12 ** 10 * f11 + g11 * u12 ** 11 * f12 + g12 * u12 ** 12 * f13 ) u13 = -f / h13 h14 = ( f1 + g1 * u13 * f2 + g2 * u13 ** 2 * f3 + g3 * u13 ** 3 * f4 + g4 * u13 ** 4 * f5 + g5 * u13 ** 5 * f6 + g6 * u13 ** 6 * f7 + g7 * u13 ** 7 * f8 + g8 * u13 ** 8 * f9 + g9 * u13 ** 9 * f10 + g10 * u13 ** 10 * f11 + g11 * u13 ** 11 * f12 + g12 * u13 ** 12 * f13 + g13 * u13 ** 13 * f14 ) u14 = -f / h14 h15 = ( f1 + g1 * u14 * f2 + g2 * u14 ** 2 * f3 + g3 * u14 ** 3 * f4 + g4 * u14 ** 4 * f5 + g5 * u14 ** 5 * f6 + g6 * u14 ** 6 * f7 + g7 * u14 ** 7 * f8 + g8 * u14 ** 8 * f9 + g9 * u14 ** 9 * f10 + g10 * u14 ** 10 * f11 + g11 * u14 ** 11 * f12 + g12 * u14 ** 12 * f13 + g13 * u14 ** 13 * f14 + g14 * u14 ** 14 * f15 ) u15 = -f / h15 # Solving for x x = x_bar + u15 w = x - 0.01171875 * x ** 17 / (1 + ecc) # Solving for the true anomaly from eccentricity anomaly E = M + ecc * ( -16384 * w ** 15 + 61440 * w ** 13 - 92160 * w ** 11 + 70400 * w ** 9 - 28800 * w ** 7 + 6048 * w ** 5 - 560 * w ** 3 + 15 * w ) nu = E_to_nu(E, ecc) return coe2rv(k, p, ecc, inc, raan, argp, nu) @jit def gooding(k, r0, v0, tof, numiter=150, rtol=1e-8): """ Solves the Elliptic Kepler Equation with a cubic convergence and accuracy better than 10e-12 rad is normally achieved. It is not valid for eccentricities equal or higher than 1.0. Parameters ---------- k : float Standard gravitational parameter of the attractor. r : 1x3 vector Position vector. v : 1x3 vector Velocity vector. tof : float Time of flight. rtol: float Relative error for accuracy of the method. Returns ------- rr : 1x3 vector Propagated position vectors. vv : 1x3 vector Note ---- Original paper for the algorithm: https://doi.org/10.1007/BF01238923 """ # Solve first for eccentricity and mean anomaly p, ecc, inc, raan, argp, nu = rv2coe(k, r0, v0) # TODO: parabolic and hyperbolic not implemented cases if ecc >= 1.0: raise NotImplementedError( "Parabolic/Hyperbolic cases still not implemented in gooding." ) M0 = nu_to_M(nu, ecc, delta=0) semi_axis_a = p / (1 - ecc ** 2) n = np.sqrt(k / np.abs(semi_axis_a) ** 3) M = M0 + n * tof # Start the computation n = 0 c = ecc * np.cos(M) s = ecc * np.sin(M) psi = s / np.sqrt(1 - 2 * c + ecc ** 2) f = 1.0 while f ** 2 >= rtol and n <= numiter: xi = np.cos(psi) eta = np.sin(psi) fd = (1 - c * xi) + s * eta fdd = c * eta + s * xi f = psi - fdd psi = psi - f * fd / (fd ** 2 - 0.5 * f * fdd) n += 1 E = M + psi nu = E_to_nu(E, ecc) return coe2rv(k, p, ecc, inc, raan, argp, nu) @jit def danby(k, r0, v0, tof, numiter=20, rtol=1e-8): """ Kepler solver for both elliptic and parabolic orbits based on Danby's algorithm. Parameters ---------- k : float Standard gravitational parameter of the attractor. r : 1x3 vector Position vector. v : 1x3 vector Velocity vector. tof : float Time of flight. rtol: float Relative error for accuracy of the method. Returns ------- rr : 1x3 vector Propagated position vectors. vv : 1x3 vector Note ---- This algorithm was developed by Danby in his paper *The solution of Kepler Equation* with DOI: https://doi.org/10.1007/BF01686811 """ # Solve first for eccentricity and mean anomaly p, ecc, inc, raan, argp, nu = rv2coe(k, r0, v0) M0 = nu_to_M(nu, ecc, delta=0) semi_axis_a = p / (1 - ecc ** 2) n = np.sqrt(k / np.abs(semi_axis_a) ** 3) M = M0 + n * tof # Range mean anomaly xma = M - 2 * np.pi * np.floor(M / 2 / np.pi) if ecc == 0: # Solving for circular orbit nu = xma return coe2rv(k, p, ecc, inc, raan, argp, nu) elif ecc < 1.0: # For elliptical orbit E = xma + 0.85 * np.sign(np.sin(xma)) * ecc else: # For parabolic and hyperbolic E = np.log(2 * xma / ecc + 1.8) # Iterations begin n = 0 while n <= numiter: if ecc < 1.0: s = ecc * np.sin(E) c = ecc * np.cos(E) f = E - s - xma fp = 1 - c fpp = s fppp = c else: s = ecc * np.sinh(E) c = ecc * np.cosh(E) f = s - E - xma fp = c - 1 fpp = s fppp = c if np.abs(f) <= rtol: if ecc < 1.0: sta = np.sqrt(1 - ecc ** 2) * np.sin(E) cta = np.cos(E) - ecc else: sta = np.sqrt(ecc ** 2 - 1) * np.sinh(E) cta = ecc - np.cosh(E) nu = np.arctan2(sta, cta) return coe2rv(k, p, ecc, inc, raan, argp, nu) else: delta = -f / fp delta_star = -f / (fp + 0.5 * delta * fpp) deltak = -f / (fp + 0.5 * delta_star * fpp + delta_star ** 2 * fppp / 6) E = E + deltak n += 1 raise ValueError("Maximum number of iterations has been reached.") poliastro-0.13.1/src/poliastro/core/stumpff.py000644 001750 001750 00000002350 13525576227 022255 0ustar00juanlujuanlu000000 000000 from math import gamma import numpy as np from ._jit import jit @jit def c2(psi): r"""Second Stumpff function. For positive arguments: .. math:: c_2(\psi) = \frac{1 - \cos{\sqrt{\psi}}}{\psi} """ eps = 1.0 if psi > eps: res = (1 - np.cos(np.sqrt(psi))) / psi elif psi < -eps: res = (np.cosh(np.sqrt(-psi)) - 1) / (-psi) else: res = 1.0 / 2.0 delta = (-psi) / gamma(2 + 2 + 1) k = 1 while res + delta != res: res = res + delta k += 1 delta = (-psi) ** k / gamma(2 * k + 2 + 1) return res @jit def c3(psi): r"""Third Stumpff function. For positive arguments: .. math:: c_3(\psi) = \frac{\sqrt{\psi} - \sin{\sqrt{\psi}}}{\sqrt{\psi^3}} """ eps = 1.0 if psi > eps: res = (np.sqrt(psi) - np.sin(np.sqrt(psi))) / (psi * np.sqrt(psi)) elif psi < -eps: res = (np.sinh(np.sqrt(-psi)) - np.sqrt(-psi)) / (-psi * np.sqrt(-psi)) else: res = 1.0 / 6.0 delta = (-psi) / gamma(2 + 3 + 1) k = 1 while res + delta != res: res = res + delta k += 1 delta = (-psi) ** k / gamma(2 * k + 3 + 1) return res poliastro-0.13.1/src/poliastro/core/thrust/000755 001750 001750 00000000000 13577132207 021541 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/core/thrust/__init__.py000644 001750 001750 00000000000 13525576227 023647 0ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/core/thrust/change_a_inc.py000644 001750 001750 00000002600 13525576227 024476 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.util import circular_velocity from .._jit import jit @jit def extra_quantities(k, a_0, a_f, inc_0, inc_f, f): """Extra quantities given by the Edelbaum (a, i) model. """ V_0, beta_0_, _ = compute_parameters(k, a_0, a_f, inc_0, inc_f) delta_V_ = delta_V(V_0, beta_0_, inc_0, inc_f) t_f_ = delta_V_ / f return delta_V_, t_f_ @jit def beta(t, *, V_0, f, beta_0): """Compute yaw angle (β) as a function of time and the problem parameters. """ return np.arctan2(V_0 * np.sin(beta_0), V_0 * np.cos(beta_0) - f * t) @jit def beta_0(V_0, V_f, inc_0, inc_f): """Compute initial yaw angle (β) as a function of the problem parameters. """ delta_i_f = abs(inc_f - inc_0) return np.arctan2( np.sin(np.pi / 2 * delta_i_f), V_0 / V_f - np.cos(np.pi / 2 * delta_i_f) ) @jit def compute_parameters(k, a_0, a_f, inc_0, inc_f): """Compute parameters of the model. """ delta_inc = abs(inc_f - inc_0) V_0 = circular_velocity(k, a_0) V_f = circular_velocity(k, a_f) beta_0_ = beta_0(V_0, V_f, inc_0, inc_f) return V_0, beta_0_, delta_inc @jit def delta_V(V_0, beta_0, inc_0, inc_f): """Compute required increment of velocity. """ delta_i_f = abs(inc_f - inc_0) return V_0 * np.cos(beta_0) - V_0 * np.sin(beta_0) / np.tan( np.pi / 2 * delta_i_f + beta_0 ) poliastro-0.13.1/src/poliastro/core/thrust/change_argp.py000644 001750 001750 00000001124 13525576227 024356 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.util import circular_velocity from .._jit import jit @jit def delta_V(V, ecc, argp_0, argp_f, f, A): """Compute required increment of velocity. """ delta_argp = argp_f - argp_0 return delta_argp / ( 3 * np.sign(delta_argp) / 2 * np.sqrt(1 - ecc ** 2) / ecc / V + A / f ) @jit def extra_quantities(k, a, ecc, argp_0, argp_f, f, A=0.0): """Extra quantities given by the model. """ V = circular_velocity(k, a) delta_V_ = delta_V(V, ecc, argp_0, argp_f, f, A) t_f_ = delta_V_ / f return delta_V_, t_f_ poliastro-0.13.1/src/poliastro/core/thrust/change_ecc_quasioptimal.py000644 001750 001750 00000000750 13525576227 026753 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.util import circular_velocity from .._jit import jit @jit def delta_V(V_0, ecc_0, ecc_f): """Compute required increment of velocity. """ return 2 / 3 * V_0 * np.abs(np.arcsin(ecc_0) - np.arcsin(ecc_f)) @jit def extra_quantities(k, a, ecc_0, ecc_f, f): """Extra quantities given by the model. """ V_0 = circular_velocity(k, a) delta_V_ = delta_V(V_0, ecc_0, ecc_f) t_f_ = delta_V_ / f return delta_V_, t_f_ poliastro-0.13.1/src/poliastro/core/thrust/change_inc_ecc.py000644 001750 001750 00000002346 13525576227 025017 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.util import circular_velocity from .._jit import jit @jit def beta(ecc_0, ecc_f, inc_0, inc_f, argp): # Note: "The argument of perigee will vary during the orbit transfer # due to the natural drift and because e may approach zero. # However, [the equation] still gives a good estimate of the desired # thrust angle." return np.arctan( abs( 3 * np.pi * (inc_f - inc_0) / ( 4 * np.cos(argp) * ( ecc_0 - ecc_f + np.log((1 + ecc_f) * (-1 + ecc_0) / ((1 + ecc_0) * (-1 + ecc_f))) ) ) ) ) @jit def delta_V(V_0, ecc_0, ecc_f, beta_): """Compute required increment of velocity. """ return 2 * V_0 * np.abs(np.arcsin(ecc_0) - np.arcsin(ecc_f)) / (3 * np.cos(beta_)) @jit def extra_quantities(k, a, ecc_0, ecc_f, inc_0, inc_f, argp, f): """Extra quantities given by the model. """ beta_ = beta(ecc_0, ecc_f, inc_0, inc_f, argp) V_0 = circular_velocity(k, a) delta_V_ = delta_V(V_0, ecc_0, ecc_f, beta_) t_f_ = delta_V_ / f return delta_V_, beta_, t_f_ poliastro-0.13.1/src/poliastro/core/util.py000644 001750 001750 00000007606 13577127436 021560 0ustar00juanlujuanlu000000 000000 import numpy as np from numpy import cos, sin from ._jit import jit @jit def circular_velocity(k, a): r"""Compute circular velocity for a given body given thegravitational parameter and the semimajor axis. .. math:: v = \sqrt{\frac{\mu}{a}} Parameters ---------- k : float Gravitational Parameter a : float Semimajor Axis """ return np.sqrt(k / a) @jit def rotate(vec, angle, axis): r"""Rotates a vector around axis x, y or z a Counter ClockWise angle. .. math:: R(\theta) = \begin{bmatrix} \cos(\theta) & -\sin(\theta) \\ \sin(\theta) & \cos(\theta) \end{bmatrix} Parameters ---------- vec : ndarray Dimension 3 vector. angle : float Angle of rotation (rad). axis : int Axis to be rotated (X-axis: 0, Y-axis: 1, Z-axis: 2) Note ---- 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`. References ---------- http://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities """ assert vec.shape == (3,) rot = np.eye(3) if axis == 0: sl = slice(1, 3, 1) elif axis == 1: sl = slice(0, 3, 2) angle = -angle elif axis == 2: sl = slice(0, 2, 1) else: raise ValueError("Invalid axis: must be one of 'x', 'y' or 'z'") rot[sl, sl] = np.array(((cos(angle), -sin(angle)), (sin(angle), cos(angle)))) # np.dot() arguments must all have the same dtype return np.dot(rot, vec.astype(rot.dtype)) @jit def transform(vec, angle, axis): """Rotates a coordinate system around axis a positive right-handed angle. Note ----- This is a convenience function, equivalent to `rotate(vec, ax, -angle)`. Refer to the documentation of that function for further information. """ return rotate(vec, -angle, axis) @jit def norm(vec): r"""Returns the norm of a 3 dimension vector. .. math:: \left \| \vec{v} \right \| = \sqrt{\sum_{i=1}^{n}v_{i}^2} Parameters ---------- vec: ndarray Dimension 3 vector. Examples -------- >>> vec = np.array([1, 1, 1]) >>> norm(vec) 1.7320508075688772 """ vec = 1.0 * vec # Cast to float return np.sqrt(vec.dot(vec)) @jit def cross(a, b): r"""Computes cross product between two vectors. .. math:: \vec{w} = \vec{u} \times \vec{v} = \begin{vmatrix} u_{y} & y_{z} \\ v_{y} & v_{z} \end{vmatrix}\vec{i} - \begin{vmatrix} u_{x} & u_{z} \\ v_{x} & v_{z} \end{vmatrix}\vec{j} + \begin{vmatrix} u_{x} & u_{y} \\ v_{x} & v_{y} \end{vmatrix}\vec{k} Parameters ---------- a : ndarray 3 Dimension vector. b : ndarray 3 Dimension vector. Examples -------- >>> i = np.array([1., 0., 0.]) >>> j = np.array([0., 1., 0.]) >>> cross(i, j) array([0., 0., 1.]) Note ----- 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], ) ) @jit def rotation_matrix(angle, axis): c = cos(angle) s = sin(angle) if axis == 0: return np.array([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]]) elif axis == 1: return np.array([[c, 0.0, s], [0.0, 1.0, 0.0], [s, 0.0, c]]) elif axis == 2: return np.array([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]]) else: raise ValueError("Invalid axis: must be one of 'x', 'y' or 'z'") poliastro-0.13.1/src/poliastro/czml/000755 001750 001750 00000000000 13577132207 020225 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/czml/__init__.py000644 001750 001750 00000000000 13525576227 022333 0ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/czml/extract_czml.py000644 001750 001750 00000026014 13577127436 023311 0ustar00juanlujuanlu000000 000000 from typing import Any, List import numpy as np from astropy import units as u from astropy.coordinates import CartesianRepresentation from astropy.time import Time, TimeDelta from czml3.core import Packet, Preamble from czml3.enums import InterpolationAlgorithms, ReferenceFrames from czml3.properties import ( Billboard, Clock, Color, Label, Material, Path, Position, SolidColorMaterial, ) from czml3.types import IntervalValue, TimeInterval from poliastro.czml.utils import ellipsoidal_to_cartesian from poliastro.twobody.propagation import propagate PIC_SATELLITE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADJSURBVDhPnZHRDcMgEEMZjVEYpaNklIzSEfLfD4qNnXAJSFWfhO7w2Zc0Tf9QG2rXrEzSUeZLOGm47WoH95x3Hl3jEgilvDgsOQUTqsNl68ezEwn1vae6lceSEEYvvWNT/Rxc4CXQNGadho1NXoJ+9iaqc2xi2xbt23PJCDIB6TQjOC6Bho/sDy3fBQT8PrVhibU7yBFcEPaRxOoeTwbwByCOYf9VGp1BYI1BA+EeHhmfzKbBoJEQwn1yzUZtyspIQUha85MpkNIXB7GizqDEECsAAAAASUVORK5CYII=" PIC_GROUNDSTATION = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPrZDRDcMgDAU9GqN0lIzijw6SUbJJygUeNQgSqepJTyHG91LVVpwDdfxM3T9TSl1EXZvDwii471fivK73cBFFQNTT/d2KoGpfGOpSIkhUpgUMxq9DFEsWv4IXhlyCnhBFnZcFEEuYqbiUlNwWgMTdrZ3JbQFoEVG53rd8ztG9aPJMnBUQf/VFraBJeWnLS0RfjbKyLJA8FkT5seDYS1Qwyv8t0B/5C2ZmH2/eTGNNBgMmAAAAAElFTkSuQmCC" class CZMLExtractor: """A class for extracting orbitary data to Cesium""" def __init__( self, start_epoch, end_epoch, N, ellipsoid=None, pr_map=None, scene3D=True ): """ Orbital constructor Parameters ---------- start_epoch: ~astropy.time.core.Time Starting epoch end_epoch: ~astropy.time.core.Time Ending epoch N: int Default number of sample points. Unless otherwise specified, the number of sampled data points will be N when calling add_orbit() scene3D: bool Determines the scene mode. If set to true, the scene is set to 3D mode, otherwise it's the orthographic projection. """ self.packets = [] # type: List[Packet] self.cust_prop = [ellipsoid, pr_map, scene3D] self.orbits = [] # type: List[Any] self.N = N self.i = 0 self.gs_n = 0 self.start_epoch = Time(start_epoch, format="isot") self.end_epoch = Time(end_epoch, format="isot") self._init_czml_() self._change_custom_params(*self.cust_prop) def _init_orbit_packet_cords_(self, i, rtol): """ Parameters ---------- i: int Index of referenced orbit rtol: float Maximum relative error permitted Returns ------- coordinate list """ cart_cords = [] # type: List[float] h = (self.end_epoch - self.orbits[i][2]).to(u.second) / self.orbits[i][1] # Get rounding factor given the relative tolerance rf = 0 while rtol < 1: rtol *= 10 rf += 1 for k in range(self.orbits[i][1] + 2): position = propagate(self.orbits[i][0], TimeDelta(k * h), rtol=rtol) cords = position.represent_as(CartesianRepresentation).xyz.to(u.meter).value cords = np.insert(cords, 0, h.value * k, axis=0) # flatten list cart_cords += list(map(lambda x: round(x[0], rf), cords.tolist())) return cart_cords def _init_czml_(self): """ Only called at the initialization of the extractor Builds packets. """ pckt = Preamble( name="document_packet", clock=IntervalValue( start=self.start_epoch.value, end=self.end_epoch.value, value=Clock(currentTime=self.start_epoch.value, multiplier=60), ), ) self.packets.append(pckt) def _change_custom_params(self, ellipsoid, pr_map, scene3D): """ Change the custom properties package. Parameters ---------- ellipsoid: list(int) Defines the attractor ellipsoid. The list must have three numbers representing the radii in the x, y and z axis pr_map: str A URL to the projection of the defined ellipsoid (UV map) """ if ellipsoid is None: ellipsoid = [6378137.0, 6378137.0, 6356752.3142451793] if pr_map is None: pr_map = ( "https://upload.wikimedia.org/wikipedia/commons/c/c4/Earthmap1000x500compac.jpg", ) custom_props = { "custom_attractor": True, "ellipsoid": [{"array": ellipsoid}], "map_url": pr_map, "scene3D": scene3D, } pckt = Packet(id="custom_properties", properties=custom_props) self.packets.append(pckt) def add_ground_station( self, pos, id_description=None, label_fill_color=None, label_font=None, label_outline_color=None, label_text=None, label_show=True, ): """ Adds a ground station Parameters ---------- orbit: poliastro.Orbit Orbit to be added pos: list [~astropy.units] coordinates of ground station [u v] ellipsoidal coordinates (0 elevation) Id parameters: ------------- id_description: str Set ground station description Label parameters ---------- label_fill_color: list (int) Fill Color in rgba format label_outline_color: list (int) Outline Color in rgba format label_font: str Set label font style and size (CSS syntax) label_text: str Set label text label_show: bool Indicates whether the label is visible """ if ( len(pos) == 2 and isinstance(pos[0], u.quantity.Quantity) and isinstance(pos[0], u.quantity.Quantity) ): u0, v0 = pos if self.cust_prop[0]: a, b = self.cust_prop[0][:1] # get semi-major and semi-minor axises else: a, b = 6378137.0, 6378137.0 pos = list(map(lambda x: x.value, ellipsoidal_to_cartesian(a, b, u0, v0))) else: raise TypeError( "Invalid coordinates. Coordinates must be of the form [u, v] where u, v are astropy units" ) pckt = Packet( id="GS" + str(self.gs_n), description=id_description, availability=TimeInterval(start=self.start_epoch, end=self.end_epoch), position=Position(cartesian=pos), label=Label( show=label_show, text=label_text, font=label_font if label_font is not None else "11pt Lucida Console", fillColor=Color(rgba=label_fill_color) if label_fill_color is not None else None, outlineColor=Color(rgba=label_outline_color) if label_outline_color is not None else None, ), billboard=Billboard(image=PIC_GROUNDSTATION, show=True), ) self.packets.append(pckt) self.gs_n += 1 def add_orbit( self, orbit, rtol=1e-10, N=None, id_name=None, id_description=None, path_width=None, path_show=None, path_color=None, label_fill_color=None, label_outline_color=None, label_font=None, label_text=None, label_show=None, ): """ Adds an orbit Parameters ---------- orbit: poliastro.Orbit Orbit to be added rtol: float Maximum relative error permitted N: int Number of sample points Id parameters: ------------- id_name: str Set orbit name id_description: str Set orbit description Path parameters --------------- path_width: int Path width path_show: bool Indicates whether the path is visible path_color: list (int) Rgba path color Label parameters ---------- label_fill_color: list (int) Fill Color in rgba format label_outline_color: list (int) Outline Color in rgba format label_font: str Set label font style and size (CSS syntax) label_text: str Set label text label_show: bool Indicates whether the label is visible """ if N is None: N = self.N if orbit.epoch < Time(self.start_epoch): orbit = orbit.propagate(self.start_epoch - orbit.epoch) elif orbit.epoch > Time(self.end_epoch): raise ValueError( "The orbit's epoch cannot exceed the constructor's ending epoch" ) if rtol <= 0 or rtol >= 1: raise ValueError( "The relative tolerance must be a value in the range (0, 1)" ) self.orbits.append([orbit, N, orbit.epoch]) cartesian_cords = self._init_orbit_packet_cords_(self.i, rtol=rtol) start_epoch = Time(min(self.orbits[self.i][2], self.start_epoch), format="isot") pckt = Packet( id=self.i, name=id_name, description=id_description, availability=TimeInterval(start=self.start_epoch, end=self.end_epoch), position=Position( interpolationDegree=5, interpolationAlgorithm=InterpolationAlgorithms.LAGRANGE, referenceFrame=ReferenceFrames.INERTIAL, cartesian=cartesian_cords, epoch=start_epoch.value, ), path=Path( show=path_show, width=path_width, material=Material( solidColor=SolidColorMaterial(color=Color(rgba=path_color)) ) if path_color is not None else Material( solidColor=SolidColorMaterial(color=Color(rgba=[255, 255, 0, 255])) ), resolution=120, ), label=Label( text=label_text, font=label_font if label_font is not None else "11pt Lucida Console", show=label_show, fillColor=Color(rgba=label_fill_color) if label_fill_color is not None else Color(rgba=[255, 255, 0, 255]), outlineColor=Color(rgba=label_outline_color) if label_outline_color is not None else Color(rgba=[255, 255, 0, 255]), ), billboard=Billboard(image=PIC_SATELLITE, show=True), ) self.packets.append(pckt) self.i += 1 poliastro-0.13.1/src/poliastro/czml/utils.py000644 001750 001750 00000001205 13577127432 021741 0ustar00juanlujuanlu000000 000000 import numpy as np def ellipsoidal_to_cartesian(a, b, lat, lon): """ Converts ellipsoidal coordinates (assuming zero elevation) to the cartesian. Parameters ---------- a: float semi-major axis b: float semi-minor axis lat: float latitude lon: float longtitude Returns ------- 3d Cartesian coordinates in the form (x, y, z) """ # radius of curvature cr = a / np.sqrt(1 - (1 - (a / b) ** 2) * np.sin(lat) ** 2) x = cr * np.cos(lat) * np.cos(lon) y = cr * np.cos(lat) * np.sin(lon) z = (a / b) ** 2 * cr * np.sin(lat) return x, y, z poliastro-0.13.1/src/poliastro/ephem.py000644 001750 001750 00000003061 13525576227 020737 0ustar00juanlujuanlu000000 000000 import numpy as np from astropy import units as u from astropy.coordinates import ( GCRS, ICRS, CartesianRepresentation, get_body_barycentric, ) from astropy.time import Time from scipy.interpolate import interp1d def build_ephem_interpolant(body, period, t_span, rtol=1e-5): """Interpolates ephemerides data Parameters ---------- body : Body Source body. period : ~astropy.units.Quantity Orbital period. t_span : list(~astropy.units.Quantity) Initial and final epochs. rtol : float, optional Relative tolerance. Controls the number of sampled data points, defaults to 1e-5. Returns ------- intrp : ~scipy.interpolate.interpolate.interp1d Interpolated function. """ h = (period * rtol).to(u.day).value t_span = (t_span[0].to(u.day).value, t_span[1].to(u.day).value + 0.01) t_values = np.linspace(*t_span, int((t_span[1] - t_span[0]) / h)) r_values = np.zeros((t_values.shape[0], 3)) for i, t in enumerate(t_values): epoch = Time(t, format="jd", scale="tdb") r = get_body_barycentric(body.name, epoch) r = ( ICRS(x=r.x, y=r.y, z=r.z, representation_type=CartesianRepresentation) .transform_to(GCRS(obstime=epoch)) .represent_as(CartesianRepresentation) ) r_values[i] = r.xyz.to(u.km) t_values = ((t_values - t_span[0]) * u.day).to(u.s).value return interp1d(t_values, r_values, kind="cubic", axis=0, assume_sorted=True) poliastro-0.13.1/src/poliastro/examples.py000644 001750 001750 00000002164 13525576227 021462 0ustar00juanlujuanlu000000 000000 """Example data. """ from astropy import time, units as u from poliastro.bodies import Earth, Sun from poliastro.twobody import Orbit iss = Orbit.from_vectors( Earth, [8.59072560e2, -4.13720368e3, 5.29556871e3] * u.km, [7.37289205, 2.08223573, 4.39999794e-1] * u.km / u.s, time.Time("2013-03-18 12:00", scale="utc"), ) """ISS orbit example Taken from Plyades (c) 2012 Helge Eichhorn (MIT License) """ molniya = Orbit.from_classical( Earth, 26600 * u.km, 0.75 * u.one, 63.4 * u.deg, 0 * u.deg, 270 * u.deg, 80 * u.deg ) """Molniya orbit example""" _r_a = Earth.R + 35950 * u.km _r_p = Earth.R + 250 * u.km _a = (_r_a + _r_p) / 2 soyuz_gto = Orbit.from_classical( Earth, _a, _r_a / _a - 1, 6 * u.deg, 188.5 * u.deg, 178 * u.deg, 0 * u.deg ) """Soyuz geostationary transfer orbit (GTO) example Taken from Soyuz User's Manual, issue 2 revision 0 """ churi = Orbit.from_classical( Sun, 3.46250 * u.AU, 0.64 * u.one, 7.04 * u.deg, 50.1350 * u.deg, 12.8007 * u.deg, 63.89 * u.deg, time.Time("2015-11-05 12:00", scale="utc"), ) """Comet 67P/Churyumov–Gerasimenko orbit example""" poliastro-0.13.1/src/poliastro/frames.py000644 001750 001750 00000022011 13577127432 021107 0ustar00juanlujuanlu000000 000000 """Coordinate frames definitions. """ from enum import Enum from typing import Dict import numpy as np from astropy import _erfa as erfa, units as u from astropy.coordinates import ( GCRS, ICRS, AffineTransform, BaseEclipticFrame, BaseRADecFrame, CartesianDifferential, CartesianRepresentation, FunctionTransformWithFiniteDifference, GeocentricMeanEcliptic, HeliocentricEclipticIAU76 as HeliocentricEclipticJ2000, TimeAttribute, UnitSphericalRepresentation, frame_transform_graph, get_body, get_body_barycentric, get_body_barycentric_posvel, ) from astropy.coordinates.baseframe import FrameMeta from astropy.coordinates.builtin_frames.utils import DEFAULT_OBSTIME, get_jd12 from astropy.coordinates.matrix_utilities import ( matrix_product, matrix_transpose, rotation_matrix, ) from astropy.coordinates.transformations import DynamicMatrixTransform from poliastro.bodies import ( Earth, Jupiter, Mars, Mercury, Neptune, Pluto, Saturn, Sun, Uranus, Venus, _Body, ) from poliastro.constants import J2000 class Planes(Enum): EARTH_EQUATOR = "Earth mean Equator and Equinox of epoch (J2000.0)" EARTH_ECLIPTIC = "Earth mean Ecliptic and Equinox of epoch (J2000.0)" # BODY_EQUATOR = 'Body mean Equator and node of date' # TODO: Implement proper conversions # --- # Planetary frames parallel to ICRS # Taken from Astropy HCRS # --- _NEED_ORIGIN_HINT = ( "The input {0} coordinates do not have length units. This " "probably means you created coordinates with lat/lon but " "no distance. PlanetaryICRS<->ICRS transforms cannot " "function in this case because there is an origin shift." ) class _PlanetaryICRS(BaseRADecFrame): obstime = TimeAttribute(default=DEFAULT_OBSTIME) def __new__(cls, *args, **kwargs): frame_transform_graph.transform(AffineTransform, cls, ICRS)(cls.to_icrs) frame_transform_graph.transform(AffineTransform, ICRS, cls)(cls.from_icrs) frame_transform_graph.transform( FunctionTransformWithFiniteDifference, cls, cls )(cls.self_transform) return super().__new__(cls) @staticmethod def to_icrs(planet_coo, _): # this is just an origin translation so without a distance it cannot go ahead if isinstance(planet_coo.data, UnitSphericalRepresentation): raise u.UnitsError(_NEED_ORIGIN_HINT.format(planet_coo.__class__.__name__)) if planet_coo.data.differentials: bary_sun_pos, bary_sun_vel = get_body_barycentric_posvel( planet_coo.body.name, planet_coo.obstime ) bary_sun_pos = bary_sun_pos.with_differentials( bary_sun_vel.represent_as(CartesianDifferential) ) else: bary_sun_pos = get_body_barycentric( planet_coo.body.name, planet_coo.obstime ) bary_sun_vel = None return None, bary_sun_pos @staticmethod def from_icrs(icrs_coo, planet_frame): # this is just an origin translation so without a distance it cannot go ahead if isinstance(icrs_coo.data, UnitSphericalRepresentation): raise u.UnitsError(_NEED_ORIGIN_HINT.format(icrs_coo.__class__.__name__)) if icrs_coo.data.differentials: bary_sun_pos, bary_sun_vel = get_body_barycentric_posvel( planet_frame.body.name, planet_frame.obstime ) # Beware! Negation operation is not supported for differentials bary_sun_pos = (-bary_sun_pos).with_differentials( -bary_sun_vel.represent_as(CartesianDifferential) ) else: bary_sun_pos = -get_body_barycentric( planet_frame.body.name, planet_frame.obstime ) bary_sun_vel = None return None, bary_sun_pos @staticmethod def self_transform(from_coo, to_frame): if np.all(from_coo.obstime == to_frame.obstime): return to_frame.realize_frame(from_coo.data) else: # like CIRS, we do this self-transform via ICRS return from_coo.transform_to(ICRS).transform_to(to_frame) # Redefine HCRS, see https://github.com/astropy/astropy/issues/6835 class HCRS(_PlanetaryICRS): body = Sun class MercuryICRS(_PlanetaryICRS): body = Mercury class VenusICRS(_PlanetaryICRS): body = Venus class MarsICRS(_PlanetaryICRS): body = Mars class JupiterICRS(_PlanetaryICRS): body = Jupiter class SaturnICRS(_PlanetaryICRS): body = Saturn class UranusICRS(_PlanetaryICRS): body = Uranus class NeptuneICRS(_PlanetaryICRS): body = Neptune class PlutoICRS(_PlanetaryICRS): body = Pluto def _make_rotation_matrix_from_reprs(start_representation, end_representation): """ Return the matrix for the direct rotation from one representation to a second representation. The representations need not be normalized first. """ A = start_representation.to_cartesian() B = end_representation.to_cartesian() rotation_axis = A.cross(B) rotation_angle = -np.arccos( A.dot(B) / (A.norm() * B.norm()) ) # negation is required # This line works around some input/output quirks of Astropy's rotation_matrix() matrix = np.array(rotation_matrix(rotation_angle, rotation_axis.xyz.value.tolist())) return matrix def _obliquity_rotation_value(equinox): """ Function to calculate obliquity of the earth. This uses obl06 of erfa. """ jd1, jd2 = get_jd12(equinox, "tt") obl = erfa.obl06(jd1, jd2) * u.radian return obl.to(u.deg) class GeocentricSolarEcliptic(BaseEclipticFrame): """ This system has its X axis towards the Sun and its Z axis perpendicular to the plane of the Earth's orbit around the Sun (positive North). This system is fixed with respect to the Earth-Sun line. It is convenient for specifying magnetospheric boundaries. It has also been widely adopted as the system for representing vector quantities in space physics databases. """ obstime = TimeAttribute(default=DEFAULT_OBSTIME) @frame_transform_graph.transform(DynamicMatrixTransform, GCRS, GeocentricSolarEcliptic) def gcrs_to_geosolarecliptic(gcrs_coo, to_frame): if not to_frame.obstime.isscalar: raise ValueError( "To perform this transformation the obstime Attribute must be a scalar." ) _earth_orbit_perpen_point_gcrs = UnitSphericalRepresentation( lon=0 * u.deg, lat=(90 * u.deg - _obliquity_rotation_value(to_frame.obstime)) ) _earth_detilt_matrix = _make_rotation_matrix_from_reprs( _earth_orbit_perpen_point_gcrs, CartesianRepresentation(0, 0, 1) ) sun_pos_gcrs = get_body("sun", to_frame.obstime).cartesian earth_pos_gcrs = get_body("earth", to_frame.obstime).cartesian sun_earth = sun_pos_gcrs - earth_pos_gcrs sun_earth_detilt = sun_earth.transform(_earth_detilt_matrix) # Earth-Sun Line in Geocentric Solar Ecliptic Frame x_axis = CartesianRepresentation(1, 0, 0) rot_matrix = _make_rotation_matrix_from_reprs(sun_earth_detilt, x_axis) return matrix_product(rot_matrix, _earth_detilt_matrix) @frame_transform_graph.transform(DynamicMatrixTransform, GeocentricSolarEcliptic, GCRS) def geosolarecliptic_to_gcrs(from_coo, gcrs_frame): return matrix_transpose(gcrs_to_geosolarecliptic(gcrs_frame, from_coo)) _FRAME_MAPPING = { Sun: {Planes.EARTH_EQUATOR: HCRS, Planes.EARTH_ECLIPTIC: HeliocentricEclipticJ2000}, Mercury: {Planes.EARTH_EQUATOR: MercuryICRS}, Venus: {Planes.EARTH_EQUATOR: VenusICRS}, Earth: {Planes.EARTH_EQUATOR: GCRS, Planes.EARTH_ECLIPTIC: GeocentricMeanEcliptic}, Mars: {Planes.EARTH_EQUATOR: MarsICRS}, Jupiter: {Planes.EARTH_EQUATOR: JupiterICRS}, Saturn: {Planes.EARTH_EQUATOR: SaturnICRS}, Uranus: {Planes.EARTH_EQUATOR: UranusICRS}, Neptune: {Planes.EARTH_EQUATOR: NeptuneICRS}, Pluto: {Planes.EARTH_EQUATOR: PlutoICRS}, } # type: Dict[_Body, Dict[Planes, FrameMeta]] def get_frame(attractor, plane, obstime=J2000): """Returns an appropriate reference frame from an attractor and a plane. Available planes are Earth equator (parallel to GCRS) and Earth ecliptic. The fundamental direction of both is the equinox of epoch (J2000). An obstime is needed to properly locate the attractor. Parameters ---------- attractor : ~poliastro.bodies.Body Body that serves as the center of the frame. plane : ~poliastro.frames.Planes Fundamental plane of the frame. obstime : ~astropy.time.Time Time of the frame. """ try: frames = _FRAME_MAPPING[attractor] except KeyError: raise NotImplementedError( "Frames for orbits around custom bodies are not yet supported" ) try: frame_class = frames[plane] except KeyError: raise NotImplementedError( "A frame with plane {} around body {} is not yet implemented".format( plane, attractor ) ) return frame_class(obstime=obstime) poliastro-0.13.1/src/poliastro/integrators.py000644 001750 001750 00000035244 13577127436 022213 0ustar00juanlujuanlu000000 000000 from copy import copy import numpy as np from scipy.integrate import DenseOutput, OdeSolver from scipy.integrate._ivp.common import ( select_initial_step, validate_max_step, validate_tol, warn_extraneous, ) A = [ np.array([]), np.array([5.26001519587677318785587544488e-2]), np.array([1.97250569845378994544595329183e-2, 5.91751709536136983633785987549e-2]), np.array( [2.95875854768068491816892993775e-2, 0, 8.87627564304205475450678981324e-2] ), np.array( [ 2.41365134159266685502369798665e-1, 0, -8.84549479328286085344864962717e-1, 9.24834003261792003115737966543e-1, ] ), np.array( [ 3.7037037037037037037037037037e-2, 0, 0, 1.70828608729473871279604482173e-1, 1.25467687566822425016691814123e-1, ] ), np.array( [ 3.7109375e-2, 0, 0, 1.70252211019544039314978060272e-1, 6.02165389804559606850219397283e-2, -1.7578125e-2, ] ), np.array( [ 3.70920001185047927108779319836e-2, 0, 0, 1.70383925712239993810214054705e-1, 1.07262030446373284651809199168e-1, -1.53194377486244017527936158236e-2, 8.27378916381402288758473766002e-3, ] ), np.array( [ 6.24110958716075717114429577812e-1, 0, 0, -3.36089262944694129406857109825e-0, -8.68219346841726006818189891453e-1, 2.75920996994467083049415600797e1, 2.01540675504778934086186788979e1, -4.34898841810699588477366255144e1, ] ), np.array( [ 4.77662536438264365890433908527e-1, 0, 0, -2.48811461997166764192642586468e-0, -5.90290826836842996371446475743e-1, 2.12300514481811942347288949897e1, 1.52792336328824235832596922938e1, -3.32882109689848629194453265587e1, -2.03312017085086261358222928593e-2, ] ), np.array( [ -9.3714243008598732571704021658e-1, 0, 0, 5.18637242884406370830023853209e0, 1.09143734899672957818500254654e0, -8.14978701074692612513997267357e0, -1.85200656599969598641566180701e1, 2.27394870993505042818970056734e1, 2.49360555267965238987089396762e0, -3.0467644718982195003823669022e0, ] ), np.array( [ 2.27331014751653820792359768449e0, 0, 0, -1.05344954667372501984066689879e1, -2.00087205822486249909675718444e0, -1.79589318631187989172765950534e1, 2.79488845294199600508499808837e1, -2.85899827713502369474065508674e0, -8.87285693353062954433549289258e0, 1.23605671757943030647266201528e1, 6.43392746015763530355970484046e-1, ] ), np.array([]), np.array( [ 5.61675022830479523392909219681e-2, 0, 0, 0, 0, 0, 2.53500210216624811088794765333e-1, -2.46239037470802489917441475441e-1, -1.24191423263816360469010140626e-1, 1.5329179827876569731206322685e-1, 8.20105229563468988491666602057e-3, 7.56789766054569976138603589584e-3, -8.298e-3, ] ), np.array( [ 3.18346481635021405060768473261e-2, 0, 0, 0, 0, 2.83009096723667755288322961402e-2, 5.35419883074385676223797384372e-2, -5.49237485713909884646569340306e-2, 0, 0, -1.08347328697249322858509316994e-4, 3.82571090835658412954920192323e-4, -3.40465008687404560802977114492e-4, 1.41312443674632500278074618366e-1, ] ), np.array( [ -4.28896301583791923408573538692e-1, 0, 0, 0, 0, -4.69762141536116384314449447206e0, 7.68342119606259904184240953878e0, 4.06898981839711007970213554331e0, 3.56727187455281109270669543021e-1, 0, 0, 0, -1.39902416515901462129418009734e-3, 2.9475147891527723389556272149e0, -9.15095847217987001081870187138e0, ] ), ] C = np.array( [ 0.0, 0.526001519587677318785587544488e-01, 0.789002279381515978178381316732e-01, 0.118350341907227396726757197510e00, 0.281649658092772603273242802490e00, 0.333333333333333333333333333333e00, 0.25e00, 0.307692307692307692307692307692e00, 0.651282051282051282051282051282e00, 0.6e00, 0.857142857142857142857142857142e00, 1.0, 1.0, 0.1e00, 0.2e00, 0.777777777777777777777777777778e00, ] ) B = np.array( [ 5.42937341165687622380535766363e-2, 0, 0, 0, 0, 4.45031289275240888144113950566e0, 1.89151789931450038304281599044e0, -5.8012039600105847814672114227e0, 3.1116436695781989440891606237e-1, -1.52160949662516078556178806805e-1, 2.01365400804030348374776537501e-1, 4.47106157277725905176885569043e-2, ] ) BHH = np.array( [ 0.244094488188976377952755905512e00, 0.733846688281611857341361741547e00, 0.220588235294117647058823529412e-01, ] ) E = np.array( [ 0.1312004499419488073250102996e-01, 0, 0, 0, 0, -0.1225156446376204440720569753e01, -0.4957589496572501915214079952e00, 0.1664377182454986536961530415e01, -0.3503288487499736816886487290e00, 0.3341791187130174790297318841e00, 0.8192320648511571246570742613e-01, -0.2235530786388629525884427845e-01, ] ) D = [ np.array([]), np.array([]), np.array([]), np.array([]), np.array( [ -0.84289382761090128651353491142e01, 0, 0, 0, 0, 0.56671495351937776962531783590e00, -0.30689499459498916912797304727e01, 0.23846676565120698287728149680e01, 0.21170345824450282767155149946e01, -0.87139158377797299206789907490e00, 0.22404374302607882758541771650e01, 0.63157877876946881815570249290e00, -0.88990336451333310820698117400e-01, 0.18148505520854727256656404962e02, -0.91946323924783554000451984436e01, -0.44360363875948939664310572000e01, ] ), np.array( [ 0.10427508642579134603413151009e02, 0, 0, 0, 0, 0.24228349177525818288430175319e03, 0.16520045171727028198505394887e03, -0.37454675472269020279518312152e03, -0.22113666853125306036270938578e02, 0.77334326684722638389603898808e01, -0.30674084731089398182061213626e02, -0.93321305264302278729567221706e01, 0.15697238121770843886131091075e02, -0.31139403219565177677282850411e02, -0.93529243588444783865713862664e01, 0.35816841486394083752465898540e02, ] ), np.array( [ 0.19985053242002433820987653617e02, 0, 0, 0, 0, -0.38703730874935176555105901742e03, -0.18917813819516756882830838328e03, 0.52780815920542364900561016686e03, -0.11573902539959630126141871134e02, 0.68812326946963000169666922661e01, -0.10006050966910838403183860980e01, 0.77771377980534432092869265740e00, -0.27782057523535084065932004339e01, -0.60196695231264120758267380846e02, 0.84320405506677161018159903784e02, 0.11992291136182789328035130030e02, ] ), np.array( [ -0.25693933462703749003312586129e02, 0, 0, 0, 0, -0.15418974869023643374053993627e03, -0.23152937917604549567536039109e03, 0.35763911791061412378285349910e03, 0.93405324183624310003907691704e02, -0.37458323136451633156875139351e02, 0.10409964950896230045147246184e03, 0.29840293426660503123344363579e02, -0.435334565900111437544321750583e02, 0.96324553959188282948394950600e02, -0.39177261675615439165231486172e02, -0.14972683625798562581422125276e03, ] ), ] def validate_max_nsteps(max_nsteps): if max_nsteps <= 0: raise ValueError("`max_nsteps` must be positive.") return max_nsteps def validate_safety_factor(safety_factor): if safety_factor >= 1.0 or safety_factor <= 1e-4: raise ValueError("`safety_factor` must lie within 1e-4 and 1.0.") return safety_factor def validate_beta_stabilizer(beta_stabilizer): if beta_stabilizer < 0 or beta_stabilizer > 0.2: raise ValueError("`beta_stabilizer` must lie within 0 and 0.2.") return beta_stabilizer class DOP835(OdeSolver): A = A C = C B = B E = E BHH = BHH D = D t: float y: np.array def __init__( self, fun, t0, y0, t_bound, max_step=np.inf, rtol=1e-7, atol=1e-12, safety_factor=0.9, min_step_change=0.333, max_step_change=6.0, beta_stabilizer=0.00, max_nsteps=100000, vectorized=False, **extraneous ): warn_extraneous(extraneous) super().__init__(fun, t0, y0, t_bound, vectorized, support_complex=True) self.y_old = None self.max_step = validate_max_step(max_step) self.beta_stabilizer = validate_beta_stabilizer(beta_stabilizer) self.max_nsteps = validate_max_nsteps(max_nsteps) self.safety_factor = validate_safety_factor(safety_factor) self.rtol, self.atol = validate_tol(rtol, atol, self.n) self.min_step_change = min_step_change self.max_step_change = max_step_change self.order = 8 self.f = self.fun(self.t, self.y) self.h_abs = select_initial_step( self.fun, self.t, self.y, self.f, self.direction, self.order, self.rtol, self.atol, ) self.nfev += 2 self.n_steps = 0 self.n_accepted = 0 self.n_rejected = 0 self.factor_old = 1e-4 # Lund-stabilization factor self.K = np.zeros((16, self.n)) self.interpolation = np.zeros((8, self.n)) def _step_impl(self): t = self.t y = self.y f = self.f K = self.K rtol = self.rtol atol = self.atol min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t) while True: if self.h_abs < min_step: return False, self.TOO_SMALL_STEP h = self.h_abs * self.direction t_new = t + h if self.direction * (t_new - self.t_bound) > 0: t_new = self.t_bound h = t_new - t h_abs = np.abs(h) K[0] = f for s in range(1, 12): a, c = self.A[s], self.C[s] dy = np.dot(K[:s].T, a) * h K[s] = self.fun(t + c * h, y + dy) self.nfev += 11 f_B = np.dot(K[:12].T, self.B) y_final = y + h * f_B scale = atol + np.maximum(np.abs(y), np.abs(y_final)) * rtol err_BHH = ( f_B - self.BHH[0] * K[0] - self.BHH[1] * K[8] - self.BHH[2] * K[11] ) err_BHH = np.sum((err_BHH / scale) ** 2) err_E = np.dot(K[:12].T, self.E) err_E = np.sum((err_E / scale) ** 2) denominator = err_E + 1e-2 * err_BHH err_E = h_abs * err_E / np.sqrt(self.n * denominator) err_exp = err_E ** (0.125 - self.beta_stabilizer * 0.2) dh_factor = err_exp / (self.factor_old ** self.beta_stabilizer) dh_factor = np.max( [ 1.0 / self.max_step_change, np.min( [1.0 / self.min_step_change, dh_factor / self.safety_factor] ), ] ) h_new_abs = h_abs / dh_factor if err_E < 1.0: self.factor_old = np.max([err_E, 1e-4]) self.n_accepted += 1 K[12] = self.fun(t + h, y_final) for s in range(13, 16): a, c = self.A[s], self.C[s] dy = np.dot(K[:s].T, a) * h K[s] = self.fun(t + c * h, y + dy) self.nfev += 4 # prepare the dense output self.interpolation[0] = y self.interpolation[1] = y_final - y self.interpolation[2] = h * K[0] - self.interpolation[1] self.interpolation[3] = ( self.interpolation[1] - h * K[12] - self.interpolation[2] ) for n in range(4, 8): self.interpolation[n] = h * np.dot(K[:16].T, self.D[n]) self.y_old = y self.t = t_new self.y = y_final self.f = K[12] self.h_abs = h_new_abs return True, None else: self.n_rejected += 1 self.h_abs /= np.min( [1.0 / self.min_step_change, err_exp / self.safety_factor] ) def _dense_output_impl(self): return DOP835DenseOutput(self.t_old, self.t, self.y_old, self.interpolation) def get_coeffs(s): coeffs = np.zeros((8)) s_back = 1.0 - s coeffs[0] = 1.0 for i in range(7): if i % 2 == 0: coeffs[i + 1] = coeffs[i] * s else: coeffs[i + 1] = coeffs[i] * s_back return np.array(coeffs) class DOP835DenseOutput(DenseOutput): def __init__(self, t_old, t_new, y_old, interpolation): super().__init__(t_old, t_new) self.h = t_new - t_old self.interpolation = copy(interpolation) self.y_old = y_old def _call_impl(self, t_eval): s = (t_eval - self.t_old) / self.h coeffs = get_coeffs(s) return np.dot(self.interpolation.T, coeffs) poliastro-0.13.1/src/poliastro/iod/000755 001750 001750 00000000000 13577132207 020033 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/iod/__init__.py000644 001750 001750 00000000114 13525576227 022147 0ustar00juanlujuanlu000000 000000 # Select default algorithm from .izzo import lambert __all__ = ["lambert"] poliastro-0.13.1/src/poliastro/iod/izzo.py000644 001750 001750 00000002222 13525576227 021405 0ustar00juanlujuanlu000000 000000 """Izzo's algorithm for Lambert's problem """ from astropy import units as u from poliastro.core.iod import izzo as izzo_fast def lambert(k, r0, r, tof, M=0, numiter=35, rtol=1e-8): """Solves the Lambert problem using the Izzo algorithm. .. versionadded:: 0.5.0 Parameters ---------- k : ~astropy.units.Quantity Gravitational constant of main attractor (km^3 / s^2). r0 : ~astropy.units.Quantity Initial position (km). r : ~astropy.units.Quantity Final position (km). tof : ~astropy.units.Quantity Time of flight (s). M : int, optional Number of full revolutions, default to 0. numiter : int, optional Maximum number of iterations, default to 35. rtol : float, optional Relative tolerance of the algorithm, default to 1e-8. Yields ------ v0, v : tuple Pair of velocity solutions. """ k_ = k.to(u.km ** 3 / u.s ** 2).value r0_ = r0.to(u.km).value r_ = r.to(u.km).value tof_ = tof.to(u.s).value sols = izzo_fast(k_, r0_, r_, tof_, M, numiter, rtol) for v0, v in sols: yield v0 * u.km / u.s, v * u.km / u.s poliastro-0.13.1/src/poliastro/iod/vallado.py000644 001750 001750 00000002534 13525576227 022042 0ustar00juanlujuanlu000000 000000 """Initial orbit determination. """ from astropy import units as u from poliastro.core.iod import vallado as vallado_fast def lambert(k, r0, r, tof, short=True, numiter=35, rtol=1e-8): """Solves the Lambert problem. .. versionadded:: 0.3.0 Parameters ---------- k : ~astropy.units.Quantity Gravitational constant of main attractor (km^3 / s^2). r0 : ~astropy.units.Quantity Initial position (km). r : ~astropy.units.Quantity Final position (km). tof : ~astropy.units.Quantity Time of flight (s). short : boolean, optional Find out the short path, default to True. If False, find long path. numiter : int, optional Maximum number of iterations, default to 35. rtol : float, optional Relative tolerance of the algorithm, default to 1e-8. Raises ------ RuntimeError If it was not possible to compute the orbit. Note ---- This uses the universal variable approach found in Battin, Mueller & White with the bisection iteration suggested by Vallado. Multiple revolutions not supported. """ k_ = k.to(u.km ** 3 / u.s ** 2).value r0_ = r0.to(u.km).value r_ = r.to(u.km).value tof_ = tof.to(u.s).value v0, v = vallado_fast(k_, r0_, r_, tof_, short, numiter, rtol) yield v0 * u.km / u.s, v * u.km / u.s poliastro-0.13.1/src/poliastro/maneuver.py000644 001750 001750 00000022122 13577127432 021457 0ustar00juanlujuanlu000000 000000 """Orbital maneuvers. """ import numpy as np from astropy import units as u from poliastro.core.elements import coe_rotation_matrix, rv_pqw from poliastro.core.util import cross from poliastro.iod.izzo import lambert as lambert_izzo from poliastro.util import norm class Maneuver(object): r"""Class to represent a Maneuver. Each ``Maneuver`` consists on a list of impulses :math:`\Delta v_i` (changes in velocity) each one applied at a certain instant :math:`t_i`. You can access them directly indexing the ``Maneuver`` object itself. >>> man = Maneuver((0 * u.s, [1, 0, 0] * u.km / u.s), ... (10 * u.s, [1, 0, 0] * u.km / u.s)) >>> man[0] (, ) >>> man.impulses[1] (, ) """ def __init__(self, *args): r"""Constructor. Parameters ---------- impulses : list List of pairs (delta_time, delta_velocity) """ self.impulses = args # HACK: Change API or validation code _dts, _dvs = zip(*args) self._dts, self._dvs = self._initialize( [(_dt * u.one).value for _dt in _dts] * (_dts[0] * u.one).unit, [(_dv * u.one).value for _dv in _dvs] * (_dvs[0] * u.one).unit, ) try: if not all(len(dv) == 3 for dv in self._dvs): raise TypeError except TypeError: raise ValueError("Delta-V must be three dimensions vectors") @u.quantity_input(dts=u.s, dvs=u.m / u.s) def _initialize(self, dts, dvs): return dts, dvs def __getitem__(self, key): return self.impulses[key] @classmethod def impulse(cls, dv): """Single impulse at current time. Parameters ---------- dv: np.array Velocity components of the impulse. """ return cls((0 * u.s, dv)) @classmethod def hohmann(cls, orbit_i, r_f): r"""Compute a Hohmann transfer between two circular orbits. By defining the relationship between orbit radius: .. math:: a_{trans} = \frac{r_{i} + r_{f}}{2} The Hohmann maneuver velocities can be expressed as: .. math:: \begin{align} \Delta v_{a} &= \sqrt{\frac{2\mu}{r_{i}} - \frac{\mu}{a_{trans}}} - v_{i}\\ \Delta v_{b} &= \sqrt{\frac{\mu}{r_{f}}} - \sqrt{\frac{2\mu}{r_{f}} - \frac{\mu}{a_{trans}}} \end{align} The time that takes to complete the maneuver can be computed as: .. math:: \tau_{trans} = \pi \sqrt{\frac{(a_{trans})^{3}}{\mu}} Parameters ---------- orbit_i: poliastro.twobody.orbit.Orbit Initial orbit r_f: astropy.unit.Quantity Final altitude of the orbit """ if orbit_i.nu is not 0 * u.deg: orbit_i = orbit_i.propagate_to_anomaly(0 * u.deg) # Initial orbit data k = orbit_i.attractor.k r_i = orbit_i.r v_i = orbit_i.v h_i = norm(cross(r_i.to(u.m).value, v_i.to(u.m / u.s).value) * u.m ** 2 / u.s) p_i = h_i ** 2 / k.to(u.m ** 3 / u.s ** 2) # Hohmann is defined always from the PQW frame, since it is the # natural plane of the orbit r_i, v_i = rv_pqw( k.to(u.m ** 3 / u.s ** 2).value, p_i.to(u.m).value, orbit_i.ecc.value, orbit_i.nu.to(u.rad).value, ) # Now, we apply Hohmman maneuver r_i = norm(r_i * u.m) v_i = norm(v_i * u.m / u.s) a_trans = (r_i + r_f) / 2 # This is the modulus of the velocities dv_a = np.sqrt(2 * k / r_i - k / a_trans) - v_i dv_b = np.sqrt(k / r_f) - np.sqrt(2 * k / r_f - k / a_trans) # Write them in PQW frame dv_a = np.array([0, dv_a.to(u.m / u.s).value, 0]) dv_b = np.array([0, -dv_b.to(u.m / u.s).value, 0]) # Transform to IJK frame rot_matrix = coe_rotation_matrix( orbit_i.inc.to(u.rad).value, orbit_i.raan.to(u.rad).value, orbit_i.argp.to(u.rad).value, ) dv_a = (rot_matrix @ dv_a) * u.m / u.s dv_b = (rot_matrix @ dv_b) * u.m / u.s t_trans = np.pi * np.sqrt(a_trans ** 3 / k) return cls((0 * u.s, dv_a), (t_trans, dv_b)) @classmethod def bielliptic(cls, orbit_i, r_b, r_f): r"""Compute a bielliptic transfer between two circular orbits. The bielliptic maneuver employs two Hohmann transfers, therefore two intermediate orbits are established. We define the different radius relationships as follows: .. math:: \begin{align} a_{trans1} &= \frac{r_{i} + r_{b}}{2}\\ a_{trans2} &= \frac{r_{b} + r_{f}}{2}\\ \end{align} The increments in the velocity are: .. math:: \begin{align} \Delta v_{a} &= \sqrt{\frac{2\mu}{r_{i}} - \frac{\mu}{a_{trans1}}} - v_{i}\\ \Delta v_{b} &= \sqrt{\frac{2\mu}{r_{b}} - \frac{\mu}{a_{trans2}}} - \sqrt{\frac{2\mu}{r_{b}} - \frac{\mu}{a_trans{1}}}\\ \Delta v_{c} &= \sqrt{\frac{\mu}{r_{f}}} - \sqrt{\frac{2\mu}{r_{f}} - \frac{\mu}{a_{trans2}}}\\ \end{align} The time of flight for this maneuver is the addition of the time needed for both transition orbits, following the same formula as Hohmann: .. math:: \begin{align} \tau_{trans1} &= \pi \sqrt{\frac{a_{trans1}^{3}}{\mu}}\\ \tau_{trans2} &= \pi \sqrt{\frac{a_{trans2}^{3}}{\mu}}\\ \end{align} Parameters ---------- orbit_i: poliastro.twobody.orbit.Orbit Initial orbit r_b: astropy.unit.Quantity Altitude of the intermediate orbit r_f: astropy.unit.Quantity Final altitude of the orbit """ if orbit_i.nu is not 0 * u.deg: orbit_i = orbit_i.propagate_to_anomaly(0 * u.deg) # Initial orbit data k = orbit_i.attractor.k r_i = orbit_i.r v_i = orbit_i.v h_i = norm(cross(r_i.to(u.m).value, v_i.to(u.m / u.s).value) * u.m ** 2 / u.s) p_i = h_i ** 2 / k.to(u.m ** 3 / u.s ** 2) # Bielliptic is defined always from the PQW frame, since it is the # natural plane of the orbit r_i, v_i = rv_pqw( k.to(u.m ** 3 / u.s ** 2).value, p_i.to(u.m).value, orbit_i.ecc.value, orbit_i.nu.to(u.rad).value, ) # Define the transfer radius r_i = norm(r_i * u.m) v_i = norm(v_i * u.m / u.s) a_trans1 = (r_i + r_b) / 2 a_trans2 = (r_b + r_f) / 2 # Compute impulses dv_a = np.sqrt(2 * k / r_i - k / a_trans1) - v_i dv_b = np.sqrt(2 * k / r_b - k / a_trans2) - np.sqrt(2 * k / r_b - k / a_trans1) dv_c = np.sqrt(k / r_f) - np.sqrt(2 * k / r_f - k / a_trans2) # Write impulses in PQW frame dv_a = np.array([0, dv_a.to(u.m / u.s).value, 0]) dv_b = np.array([0, -dv_b.to(u.m / u.s).value, 0]) dv_c = np.array([0, dv_c.to(u.m / u.s).value, 0]) rot_matrix = coe_rotation_matrix( orbit_i.inc.to(u.rad).value, orbit_i.raan.to(u.rad).value, orbit_i.argp.to(u.rad).value, ) # Transform to IJK frame dv_a = (rot_matrix @ dv_a) * u.m / u.s dv_b = (rot_matrix @ dv_b) * u.m / u.s dv_c = (rot_matrix @ dv_c) * u.m / u.s # Compute time for maneuver t_trans1 = np.pi * np.sqrt(a_trans1 ** 3 / k) t_trans2 = np.pi * np.sqrt(a_trans2 ** 3 / k) return cls((0 * u.s, dv_a), (t_trans1, dv_b), (t_trans2, dv_c)) @classmethod def lambert(cls, orbit_i, orbit_f, method=lambert_izzo, short=True, **kwargs): """ Computes lambert maneuver between two different points. Parameters ---------- orbit_i: ~poliastro.twobody.Orbit Initial orbit orbit_f: ~poliastro.twobody.Orbit Final orbit method: function Method for solving Lambert's problem short: keyword, boolean Selects between short and long solution """ # Get initial algorithm conditions k = orbit_i.attractor.k r_i = orbit_i.r r_f = orbit_f.r # Time of flight is solved by substracting both orbit epochs tof = orbit_f.epoch - orbit_i.epoch # Compute all posible solutions to the Lambert transfer sols = list(method(k, r_i, r_f, tof, **kwargs)) # Return short or long solution if short: dv_a, dv_b = sols[0] else: dv_a, dv_b = sols[-1] return cls((0 * u.s, dv_a - orbit_i.v), (tof.to(u.s), orbit_f.v - dv_b)) def get_total_time(self): """Returns total time of the maneuver. """ total_time = sum(self._dts, 0 * u.s) return total_time def get_total_cost(self): """Returns total cost of the maneuver. """ dvs = [norm(dv) for dv in self._dvs] return sum(dvs, 0 * u.km / u.s) poliastro-0.13.1/src/poliastro/neos/000755 001750 001750 00000000000 13577132207 020224 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/neos/__init__.py000644 001750 001750 00000000425 13525576227 022345 0ustar00juanlujuanlu000000 000000 """Code related to NEOs. Functions related to NEOs and different NASA APIs. All of them are coded as part of SOCIS 2017 proposal. Notes ----- The orbits returned by the functions in this package are in the :py:class:`~poliastro.frames.HeliocentricEclipticJ2000` frame. """ poliastro-0.13.1/src/poliastro/neos/dastcom5.py000644 001750 001750 00000040211 13525576227 022322 0ustar00juanlujuanlu000000 000000 """NEOs orbit from DASTCOM5 database. """ import os import re import urllib.request import zipfile import astropy.units as u import numpy as np import pandas as pd from astropy.time import Time from poliastro.bodies import Sun from poliastro.frames import HeliocentricEclipticJ2000 from poliastro.twobody.angles import M_to_nu from poliastro.twobody.orbit import Orbit AST_DTYPE = np.dtype( [ ("NO", np.int32), ("NOBS", np.int32), ("OBSFRST", np.int32), ("OBSLAST", np.int32), ("EPOCH", np.float64), ("CALEPO", np.float64), ("MA", np.float64), ("W", np.float64), ("OM", np.float64), ("IN", np.float64), ("EC", np.float64), ("A", np.float64), ("QR", np.float64), ("TP", np.float64), ("TPCAL", np.float64), ("TPFRAC", np.float64), ("SOLDAT", np.float64), ("SRC1", np.float64), ("SRC2", np.float64), ("SRC3", np.float64), ("SRC4", np.float64), ("SRC5", np.float64), ("SRC6", np.float64), ("SRC7", np.float64), ("SRC8", np.float64), ("SRC9", np.float64), ("SRC10", np.float64), ("SRC11", np.float64), ("SRC12", np.float64), ("SRC13", np.float64), ("SRC14", np.float64), ("SRC15", np.float64), ("SRC16", np.float64), ("SRC17", np.float64), ("SRC18", np.float64), ("SRC19", np.float64), ("SRC20", np.float64), ("SRC21", np.float64), ("SRC22", np.float64), ("SRC23", np.float64), ("SRC24", np.float64), ("SRC25", np.float64), ("SRC26", np.float64), ("SRC27", np.float64), ("SRC28", np.float64), ("SRC29", np.float64), ("SRC30", np.float64), ("SRC31", np.float64), ("SRC32", np.float64), ("SRC33", np.float64), ("SRC34", np.float64), ("SRC35", np.float64), ("SRC36", np.float64), ("SRC37", np.float64), ("SRC38", np.float64), ("SRC39", np.float64), ("SRC40", np.float64), ("SRC41", np.float64), ("SRC42", np.float64), ("SRC43", np.float64), ("SRC44", np.float64), ("SRC45", np.float64), ("PRELTV", np.int8), ("SPHMX3", np.int8), ("SPHMX5", np.int8), ("JGSEP", np.int8), ("TWOBOD", np.int8), ("NSATS", np.int8), ("UPARM", np.int8), ("LSRC", np.int8), ("NDEL", np.int16), ("NDOP", np.int16), ("H", np.float32), ("G", np.float32), ("A1", np.float32), ("A2", np.float32), ("A3", np.float32), ("R0", np.float32), ("ALN", np.float32), ("NM", np.float32), ("NN", np.float32), ("NK", np.float32), ("LGK", np.float32), ("RHO", np.float32), ("AMRAT", np.float32), ("ALF", np.float32), ("DEL", np.float32), ("SPHLM3", np.float32), ("SPHLM5", np.float32), ("RP", np.float32), ("GM", np.float32), ("RAD", np.float32), ("EXTNT1", np.float32), ("EXTNT2", np.float32), ("EXTNT3", np.float32), ("MOID", np.float32), ("ALBEDO", np.float32), ("BVCI", np.float32), ("UBCI", np.float32), ("IRCI", np.float32), ("RMSW", np.float32), ("RMSU", np.float32), ("RMSN", np.float32), ("RMSNT", np.float32), ("RMSH", np.float32), ("EQUNOX", "|S4"), ("PENAM", "|S6"), ("SBNAM", "|S12"), ("SPTYPT", "|S5"), ("SPTYPS", "|S5"), ("DARC", "|S9"), ("COMNT1", "|S41"), ("COMNT2", "|S80"), ("DESIG", "|S13"), ("ASTEST", "|S8"), ("IREF", "|S10"), ("ASTNAM", "|S18"), ] ) COM_DTYPE = np.dtype( [ ("NO", np.int32), ("NOBS", np.int32), ("OBSFRST", np.int32), ("OBSLAST", np.int32), ("EPOCH", np.float64), ("CALEPO", np.float64), ("MA", np.float64), ("W", np.float64), ("OM", np.float64), ("IN", np.float64), ("EC", np.float64), ("A", np.float64), ("QR", np.float64), ("TP", np.float64), ("TPCAL", np.float64), ("TPFRAC", np.float64), ("SOLDAT", np.float64), ("SRC1", np.float64), ("SRC2", np.float64), ("SRC3", np.float64), ("SRC4", np.float64), ("SRC5", np.float64), ("SRC6", np.float64), ("SRC7", np.float64), ("SRC8", np.float64), ("SRC9", np.float64), ("SRC10", np.float64), ("SRC11", np.float64), ("SRC12", np.float64), ("SRC13", np.float64), ("SRC14", np.float64), ("SRC15", np.float64), ("SRC16", np.float64), ("SRC17", np.float64), ("SRC18", np.float64), ("SRC19", np.float64), ("SRC20", np.float64), ("SRC21", np.float64), ("SRC22", np.float64), ("SRC23", np.float64), ("SRC24", np.float64), ("SRC25", np.float64), ("SRC26", np.float64), ("SRC27", np.float64), ("SRC28", np.float64), ("SRC29", np.float64), ("SRC30", np.float64), ("SRC31", np.float64), ("SRC32", np.float64), ("SRC33", np.float64), ("SRC34", np.float64), ("SRC35", np.float64), ("SRC36", np.float64), ("SRC37", np.float64), ("SRC38", np.float64), ("SRC39", np.float64), ("SRC40", np.float64), ("SRC41", np.float64), ("SRC42", np.float64), ("SRC43", np.float64), ("SRC44", np.float64), ("SRC45", np.float64), ("SRC46", np.float64), ("SRC47", np.float64), ("SRC48", np.float64), ("SRC49", np.float64), ("SRC50", np.float64), ("SRC51", np.float64), ("SRC52", np.float64), ("SRC53", np.float64), ("SRC54", np.float64), ("SRC55", np.float64), ("PRELTV", np.int8), ("SPHMX3", np.int8), ("SPHMX5", np.int8), ("JGSEP", np.int8), ("TWOBOD", np.int8), ("NSATS", np.int8), ("UPARM", np.int8), ("LSRC", np.int8), ("IPYR", np.int16), ("NDEL", np.int16), ("NDOP", np.int16), ("NOBSMT", np.int16), ("NOBSMN", np.int16), ("H", np.float32), ("G", np.float32), ("M1 (MT)", np.float32), ("M2 (MN)", np.float32), ("K1 (MTSMT)", np.float32), ("K2 (MNSMT)", np.float32), ("PHCOF (MNP)", np.float32), ("A1", np.float32), ("A2", np.float32), ("A3", np.float32), ("DT", np.float32), ("R0", np.float32), ("ALN", np.float32), ("NM", np.float32), ("NN", np.float32), ("NK", np.float32), ("S0", np.float32), ("TCL", np.float32), ("RHO", np.float32), ("AMRAT", np.float32), ("AJ1", np.float32), ("AJ2", np.float32), ("ET1", np.float32), ("ET2", np.float32), ("DTH", np.float32), ("ALF", np.float32), ("DEL", np.float32), ("SPHLM3", np.float32), ("SPHLM5", np.float32), ("RP", np.float32), ("GM", np.float32), ("RAD", np.float32), ("EXTNT1", np.float32), ("EXTNT2", np.float32), ("EXTNT3", np.float32), ("MOID", np.float32), ("ALBEDO", np.float32), ("RMSW", np.float32), ("RMSU", np.float32), ("RMSN", np.float32), ("RMSNT", np.float32), ("RMSMT", np.float32), ("RMSMN", np.float32), ("EQUNOX", "|S4"), ("PENAM", "|S6"), ("SBNAM", "|S12"), ("DARC", "|S9"), ("COMNT3", "|S49"), ("COMNT2", "|S80"), ("DESIG", "|S13"), ("COMEST", "|S14"), ("IREF", "|S10"), ("COMNAM", "|S29"), ] ) POLIASTRO_LOCAL_PATH = os.path.join(os.path.expanduser("~"), ".poliastro") DBS_LOCAL_PATH = os.path.join(POLIASTRO_LOCAL_PATH, "dastcom5", "dat") AST_DB_PATH = os.path.join(DBS_LOCAL_PATH, "dast5_le.dat") COM_DB_PATH = os.path.join(DBS_LOCAL_PATH, "dcom5_le.dat") FTP_DB_URL = "ftp://ssd.jpl.nasa.gov/pub/ssd/" def asteroid_db(): """Return complete DASTCOM5 asteroid database. Returns ------- database : numpy.ndarray Database with custom dtype. """ with open(AST_DB_PATH, "rb") as f: f.seek(835, os.SEEK_SET) data = np.fromfile(f, dtype=AST_DTYPE) return data def comet_db(): """Return complete DASTCOM5 comet database. Returns ------- database : numpy.ndarray Database with custom dtype. """ with open(COM_DB_PATH, "rb") as f: f.seek(976, os.SEEK_SET) data = np.fromfile(f, dtype=COM_DTYPE) return data def orbit_from_name(name): """Return :py:class:`~poliastro.twobody.orbit.Orbit` given a name. Retrieve info from JPL DASTCOM5 database. Parameters ---------- name : str NEO name. Returns ------- orbit : list (~poliastro.twobody.orbit.Orbit) NEO orbits. """ records = record_from_name(name) orbits = [] for record in records: orbits.append(orbit_from_record(record)) return orbits def orbit_from_record(record): """Return :py:class:`~poliastro.twobody.orbit.Orbit` given a record. Retrieve info from JPL DASTCOM5 database. Parameters ---------- record : int Object record. Returns ------- orbit : ~poliastro.twobody.orbit.Orbit NEO orbit. """ body_data = read_record(record) a = body_data["A"].item() * u.au ecc = body_data["EC"].item() * u.one inc = body_data["IN"].item() * u.deg raan = body_data["OM"].item() * u.deg argp = body_data["W"].item() * u.deg m = body_data["MA"].item() * u.deg nu = M_to_nu(m, ecc) epoch = Time(body_data["EPOCH"].item(), format="jd", scale="tdb") orbit = Orbit.from_classical(Sun, a, ecc, inc, raan, argp, nu, epoch) orbit._frame = HeliocentricEclipticJ2000(obstime=epoch) return orbit def record_from_name(name): """Search `dastcom.idx` and return logical records that match a given string. Body name, SPK-ID, or alternative designations can be used. Parameters ---------- name : str Body name. Returns ------- records : list (int) DASTCOM5 database logical records matching str. """ records = [] lines = string_record_from_name(name) for line in lines: records.append(int(line[:6].lstrip())) return records def string_record_from_name(name): """Search `dastcom.idx` and return body full record. Search DASTCOM5 index and return body records that match string, containing logical record, name, alternative designations, SPK-ID, etc. Parameters ---------- name : str Body name. Returns ------- lines: list(str) Body records """ idx_path = os.path.join(DBS_LOCAL_PATH, "dastcom.idx") lines = [] with open(idx_path, "r") as inF: for line in inF: if re.search(r"\b" + name.casefold() + r"\b", line.casefold()): lines.append(line) return lines def read_headers(): """Read `DASTCOM5` headers and return asteroid and comet headers. Headers are two numpy arrays with custom dtype. Returns ------- ast_header, com_header : tuple (numpy.ndarray) DASTCOM5 headers. """ ast_path = os.path.join(DBS_LOCAL_PATH, "dast5_le.dat") ast_dtype = np.dtype( [ ("IBIAS1", np.int32), ("BEGINP1", "|S8"), ("BEGINP2", "|S8"), ("BEGINP3", "|S8"), ("ENDPT1", "|S8"), ("ENDPT2", "|S8"), ("ENDPT3", "|S8"), ("CALDATE", "|S19"), ("JDDATE", np.float64), ("FTYP", "|S1"), ("BYTE2A", np.int16), ("IBIAS0", np.int32), ] ) with open(ast_path, "rb") as f: ast_header = np.fromfile(f, dtype=ast_dtype, count=1) com_path = os.path.join(DBS_LOCAL_PATH, "dcom5_le.dat") com_dtype = np.dtype( [ ("IBIAS2", np.int32), ("BEGINP1", "|S8"), ("BEGINP2", "|S8"), ("BEGINP3", "|S8"), ("ENDPT1", "|S8"), ("ENDPT2", "|S8"), ("ENDPT3", "|S8"), ("CALDATE", "|S19"), ("JDDATE", np.float64), ("FTYP", "|S1"), ("BYTE2C", np.int16), ] ) with open(com_path, "rb") as f: com_header = np.fromfile(f, dtype=com_dtype, count=1) return ast_header, com_header def read_record(record): """Read `DASTCOM5` record and return body data. Body data consists of numpy array with custom dtype. Parameters ---------- record : int Body record. Returns ------- body_data : numpy.ndarray Body information. """ ast_header, com_header = read_headers() ast_path = os.path.join(DBS_LOCAL_PATH, "dast5_le.dat") com_path = os.path.join(DBS_LOCAL_PATH, "dcom5_le.dat") # ENDPT1 indicates end of numbered asteroids records if record <= int(ast_header["ENDPT2"][0].item()): # ENDPT2 indicates end of unnumbered asteroids records if record <= int(ast_header["ENDPT1"][0].item()): # phis_rec = record_size * (record_number - IBIAS - 1 (header record)) phis_rec = 835 * (record - ast_header["IBIAS0"][0].item() - 1) else: phis_rec = 835 * (record - ast_header["IBIAS1"][0].item() - 1) with open(ast_path, "rb") as f: f.seek(phis_rec, os.SEEK_SET) body_data = np.fromfile(f, dtype=AST_DTYPE, count=1) else: phis_rec = 976 * (record - com_header["IBIAS2"][0].item() - 1) with open(com_path, "rb") as f: f.seek(phis_rec, os.SEEK_SET) body_data = np.fromfile(f, dtype=COM_DTYPE, count=1) return body_data def download_dastcom5(): """Downloads DASTCOM5 database. Downloads and unzip DASTCOM5 file in default poliastro path (~/.poliastro). """ dastcom5_dir = os.path.join(POLIASTRO_LOCAL_PATH, "dastcom5") dastcom5_zip_path = os.path.join(POLIASTRO_LOCAL_PATH, "dastcom5.zip") if os.path.isdir(dastcom5_dir): raise FileExistsError( "dastcom5 is already created in " + os.path.abspath(dastcom5_dir) ) if not zipfile.is_zipfile(dastcom5_zip_path): if not os.path.isdir(POLIASTRO_LOCAL_PATH): os.makedirs(POLIASTRO_LOCAL_PATH) urllib.request.urlretrieve( FTP_DB_URL + "dastcom5.zip", dastcom5_zip_path, _show_download_progress ) with zipfile.ZipFile(dastcom5_zip_path) as myzip: myzip.extractall(POLIASTRO_LOCAL_PATH) def _show_download_progress(transferred, block, totalsize): trans_mb = transferred * block / (1024 * 1024) total_mb = totalsize / (1024 * 1024) print("%.2f MB / %.2f MB" % (trans_mb, total_mb), end="\r", flush=True) def entire_db(): """Return complete DASTCOM5 database. Merge asteroid and comet databases, only with fields related to orbital data, discarding the rest. Returns ------- database : numpy.ndarray Database with custom dtype. """ ast_database = asteroid_db() com_database = comet_db() ast_database = pd.DataFrame( ast_database[ list(ast_database.dtype.names[:17]) + list(ast_database.dtype.names[-4:-3]) + list(ast_database.dtype.names[-2:]) ] ) ast_database.rename( columns={"ASTNAM": "NAME", "NO": "NUMBER", "CALEPO": "CALEPOCH"}, inplace=True ) com_database = pd.DataFrame( com_database[ list(com_database.dtype.names[:17]) + list(com_database.dtype.names[-4:-3]) + list(com_database.dtype.names[-2:]) ] ) com_database.rename( columns={"COMNAM": "NAME", "NO": "NUMBER", "CALEPO": "CALEPOCH"}, inplace=True ) df = ast_database.append(com_database, ignore_index=True) df[["NAME", "DESIG", "IREF"]] = df[["NAME", "DESIG", "IREF"]].apply( lambda x: x.str.strip().str.decode("utf-8") ) return df poliastro-0.13.1/src/poliastro/plotting/000755 001750 001750 00000000000 13577132207 021120 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/plotting/__init__.py000644 001750 001750 00000000236 13525576227 023241 0ustar00juanlujuanlu000000 000000 from .core import OrbitPlotter2D, OrbitPlotter3D from .static import StaticOrbitPlotter __all__ = ["OrbitPlotter2D", "OrbitPlotter3D", "StaticOrbitPlotter"] poliastro-0.13.1/src/poliastro/plotting/_base.py000644 001750 001750 00000011625 13525576227 022557 0ustar00juanlujuanlu000000 000000 from collections import namedtuple from itertools import cycle from typing import List import numpy as np import plotly.colors from astropy import units as u from astropy.coordinates import CartesianRepresentation from plotly.graph_objects import Figure from poliastro.plotting.util import BODY_COLORS, generate_label from poliastro.util import norm class Trajectory(namedtuple("Trajectory", ["trajectory", "state", "label", "color"])): pass class BaseOrbitPlotter: """ Parent Class for the 2D and 3D OrbitPlotter Classes based on Plotly. """ def __init__(self, figure=None): self._figure = figure or Figure() self._layout = None self._trajectories = [] # type: List[Trajectory] self._attractor = None self._attractor_radius = np.inf * u.km self._color_cycle = cycle(plotly.colors.DEFAULT_PLOTLY_COLORS) @property def trajectories(self): return self._trajectories def _set_attractor(self, attractor): if self._attractor is None: self._attractor = attractor elif attractor is not self._attractor: raise NotImplementedError( f"Attractor has already been set to {self._attractor.name}." ) def set_attractor(self, attractor): """Sets plotting attractor. Parameters ---------- attractor : ~poliastro.bodies.Body Central body. """ self._set_attractor(attractor) def _redraw_attractor(self): # Select a sensible value for the radius: realistic for low orbits, # visible for high and very high orbits min_radius = min( [ trajectory.represent_as(CartesianRepresentation).norm().min() * 0.15 for trajectory, _, _, _ in self._trajectories ] or [0 * u.m] ) radius = max(self._attractor.R.to(u.km), min_radius.to(u.km)) # TODO: Remove previously plotted sphere? self._plot_sphere( radius, BODY_COLORS.get(self._attractor.name, "#999999"), self._attractor.name, ) self._attractor_radius = radius def _plot_point(self, radius, color, name, center=None): raise NotImplementedError def _plot_sphere(self, radius, color, name, center=None): raise NotImplementedError def plot_trajectory(self, trajectory, *, label=None, color=None): """Plots a precomputed trajectory. An attractor must be set first. Parameters ---------- trajectory : ~astropy.coordinates.CartesianRepresentation Trajectory to plot. label : string, optional color : string, optional """ if self._attractor is None: raise ValueError( "An attractor must be set up first, please use " "set_attractor(Major_Body) or plot(orbit)." ) else: if color is None: color = next(self._color_cycle) trace = self._plot_trajectory(trajectory, str(label), color, False) self._trajectories.append( Trajectory(trajectory, None, label, trace.line.color) ) if not self._figure._in_batch_mode: return self.show() def _plot_trajectory(self, trajectory, label, color, dashed): raise NotImplementedError def plot(self, orbit, *, label=None, color=None): """Plots state and osculating orbit in their plane. Parameters ---------- orbit : ~poliastro.twobody.orbit.Orbit Orbit to plot. label : string, optional Label of the orbit. color : string, optional Color of the line and the position. """ if color is None: color = next(self._color_cycle) self._set_attractor(orbit.attractor) label = generate_label(orbit, label) trajectory = orbit.sample() trace = self._plot_trajectory(trajectory, label, color, True) self._trajectories.append( Trajectory(trajectory, orbit.r, label, trace.line.color) ) # Redraw the attractor now to compute the attractor radius self._redraw_attractor() # Plot required 2D/3D shape in the position of the body radius = min( self._attractor_radius * 0.5, (norm(orbit.r) - orbit.attractor.R) * 0.5 ) # Arbitrary thresholds self._plot_point(radius, color, label, center=orbit.r) if not self._figure._in_batch_mode: return self.show() def _prepare_plot(self): if self._attractor is not None: self._redraw_attractor() self._figure.layout.update(self._layout) def show(self): """Shows the plot in the Notebook. Updates the layout and returns the underlying figure. """ self._prepare_plot() return self._figure poliastro-0.13.1/src/poliastro/plotting/core.py000644 001750 001750 00000014640 13525576227 022436 0ustar00juanlujuanlu000000 000000 """ Plotting utilities. """ import numpy as np from astropy import units as u from astropy.coordinates import CartesianRepresentation from plotly.graph_objects import Layout, Scatter, Scatter3d, Surface from poliastro.plotting.util import generate_sphere from poliastro.util import norm from ._base import BaseOrbitPlotter class OrbitPlotter3D(BaseOrbitPlotter): """OrbitPlotter3D class. """ def __init__(self, figure=None, dark=False): super().__init__(figure) self._layout = Layout( autosize=True, scene=dict( xaxis=dict(title="x (km)"), yaxis=dict(title="y (km)"), zaxis=dict(title="z (km)"), aspectmode="data", # Important! ), ) if dark: self._layout.template = "plotly_dark" def _plot_point(self, radius, color, name, center=[0, 0, 0] * u.km): # We use _plot_sphere here because it's not easy to specify the size of a marker # in data units instead of pixels, see # https://stackoverflow.com/q/47086547 return self._plot_sphere(radius, color, name, center) def _plot_sphere(self, radius, color, name, center=[0, 0, 0] * u.km): xx, yy, zz = generate_sphere(radius, center) sphere = Surface( x=xx.to(u.km).value, y=yy.to(u.km).value, z=zz.to(u.km).value, name=name, colorscale=[[0, color], [1, color]], cauto=False, cmin=1, cmax=1, showscale=False, ) self._figure.add_trace(sphere) return sphere def _plot_trajectory(self, trajectory, label, color, dashed): trace = Scatter3d( x=trajectory.x.to(u.km).value, y=trajectory.y.to(u.km).value, z=trajectory.z.to(u.km).value, name=label, line=dict(color=color, width=5, dash="dash" if dashed else "solid"), mode="lines", # Boilerplate ) self._figure.add_trace(trace) return trace @u.quantity_input(elev=u.rad, azim=u.rad, distance=u.km) def set_view(self, elev, azim, distance=5 * u.km): x = distance * np.cos(elev) * np.cos(azim) y = distance * np.cos(elev) * np.sin(azim) z = distance * np.sin(elev) self._layout.update( { "scene": { "camera": { "eye": { "x": x.to(u.km).value, "y": y.to(u.km).value, "z": z.to(u.km).value, } } } } ) if not self._figure._in_batch_mode: return self.show() class OrbitPlotter2D(BaseOrbitPlotter): """OrbitPlotter2D class. .. versionadded:: 0.9.0 """ def __init__(self, figure=None): super().__init__(figure) self._layout = Layout( autosize=True, xaxis=dict(title="x (km)", constrain="domain"), yaxis=dict(title="y (km)", scaleanchor="x"), shapes=[], ) self._frame = None def _project(self, rr): rr_proj = rr - rr.dot(self._frame[2])[:, None] * self._frame[2] x = rr_proj.dot(self._frame[0]) y = rr_proj.dot(self._frame[1]) return x, y def _plot_point(self, radius, color, name, center=[0, 0, 0] * u.km): x_center, y_center = self._project( center[None] ) # Indexing trick to add one extra dimension trace = Scatter( x=x_center.to(u.km).value, y=y_center.to(u.km).value, mode="markers", marker=dict(size=10, color=color), name=name, ) self._figure.add_trace(trace) return trace def _plot_sphere(self, radius, color, name, center=[0, 0, 0] * u.km): x_center, y_center = self._project( center[None] ) # Indexing trick to add one extra dimension shape = { "type": "circle", "xref": "x", "yref": "y", "x0": (x_center[0] - radius).to(u.km).value, "y0": (y_center[0] - radius).to(u.km).value, "x1": (x_center[0] + radius).to(u.km).value, "y1": (y_center[0] + radius).to(u.km).value, "opacity": 1, "fillcolor": color, "line": {"color": color}, } self._layout.shapes += (shape,) return shape def _plot_trajectory(self, trajectory, label, color, dashed): if self._frame is None: raise ValueError( "A frame must be set up first, please use " "set_frame(*orbit.pqw()) or plot(orbit)." ) rr = trajectory.represent_as(CartesianRepresentation).xyz.transpose() x, y = self._project(rr) trace = Scatter( x=x.to(u.km).value, y=y.to(u.km).value, name=label, line=dict(color=color, width=2, dash="dash" if dashed else "solid"), hoverinfo="none", # TODO: Review mode="lines", # Boilerplate ) self._figure.add_trace(trace) return trace def set_frame(self, p_vec, q_vec, w_vec): """Sets perifocal frame. Raises ------ ValueError If the vectors are not a set of mutually orthogonal unit vectors. """ if self._frame and self.trajectories: raise NotImplementedError( "OrbitPlotter2D does not support reprojecting yet" ) if not np.allclose([norm(v) for v in (p_vec, q_vec, w_vec)], 1): raise ValueError("Vectors must be unit.") elif not np.allclose([p_vec.dot(q_vec), q_vec.dot(w_vec), w_vec.dot(p_vec)], 0): raise ValueError("Vectors must be mutually orthogonal.") else: self._frame = p_vec, q_vec, w_vec def plot(self, orbit, *, label=None, color=None): """Plots state and osculating orbit in their plane. Parameters ---------- orbit : ~poliastro.twobody.orbit.Orbit Orbit to plot. label : string, optional Label of the orbit. color : string, optional Color of the line and the position. """ if not self._frame: self.set_frame(*orbit.pqw()) return super().plot(orbit, label=label, color=color) poliastro-0.13.1/src/poliastro/plotting/misc.py000644 001750 001750 00000004215 13525576227 022436 0ustar00juanlujuanlu000000 000000 from typing import Union from poliastro.bodies import ( Earth, Jupiter, Mars, Mercury, Neptune, Saturn, Uranus, Venus, ) from poliastro.twobody import Orbit from .core import OrbitPlotter2D, OrbitPlotter3D from .static import StaticOrbitPlotter def _plot_bodies(orbit_plotter, outer=True, epoch=None): bodies = [Mercury, Venus, Earth, Mars] if outer: bodies.extend([Jupiter, Saturn, Uranus, Neptune]) for body in bodies: orb = Orbit.from_body_ephem(body, epoch) orbit_plotter.plot(orb, label=str(body)) def _plot_solar_system_2d(outer=True, epoch=None, interactive=False): pqw = Orbit.from_body_ephem(Earth, epoch).pqw() if interactive: orbit_plotter = ( OrbitPlotter2D() ) # type: Union[OrbitPlotter2D, StaticOrbitPlotter] orbit_plotter.set_frame(*pqw) else: orbit_plotter = StaticOrbitPlotter() orbit_plotter.set_frame(*pqw) _plot_bodies(orbit_plotter, outer, epoch) return orbit_plotter def _plot_solar_system_3d(outer=True, epoch=None): orbit_plotter = OrbitPlotter3D() _plot_bodies(orbit_plotter, outer, epoch) return orbit_plotter def plot_solar_system(outer=True, epoch=None, use_3d=False, interactive=False): """ Plots the whole solar system in one single call. .. versionadded:: 0.9.0 Parameters ------------ outer : bool, optional Whether to print the outer Solar System, default to True. epoch : ~astropy.time.Time, optional Epoch value of the plot, default to J2000. use_3d : bool, optional Produce 3D plot, default to False. interactive : bool, optional Produce an interactive (rather than static) image of the orbit, default to False. This option requires Plotly properly installed and configured for your environment. """ if not interactive and use_3d: raise ValueError( "The static plotter does not support 3D, use `interactive=True`" ) elif use_3d: op = _plot_solar_system_3d(outer, epoch) else: op = _plot_solar_system_2d(outer, epoch, interactive) return op poliastro-0.13.1/src/poliastro/plotting/porkchop.py000644 001750 001750 00000015060 13577127432 023325 0ustar00juanlujuanlu000000 000000 """ This is the script for porkchop plotting """ import matplotlib.pyplot as plt import numpy as np from astropy import coordinates as coord, units as u from poliastro.bodies import ( Earth, Jupiter, Mars, Mercury, Moon, Neptune, Pluto, Saturn, Sun, Uranus, Venus, ) from poliastro.maneuver import Maneuver from poliastro.twobody.orbit import Orbit from poliastro.util import norm def _get_state(body, time): """ Computes the position of a body for a given time. """ solar_system_bodies = [ Sun, Mercury, Venus, Earth, Moon, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, ] # We check if body belongs to poliastro.bodies if body in solar_system_bodies: rr, vv = coord.get_body_barycentric_posvel(body.name, time) else: rr, vv = body.propagate(time).rv() rr = coord.CartesianRepresentation(rr) vv = coord.CartesianRepresentation(vv) return rr.xyz, vv.xyz def _targetting(departure_body, target_body, t_launch, t_arrival): """This function returns the increment in departure and arrival velocities.""" # Get position and velocities for departure and arrival rr_dpt_body, vv_dpt_body = _get_state(departure_body, t_launch) rr_arr_body, vv_arr_body = _get_state(target_body, t_arrival) # Transform into Orbit objects attractor = departure_body.parent ss_dpt = Orbit.from_vectors(attractor, rr_dpt_body, vv_dpt_body, epoch=t_launch) ss_arr = Orbit.from_vectors(attractor, rr_arr_body, vv_arr_body, epoch=t_arrival) # Define time of flight tof = ss_arr.epoch - ss_dpt.epoch if tof <= 0: return None, None, None, None, None try: # Lambert is now a Maneuver object man_lambert = Maneuver.lambert(ss_dpt, ss_arr) # Get norm delta velocities dv_dpt = norm(man_lambert.impulses[0][1]) dv_arr = norm(man_lambert.impulses[1][1]) # Compute all the output variables c3_launch = dv_dpt ** 2 c3_arrival = dv_arr ** 2 return ( dv_dpt.to(u.km / u.s).value, dv_arr.to(u.km / u.s).value, c3_launch.to(u.km ** 2 / u.s ** 2).value, c3_arrival.to(u.km ** 2 / u.s ** 2).value, tof.jd, ) except AssertionError: return None, None, None, None, None # numpy.vectorize is amazing targetting_vec = np.vectorize( _targetting, otypes=[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray], excluded=["departure_body", "target_body"], ) def porkchop( departure_body, target_body, launch_span, arrival_span, ax=None, tfl=True, vhp=True, max_c3=45.0 * u.km ** 2 / u.s ** 2, max_vhp=5 * u.km / u.s, ): """Plots porkchop between two bodies. Parameters ---------- departure_body: poliastro.bodies.Body Body from which departure is done target_body: poliastro.bodies.Body Body for targetting launch_span: astropy.time.Time Time span for launch arrival_span: astropy.time.Time Time span for arrival ax: matplotlib.axes.Axes: For custom figures tfl: boolean For plotting time flight contour lines vhp: boolean For plotting arrival velocity contour lines max_c3: float Sets the maximum C3 value for porkchop max_vhp: float Sets the maximum arrival velocity for porkchop Returns ------- c3_launch: np.ndarray Characteristic launch energy c3_arrrival: np.ndarray Characteristic arrival energy tof: np.ndarray Time of flight for each transfer Example ------- >>> from poliastro.plotting.porkchop import porkchop >>> from poliastro.bodies import Earth, Mars >>> from poliastro.util import time_range >>> launch_span = time_range("2005-04-30", end="2005-10-07") >>> arrival_span = time_range("2005-11-16", end="2006-12-21") >>> dv_launch, dev_dpt, c3dpt, c3arr, tof = porkchop(Earth, Mars, launch_span, arrival_span) """ dv_launch, dv_arrival, c3_launch, c3_arrival, tof = targetting_vec( departure_body, target_body, launch_span[np.newaxis, :], arrival_span[:, np.newaxis], ) # Start drawing porkchop if ax is None: fig, ax = plt.subplots(figsize=(15, 15)) else: fig = ax.figure c3_levels = np.linspace(0, max_c3.to(u.km ** 2 / u.s ** 2).value, 30) c = ax.contourf( [D.to_datetime() for D in launch_span], [A.to_datetime() for A in arrival_span], c3_launch, c3_levels, ) line = ax.contour( [D.to_datetime() for D in launch_span], [A.to_datetime() for A in arrival_span], c3_launch, c3_levels, colors="black", linestyles="solid", ) cbar = fig.colorbar(c) cbar.set_label("km2 / s2") ax.clabel(line, inline=1, fmt="%1.1f", colors="k", fontsize=10) if tfl: time_levels = np.linspace(100, 500, 5) tfl_contour = ax.contour( [D.to_datetime() for D in launch_span], [A.to_datetime() for A in arrival_span], tof, time_levels, colors="red", linestyles="dashed", linewidths=3.5, ) ax.clabel(tfl_contour, inline=1, fmt="%1.1f", colors="r", fontsize=14) if vhp: vhp_levels = np.linspace(0, max_vhp.to(u.km / u.s).value, 5) vhp_contour = ax.contour( [D.to_datetime() for D in launch_span], [A.to_datetime() for A in arrival_span], dv_arrival, vhp_levels, colors="navy", linewidths=2.0, ) ax.clabel(vhp_contour, inline=1, fmt="%1.1f", colors="navy", fontsize=12) ax.grid() fig.autofmt_xdate() if not hasattr(target_body, "name"): ax.set_title( f"{departure_body.name} - Target Body for year {launch_span[0].datetime.year}, C3 Launch", fontsize=14, fontweight="bold", ) else: ax.set_title( f"{departure_body.name} - {target_body.name} for year {launch_span[0].datetime.year}, C3 Launch", fontsize=14, fontweight="bold", ) ax.set_xlabel("Launch date", fontsize=10, fontweight="bold") ax.set_ylabel("Arrival date", fontsize=10, fontweight="bold") return ( dv_launch * u.km / u.s, dv_arrival * u.km / u.s, c3_launch * u.km ** 2 / u.s ** 2, c3_arrival * u.km ** 2 / u.s ** 2, tof * u.d, ) poliastro-0.13.1/src/poliastro/plotting/static.py000644 001750 001750 00000020460 13577127436 022773 0ustar00juanlujuanlu000000 000000 from typing import List import matplotlib as mpl import numpy as np from astropy import units as u from astropy.coordinates import CartesianRepresentation from matplotlib import pyplot as plt from matplotlib.collections import LineCollection from matplotlib.colors import LinearSegmentedColormap, to_rgba from poliastro.plotting.util import BODY_COLORS, generate_label from poliastro.util import norm from ._base import Trajectory def _segments_from_arrays(x, y): # Copied pasted from # https://matplotlib.org/3.1.0/gallery/lines_bars_and_markers/multicolored_line.html # because this API is impossible to understand points = np.column_stack([x.to(u.km).value, y.to(u.km).value])[:, None, :] segments = np.concatenate([points[:-1], points[1:]], axis=1) return segments class StaticOrbitPlotter: """StaticOrbitPlotter class. This class holds the perifocal plane of the first :py:class:`~poliastro.twobody.orbit.Orbit` plotted in it using :py:meth:`plot`, so all following plots will be projected on that plane. Alternatively, you can call :py:meth:`set_frame` to set the frame before plotting. """ def __init__(self, ax=None, num_points=150, dark=False): """Constructor. Parameters ---------- ax : ~matplotlib.axes.Axes Axes in which to plot. If not given, new ones will be created. num_points : int, optional Number of points to use in plots, default to 150. dark : bool, optional If set as True, plots the orbit in Dark mode. """ self.ax = ax if not self.ax: if dark: with plt.style.context("dark_background"): _, self.ax = plt.subplots(figsize=(6, 6)) else: _, self.ax = plt.subplots(figsize=(6, 6)) self.num_points = num_points self._frame = None self._attractor = None self._attractor_radius = np.inf * u.km self._trajectories = [] # type: List[Trajectory] @property def trajectories(self): return self._trajectories def set_frame(self, p_vec, q_vec, w_vec): """Sets perifocal frame. Raises ------ ValueError If the vectors are not a set of mutually orthogonal unit vectors. """ if not np.allclose([norm(v) for v in (p_vec, q_vec, w_vec)], 1): raise ValueError("Vectors must be unit.") elif not np.allclose([p_vec.dot(q_vec), q_vec.dot(w_vec), w_vec.dot(p_vec)], 0): raise ValueError("Vectors must be mutually orthogonal.") else: self._frame = p_vec, q_vec, w_vec if self._trajectories: self._redraw() def _get_colors(self, color, trail): if trail and color is None: # HACK: https://stackoverflow.com/a/13831816/554319 color = next(self.ax._get_lines.prop_cycler)["color"] if trail: colors = [color, to_rgba(color, 0)] else: colors = [color] return colors def _redraw(self): for artist in self.ax.lines + self.ax.collections: artist.remove() for trajectory, state, label, colors in self._trajectories: self._plot(trajectory, state, label, colors) self.ax.relim() self.ax.autoscale() def _plot_trajectory(self, trajectory, colors=None, linestyle="dashed"): rr = trajectory.represent_as(CartesianRepresentation).xyz.transpose() x, y = self._project(rr) if len(colors) > 1: segments = _segments_from_arrays(x, y) cmap = LinearSegmentedColormap.from_list( f"{colors[0]}_to_alpha", colors # Useless name ) lc = LineCollection(segments, linestyles=linestyle, cmap=cmap) lc.set_array(np.linspace(1, 0, len(x))) self.ax.add_collection(lc) lines = [lc] else: lines = self.ax.plot( x.to(u.km).value, y.to(u.km).value, linestyle=linestyle, color=colors[0] ) colors = [lines[0].get_color()] return lines, colors def plot_trajectory(self, trajectory, *, label=None, color=None, trail=False): """Plots a precomputed trajectory. Parameters ---------- trajectory : ~astropy.coordinates.BaseRepresentation, ~astropy.coordinates.BaseCoordinateFrame Trajectory to plot. label : str, optional Label. color : str, optional Color string. trail: bool, optional Plots the Orbit's trail """ if self._attractor is None or self._frame is None: raise ValueError( "An attractor and a frame must be set up first, please use " "set_attractor(Major_Body) and set_frame(*orbit.pqw()) " "or plot(orbit)." ) self._redraw_attractor( trajectory.represent_as(CartesianRepresentation).norm().min() * 0.15 ) # Arbitrary threshold colors = self._get_colors(color, trail) lines, colors = self._plot_trajectory(trajectory, colors) if label: lines[0].set_label(label) self.ax.legend( loc="upper left", bbox_to_anchor=(1.05, 1.015), title="Names and epochs" ) self._trajectories.append(Trajectory(trajectory, None, label, colors)) return lines def set_attractor(self, attractor): """Sets plotting attractor. Parameters ---------- attractor : ~poliastro.bodies.Body Central body. """ if self._attractor is None: self._attractor = attractor elif attractor is not self._attractor: raise NotImplementedError( f"Attractor has already been set to {self._attractor.name}." ) def _project(self, rr): rr_proj = rr - rr.dot(self._frame[2])[:, None] * self._frame[2] x = rr_proj.dot(self._frame[0]) y = rr_proj.dot(self._frame[1]) return x, y def _redraw_attractor(self, min_radius=0 * u.km): radius = max(self._attractor.R.to(u.km), min_radius.to(u.km)) color = BODY_COLORS.get(self._attractor.name, "#999999") for attractor in self.ax.findobj(match=mpl.patches.Circle): attractor.remove() if radius < self._attractor_radius: self._attractor_radius = radius self.ax.add_patch( mpl.patches.Circle((0, 0), self._attractor_radius.value, lw=0, color=color) ) def _plot(self, trajectory, state=None, label=None, colors=None): lines, colors = self._plot_trajectory(trajectory, colors) if state is not None: x0, y0 = self._project(state[None]) # Plot current position (l,) = self.ax.plot( x0.to(u.km).value, y0.to(u.km).value, "o", mew=0, color=colors[0] ) lines.append(l) if label: if not self.ax.get_legend(): size = self.ax.figure.get_size_inches() + [8, 0] self.ax.figure.set_size_inches(size) # This will apply the label to either the point or the osculating # orbit depending on the last plotted line # NOTE: What about generating both labels, # indicating that one is the osculating orbit? lines[-1].set_label(label) self.ax.legend( loc="upper left", bbox_to_anchor=(1.05, 1.015), title="Names and epochs" ) self.ax.set_xlabel("$x$ (km)") self.ax.set_ylabel("$y$ (km)") self.ax.set_aspect(1) return lines, colors def plot(self, orbit, label=None, color=None, trail=False): """Plots state and osculating orbit in their plane. """ if not self._frame: self.set_frame(*orbit.pqw()) self.set_attractor(orbit.attractor) self._redraw_attractor(orbit.r_p * 0.15) # Arbitrary threshold positions = orbit.sample(self.num_points) if label: label = generate_label(orbit, label) colors = self._get_colors(color, trail) lines, colors = self._plot(positions, orbit.r, label, colors) self._trajectories.append(Trajectory(positions, orbit.r, label, colors)) return lines poliastro-0.13.1/src/poliastro/plotting/util.py000644 001750 001750 00000001523 13525576227 022457 0ustar00juanlujuanlu000000 000000 import numpy as np BODY_COLORS = {"Sun": "#ffcc00", "Earth": "#204a87", "Jupiter": "#ba3821"} def generate_label(orbit, label): orbit.epoch.out_subfmt = "date_hm" label_ = f"{orbit.epoch.iso}" if label: label_ += f" ({label})" return label_ def generate_sphere(radius, center, num=20): u1 = np.linspace(0, 2 * np.pi, num) v1 = u1.copy() uu, vv = np.meshgrid(u1, v1) x_center, y_center, z_center = center xx = x_center + radius * np.cos(uu) * np.sin(vv) yy = y_center + radius * np.sin(uu) * np.sin(vv) zz = z_center + radius * np.cos(vv) return xx, yy, zz def generate_circle(radius, center, num=500): u1 = np.linspace(0, 2 * np.pi, num) x_center, y_center, z_center = center xx = x_center + radius * np.cos(u1) yy = y_center + radius * np.sin(u1) return xx, yy poliastro-0.13.1/src/poliastro/spheroid_location.py000644 001750 001750 00000010740 13525576227 023350 0ustar00juanlujuanlu000000 000000 import numpy as np class SpheroidLocation(object): """ Class representing a ground station on an arbitrary ellipsoid. """ def __init__(self, lon, lat, h, body): """ Parameters ---------- lon: ~astropy.units.quantity.Quantity geodetic longitude lat: ~astropy.units.quantity.Quantity geodetic latitude h: ~astropy.units.quantity.Quantity geodetic height body: ~poliastro.bodies.Body Planetary body the spheroid location lies on """ self._lon = lon self._lat = lat self._h = h self._a = body.R self._b = body.R self._c = body.R_polar @property def cartesian_cords(self): """Convert to the Cartesian Coordinate system. """ e2 = 1 - (self._c / self._a) ** 2 N = self._a / np.sqrt(1 - e2 * np.sin(self._lon) ** 2) x = (N + self._h) * np.cos(self._lon) * np.cos(self._lat) y = (N + self._h) * np.cos(self._lon) * np.sin(self._lat) z = ((1 - e2) * N + self._h) * np.sin(self._lon) return x, y, z @property def f(self): """Get first flattening. """ return 1 - self._c / self._a @property def N(self): """Normal vector of the ellipsoid at the given location. """ x, y, z = self.cartesian_cords a, b, c = self._a.value, self._b.value, self._c.value N = np.array([2 * x.value / a ** 2, 2 * y.value / b ** 2, 2 * z.value / c ** 2]) N /= np.linalg.norm(N) return N @property def tangential_vecs(self): """Returns orthonormal vectors tangential to the ellipsoid at the given location. """ N = self.N u = np.array([1.0, 0, 0]) u -= u.dot(N) * N u /= np.linalg.norm(u) v = np.cross(N, u) return u, v @property def radius_of_curvature(self): """Radius of curvature of the meridian at the latitude of the given location. """ e2 = 1 - (self._c / self._a) ** 2 rc = self._a * (1 - e2) / (1 - e2 * np.sin(self._lat) ** 2) ** 1.5 return rc def distance(self, px, py, pz): """ Calculates the distane from an arbitrary point to the given location (Cartesian coordinates). Parameters ---------- px: ~astropy.units.quantity.Quantity x-coordinate of the point py: ~astropy.units.quantity.Quantity y-coordinate of the point pz: ~astropy.units.quantity.Quantity z-coordinate of the point """ c = np.array([c.value for c in self.cartesian_cords]) u = np.array([px.value, py.value, pz.value]) d = np.linalg.norm(c - u) return d def is_visible(self, px, py, pz): """ Determines whether an object located at a given point is visible from the given location. Returns true if true, false otherwise. Parameters ---------- px: ~astropy.units.quantity.Quantity x-coordinate of the point py: ~astropy.units.quantity.Quantity y-coordinate of the point pz: ~astropy.units.quantity.Quantity z-coordinate of the point """ c = [c.value for c in self.cartesian_cords] u = np.array([px.value, py.value, pz.value]) N = self.N d = -N.dot(c) p = N.dot(u) + d return p >= 0 def cartesian_to_ellipsoidal(self, x, y, z): """ Converts ellipsoidal coordinates to the Cartesian coordinate system for the given ellipsoid. Instead of the iterative formula, the function uses the approximation introduced in Bowring, B. R. (1976). TRANSFORMATION FROM SPATIAL TO GEOGRAPHICAL COORDINATES Parameters ---------- x: ~astropy.units.quantity.Quantity x coordinate y: ~astropy.units.quantity.Quantity y coordinate z: ~astropy.units.quantity.Quantity z coordinate """ a = self._a c = self._c e2 = 1 - (c / a) ** 2 e2_ = e2 / (1 - e2) p = np.sqrt(x ** 2 + y ** 2) th = np.arctan(z * a / (p * c)) lon = np.arctan(y / x) lat = np.arctan( (z + e2_ * c * np.sin(th) ** 3) / (p - e2 * a * np.cos(th) ** 3) ) v = a / np.sqrt(1 - e2 * np.sin(lat) ** 2) h = p / np.cos(lat) - v h = z / np.sin(lat) - (1 - e2) * v return lat, lon, h poliastro-0.13.1/src/poliastro/testing.py000644 001750 001750 00000000260 13577127432 021311 0ustar00juanlujuanlu000000 000000 """Testing utilities. """ import os.path import pytest def test(): """Initiate poliastro testing """ pytest.main([os.path.dirname(os.path.abspath(__file__))]) poliastro-0.13.1/src/poliastro/tests/000755 001750 001750 00000000000 13577132207 020422 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/tests/test_bodies.py000644 001750 001750 00000003676 13577127432 023320 0ustar00juanlujuanlu000000 000000 import pytest from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from poliastro import bodies def test_body_has_k_given_in_constructor(): k = 3.98e5 * u.km ** 3 / u.s ** 2 earth = bodies.Body(None, k, "") assert earth.k == k def test_body_from_parameters_raises_valueerror_if_k_units_not_correct(): wrong_k = 4902.8 * u.kg _name = _symbol = "" _R = 0 with pytest.raises(u.UnitsError) as excinfo: bodies.Body.from_parameters(None, wrong_k, _name, _symbol, _R) assert ( "UnitsError: Argument 'k' to function 'from_parameters' must be in units convertible to 'km3 / s2'." in excinfo.exconly() ) def test_body_printing_has_name_and_symbol(): name = "2 Pallas" symbol = u"\u26b4" k = 1.41e10 * u.m ** 3 / u.s ** 2 pallas2 = bodies.Body(None, k, name, symbol) assert name in str(pallas2) assert symbol in str(pallas2) def test_earth_has_k_given_in_literature(): expected_k = 3.986004418e14 * u.m ** 3 / u.s ** 2 k = bodies.Earth.k assert_quantity_allclose(k.decompose([u.km, u.s]), expected_k) def test_body_kwargs(): name = "2 Pallas" symbol = u"\u26b4" k = 1.41e10 * u.m ** 3 / u.s ** 2 pallas2 = bodies.Body(None, k, name, symbol) assert pallas2.kwargs == {} pallas2 = bodies.Body(None, k, name, symbol, custom="data") assert "custom" in pallas2.kwargs def test_from_relative(): TRAPPIST1 = bodies.Body.from_relative( reference=bodies.Sun, parent=None, k=0.08, # Relative to the Sun name="TRAPPIST", symbol=None, R=0.114, ) # Relative to the Sun # check values properly calculated VALUECHECK = bodies.Body.from_relative( reference=bodies.Earth, parent=TRAPPIST1, k=1, name="VALUECHECK", symbol=None, R=1, ) assert bodies.Earth.k == VALUECHECK.k assert bodies.Earth.R == VALUECHECK.R poliastro-0.13.1/src/poliastro/tests/test_coordinates.py000644 001750 001750 00000005046 13577127432 024356 0ustar00juanlujuanlu000000 000000 import astropy.units as u from astropy import time from astropy.tests.helper import assert_quantity_allclose from numpy.linalg import norm from poliastro import bodies, coordinates from poliastro.examples import molniya from poliastro.twobody.orbit import Orbit # Note that function are tested using astropy current builtin ephemeris. # Horizons uses JPL ephemeris DE431, so expected values are hardcoded, # instead of being obtained using Horizons. def test_body_centered_to_icrs_transformation(): vexpress_r_venus = [ -2.707041558060933e03, 1.112962479175306e04, -3.792944408664889e04, ] * u.km vexpress_v_venus = ( [-2.045118200275925e-01, 7.978578482960554e-01, 2.664944903217139e00] * u.km / u.s ) expected_r = [ -3.47202219448080286e07, 9.16853879708216339e07, 4.34117810525591150e07, ] * u.km expected_v = ( [-3.34053728321152121e01, -1.16604776013667291e01, -2.39943678872506838e-01] * u.km / u.s ) r, v = coordinates.body_centered_to_icrs( vexpress_r_venus, vexpress_v_venus, bodies.Venus, time.Time("2014-08-23 00:00", scale="tdb"), ) assert_quantity_allclose(r, expected_r) assert_quantity_allclose(v, expected_v) def test_icrs_to_body_centered_transformation(): vexpress_r_icrs = [ -3.472125578094885e07, 9.168528034176737e07, 4.341160627674723e07, ] * u.km vexpress_v_icrs = ( [-3.340574196483147e01, -1.165974037637970e01, -2.395829145441408e-01] * u.km / u.s ) expected_r = [ -3.74486105008138566e03, 1.10085874027602295e04, -3.80681106516677464e04, ] * u.km expected_v = ( [-2.04845025352488774e-01, 7.98692896032012989e-01, 2.66498465286454023e00] * u.km / u.s ) r, v = coordinates.icrs_to_body_centered( vexpress_r_icrs, vexpress_v_icrs, bodies.Venus, time.Time("2014-08-23 00:00", scale="tdb"), ) assert_quantity_allclose(r, expected_r) assert_quantity_allclose(v, expected_v) def test_inertial_body_centered_to_pqw(): molniya_r_peri, molniya_v_peri = coordinates.inertial_body_centered_to_pqw( molniya.r, molniya.v, bodies.Earth ) molniya_peri = Orbit.from_vectors( bodies.Earth, molniya_r_peri, molniya_v_peri, molniya.epoch ) assert_quantity_allclose(molniya_peri.e_vec[-2:], [0, 0], atol=1e-12) assert_quantity_allclose(norm(molniya_peri.e_vec), norm(molniya.e_vec)) poliastro-0.13.1/src/poliastro/tests/test_czml.py000644 001750 001750 00000027531 13577127432 023014 0ustar00juanlujuanlu000000 000000 import pytest # noqa: E402 isort:skip import sys from astropy import units as u # noqa: E402 from poliastro.examples import iss, molniya # noqa: E402 try: from poliastro.czml.extract_czml import CZMLExtractor # noqa: E402 import czml3 # noqa: F401 except ImportError: pass @pytest.mark.skipif("czml3" not in sys.modules, reason="requires czml3") def test_czml_custom_packet(): start_epoch = iss.epoch end_epoch = iss.epoch + molniya.period sample_points = 10 ellipsoidr = [6373100, 6373100, 6373100] pr_map_url = ( "https://upload.wikimedia.org/wikipedia/commons/c/c4/Earthmap1000x500compac.jpg" ) scene = False expected_packet = """{ "id": "custom_properties", "properties": { "custom_attractor": true, "ellipsoid": [ { "array": [ 6373100, 6373100, 6373100 ] } ], "map_url": "https://upload.wikimedia.org/wikipedia/commons/c/c4/Earthmap1000x500compac.jpg", "scene3D": false } }""" extractor = CZMLExtractor( start_epoch, end_epoch, sample_points, ellipsoid=ellipsoidr, pr_map=pr_map_url, scene3D=scene, ) pckt = extractor.packets[-1] # Test that custom packet parameters where set correctly assert repr(pckt) == expected_packet @pytest.mark.skipif("czml3" not in sys.modules, reason="requires czml3") def test_czml_add_orbit(): start_epoch = iss.epoch end_epoch = iss.epoch + molniya.period sample_points = 10 expected_doc = """[{ "id": "document", "version": "1.0", "name": "document_packet", "clock": { "interval": "2013-03-18T12:00:00.000/2013-03-18T23:59:35.108", "currentTime": "2013-03-18T12:00:00.000", "multiplier": 60, "range": "LOOP_STOP", "step": "SYSTEM_CLOCK_MULTIPLIER" } }, { "id": "custom_properties", "properties": { "custom_attractor": true, "ellipsoid": [ { "array": [ 6378137.0, 6378137.0, 6356752.314245179 ] } ], "map_url": [ "https://upload.wikimedia.org/wikipedia/commons/c/c4/Earthmap1000x500compac.jpg" ], "scene3D": true } }, { "id": 0, "availability": "2013-03-18T12:00:00Z/2013-03-18T23:59:35Z", "position": { "epoch": "2013-03-18T12:00:00.000", "interpolationAlgorithm": "LAGRANGE", "interpolationDegree": 5, "referenceFrame": "INERTIAL", "cartesian": [ 0.0, -5874061.7735, 20159787.8726, 40258166.1202, 4317.5108, -11640044.4188, 17864068.2606, 35673719.9898, 8635.0217, -16129541.0199, 13690511.4237, 27339319.5726, 12952.5325, -17354822.809, 6974596.3021, 13927946.9767, 17270.0433, -2602428.8844, -2846586.9794, -5684502.8438, 21587.5541, 17029383.6013, 5939060.5902, 11860029.9442, 25905.065, 16513858.1069, 13042152.6757, 26044577.071, 30222.5758, 12264870.9449, 17472285.3551, 34891347.6059, 34540.0866, 6599717.4359, 19973508.888, 39886175.5838, 38857.5975, 377235.7083, 20840447.5591, 41617412.1057, 43175.1083, -5874061.7735, 20159787.8726, 40258166.1202, 47492.6191, -11640044.4188, 17864068.2606, 35673719.9898 ] }, "billboard": { "show": true, "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADJSURBVDhPnZHRDcMgEEMZjVEYpaNklIzSEfLfD4qNnXAJSFWfhO7w2Zc0Tf9QG2rXrEzSUeZLOGm47WoH95x3Hl3jEgilvDgsOQUTqsNl68ezEwn1vae6lceSEEYvvWNT/Rxc4CXQNGadho1NXoJ+9iaqc2xi2xbt23PJCDIB6TQjOC6Bho/sDy3fBQT8PrVhibU7yBFcEPaRxOoeTwbwByCOYf9VGp1BYI1BA+EeHhmfzKbBoJEQwn1yzUZtyspIQUha85MpkNIXB7GizqDEECsAAAAASUVORK5CYII=" }, "label": { "text": "Molniya", "font": "11pt Lucida Console", "style": "FILL", "fillColor": { "rgba": [ 125, 80, 120, 255 ] }, "outlineColor": { "rgba": [ 255, 255, 0, 255 ] }, "outlineWidth": 1.0 }, "path": { "resolution": 120, "material": { "solidColor": { "color": { "rgba": [ 255, 255, 0, 255 ] } } } } }, { "id": 1, "availability": "2013-03-18T12:00:00Z/2013-03-18T23:59:35Z", "position": { "epoch": "2013-03-18T12:00:00.000", "interpolationAlgorithm": "LAGRANGE", "interpolationDegree": 5, "referenceFrame": "INERTIAL", "cartesian": [ 0.0, 859072.56, -4137203.68, 5295568.71, 4317.5108, -6275612.3001, -2500051.0065, 496823.4938, 8635.0217, -2951952.7192, 3308039.2745, -5135374.5943, 12952.5325, 5284474.5583, 3626657.2697, -2240110.0348, 17270.0433, 4755661.276, -2067765.0104, 4367882.9214, 21587.5541, -3674993.328, -4315653.7297, 3705413.3249, 25905.065, -5976376.971, 633013.0667, -3135590.7655, 30222.5758, 1672241.9629, 4536136.0948, -4766186.3254, 34540.0866, 6551964.1874, 915641.8629, 1510256.5228, 38857.5975, 554555.7813, -4218442.3606, 5271693.5079, 43175.1083, -6358691.2001, -2321273.8442, 249707.6563, 47492.6191, -2675736.8087, 3448931.4082, -5194186.3038 ] }, "billboard": { "show": true, "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADJSURBVDhPnZHRDcMgEEMZjVEYpaNklIzSEfLfD4qNnXAJSFWfhO7w2Zc0Tf9QG2rXrEzSUeZLOGm47WoH95x3Hl3jEgilvDgsOQUTqsNl68ezEwn1vae6lceSEEYvvWNT/Rxc4CXQNGadho1NXoJ+9iaqc2xi2xbt23PJCDIB6TQjOC6Bho/sDy3fBQT8PrVhibU7yBFcEPaRxOoeTwbwByCOYf9VGp1BYI1BA+EeHhmfzKbBoJEQwn1yzUZtyspIQUha85MpkNIXB7GizqDEECsAAAAASUVORK5CYII=" }, "label": { "text": "ISS", "font": "11pt Lucida Console", "style": "FILL", "fillColor": { "rgba": [ 255, 255, 0, 255 ] }, "outlineColor": { "rgba": [ 255, 255, 0, 255 ] }, "outlineWidth": 1.0 }, "path": { "show": false, "resolution": 120, "material": { "solidColor": { "color": { "rgba": [ 255, 255, 0, 255 ] } } } } }]""" extractor = CZMLExtractor(start_epoch, end_epoch, sample_points) extractor.add_orbit( molniya, rtol=1e-4, label_text="Molniya", label_fill_color=[125, 80, 120, 255] ) extractor.add_orbit(iss, rtol=1e-4, label_text="ISS", path_show=False) assert repr(extractor.packets) == expected_doc @pytest.mark.skipif("czml3" not in sys.modules, reason="requires czml3") def test_czml_ground_station(): start_epoch = iss.epoch end_epoch = iss.epoch + molniya.period sample_points = 10 expected_doc = """[{ "id": "document", "version": "1.0", "name": "document_packet", "clock": { "interval": "2013-03-18T12:00:00.000/2013-03-18T23:59:35.108", "currentTime": "2013-03-18T12:00:00.000", "multiplier": 60, "range": "LOOP_STOP", "step": "SYSTEM_CLOCK_MULTIPLIER" } }, { "id": "custom_properties", "properties": { "custom_attractor": true, "ellipsoid": [ { "array": [ 6378137.0, 6378137.0, 6356752.314245179 ] } ], "map_url": [ "https://upload.wikimedia.org/wikipedia/commons/c/c4/Earthmap1000x500compac.jpg" ], "scene3D": true } }, { "id": "GS0", "availability": "2013-03-18T12:00:00Z/2013-03-18T23:59:35Z", "position": { "cartesian": [ 2539356.1623202674, 4775834.339416022, 3379897.6662185807 ] }, "billboard": { "show": true, "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPrZDRDcMgDAU9GqN0lIzijw6SUbJJygUeNQgSqepJTyHG91LVVpwDdfxM3T9TSl1EXZvDwii471fivK73cBFFQNTT/d2KoGpfGOpSIkhUpgUMxq9DFEsWv4IXhlyCnhBFnZcFEEuYqbiUlNwWgMTdrZ3JbQFoEVG53rd8ztG9aPJMnBUQf/VFraBJeWnLS0RfjbKyLJA8FkT5seDYS1Qwyv8t0B/5C2ZmH2/eTGNNBgMmAAAAAElFTkSuQmCC" }, "label": { "show": true, "text": "GS test", "font": "11pt Lucida Console", "style": "FILL", "fillColor": { "rgba": [ 120, 120, 120, 255 ] }, "outlineWidth": 1.0 } }, { "id": "GS1", "availability": "2013-03-18T12:00:00Z/2013-03-18T23:59:35Z", "position": { "cartesian": [ 4456924.997008477, 1886774.8000006324, 4154098.219336245 ] }, "billboard": { "show": true, "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPrZDRDcMgDAU9GqN0lIzijw6SUbJJygUeNQgSqepJTyHG91LVVpwDdfxM3T9TSl1EXZvDwii471fivK73cBFFQNTT/d2KoGpfGOpSIkhUpgUMxq9DFEsWv4IXhlyCnhBFnZcFEEuYqbiUlNwWgMTdrZ3JbQFoEVG53rd8ztG9aPJMnBUQf/VFraBJeWnLS0RfjbKyLJA8FkT5seDYS1Qwyv8t0B/5C2ZmH2/eTGNNBgMmAAAAAElFTkSuQmCC" }, "label": { "show": false, "font": "11pt Lucida Console", "style": "FILL", "outlineWidth": 1.0 } }]""" extractor = CZMLExtractor(start_epoch, end_epoch, sample_points) extractor.add_ground_station( [32 * u.degree, 62 * u.degree], label_fill_color=[120, 120, 120, 255], label_text="GS test", ) extractor.add_ground_station([0.70930 * u.rad, 0.40046 * u.rad], label_show=False) assert repr(extractor.packets) == expected_doc @pytest.mark.skipif("czml3" not in sys.modules, reason="requires czml3") def test_czml_invalid_orbit_epoch_error(): start_epoch = molniya.epoch end_epoch = molniya.epoch + molniya.period extractor = CZMLExtractor(start_epoch, end_epoch, 10) with pytest.raises(ValueError) as excinfo: extractor.add_orbit(iss, label_text="ISS", path_show=False) assert ( "ValueError: The orbit's epoch cannot exceed the constructor's ending epoch" in excinfo.exconly() ) poliastro-0.13.1/src/poliastro/tests/test_examples.py000644 001750 001750 00000000123 13576506121 023644 0ustar00juanlujuanlu000000 000000 # This line tests all the statements so far from poliastro import examples # noqa poliastro-0.13.1/src/poliastro/tests/test_frames.py000644 001750 001750 00000007515 13577127432 023324 0ustar00juanlujuanlu000000 000000 import pytest from astropy import units as u from astropy.coordinates import ( CartesianRepresentation, get_body_barycentric, solar_system_ephemeris, ) from astropy.tests.helper import assert_quantity_allclose from astropy.time import Time from poliastro.bodies import ( Earth, Jupiter, Mars, Mercury, Neptune, Pluto, Saturn, Sun, Uranus, Venus, ) from poliastro.constants import J2000 from poliastro.frames import ( GCRS, HCRS, ICRS, GeocentricSolarEcliptic, JupiterICRS, MarsICRS, MercuryICRS, NeptuneICRS, PlutoICRS, SaturnICRS, UranusICRS, VenusICRS, ) @pytest.mark.parametrize( "body, frame", [ (Mercury, MercuryICRS), (Venus, VenusICRS), (Mars, MarsICRS), (Jupiter, JupiterICRS), (Saturn, SaturnICRS), (Uranus, UranusICRS), (Neptune, NeptuneICRS), (Pluto, PlutoICRS), ], ) def test_planetary_frames_have_proper_string_representations(body, frame): coords = frame() assert body.name in repr(coords) @pytest.mark.parametrize( "body, frame", [ (Sun, HCRS), (Mercury, MercuryICRS), (Venus, VenusICRS), (Earth, GCRS), (Mars, MarsICRS), (Jupiter, JupiterICRS), (Saturn, SaturnICRS), (Uranus, UranusICRS), (Neptune, NeptuneICRS), ], ) def test_planetary_icrs_frame_is_just_translation(body, frame): with solar_system_ephemeris.set("builtin"): epoch = J2000 vector = CartesianRepresentation(x=100 * u.km, y=100 * u.km, z=100 * u.km) vector_result = ( frame(vector, obstime=epoch) .transform_to(ICRS) .represent_as(CartesianRepresentation) ) expected_result = get_body_barycentric(body.name, epoch) + vector assert_quantity_allclose(vector_result.xyz, expected_result.xyz) @pytest.mark.parametrize( "body, frame", [ (Sun, HCRS), (Mercury, MercuryICRS), (Venus, VenusICRS), (Earth, GCRS), (Mars, MarsICRS), (Jupiter, JupiterICRS), (Saturn, SaturnICRS), (Uranus, UranusICRS), (Neptune, NeptuneICRS), ], ) def test_icrs_body_position_to_planetary_frame_yields_zeros(body, frame): with solar_system_ephemeris.set("builtin"): epoch = J2000 vector = get_body_barycentric(body.name, epoch) vector_result = ( ICRS(vector) .transform_to(frame(obstime=epoch)) .represent_as(CartesianRepresentation) ) assert_quantity_allclose(vector_result.xyz, [0, 0, 0] * u.km, atol=1e-7 * u.km) def test_round_trip_from_GeocentricSolarEcliptic_gives_same_results(): gcrs = GCRS(ra="02h31m49.09s", dec="+89d15m50.8s", distance=200 * u.km) gse = gcrs.transform_to(GeocentricSolarEcliptic(obstime=Time("J2000"))) gcrs_back = gse.transform_to(GCRS(obstime=Time("J2000"))) assert_quantity_allclose(gcrs_back.dec.value, gcrs.dec.value, atol=1e-7) assert_quantity_allclose(gcrs_back.ra.value, gcrs.ra.value, atol=1e-7) def test_GeocentricSolarEcliptic_against_data(): gcrs = GCRS(ra="02h31m49.09s", dec="+89d15m50.8s", distance=200 * u.km) gse = gcrs.transform_to(GeocentricSolarEcliptic(obstime=J2000)) lon = 233.11691362602866 lat = 48.64606410986667 assert_quantity_allclose(gse.lat.value, lat, atol=1e-7) assert_quantity_allclose(gse.lon.value, lon, atol=1e-7) def test_GeocentricSolarEcliptic_raises_error_nonscalar_obstime(): with pytest.raises(ValueError) as excinfo: gcrs = GCRS(ra="02h31m49.09s", dec="+89d15m50.8s", distance=200 * u.km) gcrs.transform_to(GeocentricSolarEcliptic(obstime=Time(["J3200", "J2000"]))) assert ( "To perform this transformation the " "obstime Attribute must be a scalar." in str(excinfo.value) ) poliastro-0.13.1/src/poliastro/tests/test_hyper.py000644 001750 001750 00000000544 13576506121 023164 0ustar00juanlujuanlu000000 000000 import numpy as np import pytest from numpy.testing import assert_allclose from scipy import special from poliastro.core.hyper import hyp2f1b as hyp2f1 @pytest.mark.parametrize("x", np.linspace(0, 1, num=11)) def test_hyp2f1_battin_scalar(x): expected_res = special.hyp2f1(3, 1, 5 / 2, x) res = hyp2f1(x) assert_allclose(res, expected_res) poliastro-0.13.1/src/poliastro/tests/test_iod.py000644 001750 001750 00000010244 13576506121 022606 0ustar00juanlujuanlu000000 000000 import pytest from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from poliastro.bodies import Earth from poliastro.core import iod from poliastro.iod import izzo, vallado @pytest.mark.parametrize("lambert", [vallado.lambert, izzo.lambert]) def test_vallado75(lambert): k = Earth.k r0 = [15945.34, 0.0, 0.0] * u.km r = [12214.83399, 10249.46731, 0.0] * u.km tof = 76.0 * u.min expected_va = [2.058925, 2.915956, 0.0] * u.km / u.s expected_vb = [-3.451569, 0.910301, 0.0] * u.km / u.s va, vb = next(lambert(k, r0, r, tof)) assert_quantity_allclose(va, expected_va, rtol=1e-5) assert_quantity_allclose(vb, expected_vb, rtol=1e-4) @pytest.mark.parametrize("lambert", [vallado.lambert, izzo.lambert]) def test_curtis52(lambert): k = Earth.k r0 = [5000.0, 10000.0, 2100.0] * u.km r = [-14600.0, 2500.0, 7000.0] * u.km tof = 1.0 * u.h expected_va = [-5.9925, 1.9254, 3.2456] * u.km / u.s expected_vb = [-3.3125, -4.1966, -0.38529] * u.km / u.s va, vb = next(lambert(k, r0, r, tof)) assert_quantity_allclose(va, expected_va, rtol=1e-4) assert_quantity_allclose(vb, expected_vb, rtol=1e-4) @pytest.mark.parametrize("lambert", [vallado.lambert, izzo.lambert]) def test_curtis53(lambert): k = Earth.k r0 = [273378.0, 0.0, 0.0] * u.km r = [145820.0, 12758.0, 0.0] * u.km tof = 13.5 * u.h # ERRATA: j component is positive expected_va = [-2.4356, 0.26741, 0.0] * u.km / u.s va, vb = next(lambert(k, r0, r, tof)) assert_quantity_allclose(va, expected_va, rtol=1e-4) @pytest.mark.parametrize("lambert", [izzo.lambert]) def test_molniya_der_zero_full_revolution(lambert): k = Earth.k r0 = [22592.145603, -1599.915239, -19783.950506] * u.km r = [1922.067697, 4054.157051, -8925.727465] * u.km tof = 10 * u.h expected_va = [2.000652697, 0.387688615, -2.666947760] * u.km / u.s expected_vb = [-3.79246619, -1.77707641, 6.856814395] * u.km / u.s va, vb = next(lambert(k, r0, r, tof, M=0)) assert_quantity_allclose(va, expected_va, rtol=1e-6) assert_quantity_allclose(vb, expected_vb, rtol=1e-6) @pytest.mark.parametrize("lambert", [izzo.lambert]) def test_molniya_der_one_full_revolution(lambert): k = Earth.k r0 = [22592.145603, -1599.915239, -19783.950506] * u.km r = [1922.067697, 4054.157051, -8925.727465] * u.km tof = 10 * u.h expected_va_l = [0.50335770, 0.61869408, -1.57176904] * u.km / u.s expected_vb_l = [-4.18334626, -1.13262727, 6.13307091] * u.km / u.s expected_va_r = [-2.45759553, 1.16945801, 0.43161258] * u.km / u.s expected_vb_r = [-5.53841370, 0.01822220, 5.49641054] * u.km / u.s (va_l, vb_l), (va_r, vb_r) = lambert(k, r0, r, tof, M=1) assert_quantity_allclose(va_l, expected_va_l, rtol=1e-5) assert_quantity_allclose(vb_l, expected_vb_l, rtol=1e-5) assert_quantity_allclose(va_r, expected_va_r, rtol=1e-5) assert_quantity_allclose(vb_r, expected_vb_r, rtol=1e-4) @pytest.mark.parametrize("lambert", [izzo.lambert]) def test_raises_exception_for_non_feasible_solution(lambert): k = Earth.k r0 = [22592.145603, -1599.915239, -19783.950506] * u.km r = [1922.067697, 4054.157051, -8925.727465] * u.km tof = 5 * u.h with pytest.raises(ValueError) as excinfo: next(lambert(k, r0, r, tof, M=1)) assert "ValueError: No feasible solution, try lower M" in excinfo.exconly() @pytest.mark.parametrize("lambert", [izzo.lambert]) def test_collinear_vectors_input(lambert): k = Earth.k r0 = [22592.145603, -1599.915239, -19783.950506] * u.km r = [22592.145603, -1599.915239, -19783.950506] * u.km tof = 5 * u.h with pytest.raises(ValueError) as excinfo: next(lambert(k, r0, r, tof, M=0)) assert ( "ValueError: Lambert solution cannot be computed for collinear vectors" in excinfo.exconly() ) @pytest.mark.parametrize("M", [1, 2, 3]) def test_minimum_time_of_flight_convergence(M): ll = -1 x_T_min_expected, T_min_expected = iod._compute_T_min(ll, M, numiter=10, rtol=1e-8) y = iod._compute_y(x_T_min_expected, ll) T_min = iod._tof_equation_y(x_T_min_expected, y, 0.0, ll, M) assert T_min_expected == T_min poliastro-0.13.1/src/poliastro/tests/test_jit.py000644 001750 001750 00000002710 13577127432 022625 0ustar00juanlujuanlu000000 000000 from contextlib import contextmanager from poliastro.core import _jit @contextmanager def _fake_numba_import(): # Black magic, beware # https://stackoverflow.com/a/2484402/554319 import sys class FakeImportFailure: def __init__(self, modules): self.modules = modules def find_module(self, fullname, *args, **kwargs): if fullname in self.modules: raise ImportError("Debug import failure for %s" % fullname) fail_loader = FakeImportFailure(["numba"]) import poliastro.core._jit from poliastro.core import _jit del poliastro.core._jit del _jit del sys.modules["poliastro.core._jit"] if "numba" in sys.modules: del sys.modules["numba"] sys.meta_path.insert(0, fail_loader) yield sys.meta_path.remove(fail_loader) def test_ijit_returns_same_function_without_args(): def expected_foo(): return True foo = _jit.ijit(expected_foo) assert foo is expected_foo def test_ijit_returns_same_function_with_args(): def expected_foo(): return True foo = _jit.ijit(1)(expected_foo) assert foo is expected_foo def test_no_numba_emits_warning(recwarn): with _fake_numba_import(): from poliastro.core import _jit # noqa assert len(recwarn) == 1 w = recwarn.pop(UserWarning) assert issubclass(w.category, UserWarning) assert "Could not import numba package" in str(w.message) poliastro-0.13.1/src/poliastro/tests/test_maneuver.py000644 001750 001750 00000005562 13577127432 023671 0ustar00juanlujuanlu000000 000000 import numpy as np import pytest from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from astropy.time import Time from numpy.testing import assert_allclose from poliastro.bodies import Earth, Moon from poliastro.maneuver import Maneuver from poliastro.twobody import Orbit def test_maneuver_raises_error_if_units_are_wrong(): wrong_dt = 1.0 _v = np.zeros(3) * u.km / u.s # Unused velocity with pytest.raises(u.UnitsError) as excinfo: Maneuver([wrong_dt, _v]) assert ( "UnitsError: Argument 'dts' to function '_initialize' must be in units convertible to 's'." in excinfo.exconly() ) def test_maneuver_raises_error_if_dvs_are_not_vectors(): dt = 1 * u.s wrong_dv = 1 * u.km / u.s with pytest.raises(ValueError) as excinfo: Maneuver((dt, wrong_dv)) assert "ValueError: Delta-V must be three dimensions vectors" in excinfo.exconly() def test_maneuver_total_time(): dt1 = 10.0 * u.s dt2 = 100.0 * u.s _v = np.zeros(3) * u.km / u.s # Unused velocity expected_total_time = 110.0 * u.s man = Maneuver((dt1, _v), (dt2, _v)) assert_quantity_allclose(man.get_total_time(), expected_total_time) def test_maneuver_impulse(): dv = [1, 0, 0] * u.m / u.s man = Maneuver.impulse(dv) assert man.impulses[0] == (0 * u.s, dv) def test_hohmann_maneuver(): # Data from Vallado, example 6.1 alt_i = 191.34411 * u.km alt_f = 35781.34857 * u.km ss_i = Orbit.circular(Earth, alt_i) expected_dv = 3.935224 * u.km / u.s expected_t_trans = 5.256713 * u.h man = Maneuver.hohmann(ss_i, Earth.R + alt_f) assert_quantity_allclose(man.get_total_cost(), expected_dv, rtol=1e-5) assert_quantity_allclose(man.get_total_time(), expected_t_trans, rtol=1e-5) assert_quantity_allclose( ss_i.apply_maneuver(man).ecc, 0 * u.one, atol=1e-14 * u.one ) def test_bielliptic_maneuver(): # Data from Vallado, example 6.2 alt_i = 191.34411 * u.km alt_b = 503873.0 * u.km alt_f = 376310.0 * u.km ss_i = Orbit.circular(Earth, alt_i) expected_dv = 3.904057 * u.km / u.s expected_t_trans = 593.919803 * u.h man = Maneuver.bielliptic(ss_i, Earth.R + alt_b, Earth.R + alt_f) assert_allclose(ss_i.apply_maneuver(man).ecc, 0 * u.one, atol=1e-12 * u.one) assert_quantity_allclose(man.get_total_cost(), expected_dv, rtol=1e-5) assert_quantity_allclose(man.get_total_time(), expected_t_trans, rtol=1e-6) def test_apply_maneuver_correct_dimensions(): orb = Orbit.from_vectors( Moon, [-22681.58976181, 942.47776988, 0] * u.km, [-0.04578917, -0.19408599, 0.0] * u.km / u.s, Time("2023-08-30 23:14", scale="tdb"), ) man = Maneuver((1 * u.s, [0.01, 0, 0] * u.km / u.s)) new_orb = orb.apply_maneuver(man, intermediate=False) assert new_orb.r.ndim == 1 assert new_orb.v.ndim == 1 poliastro-0.13.1/src/poliastro/tests/test_spheroid_location.py000644 001750 001750 00000003115 13577127432 025544 0ustar00juanlujuanlu000000 000000 from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from poliastro.bodies import Earth from poliastro.spheroid_location import SpheroidLocation def test_cartesian_coordinates(): expected_cords = [ 3764258.64785411 * u.m, 3295359.33856106 * u.m, 3942945.28570563 * u.m, ] el_cords = [38.43 * u.deg, 41.2 * u.deg, 0 * u.m] p = SpheroidLocation(*el_cords, Earth) c_cords = p.cartesian_cords assert_quantity_allclose(c_cords, expected_cords) def test_tangential_vectors(): el_cords = [38.43 * u.deg, 41.2 * u.deg, 0 * u.m] p = SpheroidLocation(*el_cords, Earth) N = p.N v1, v2 = p.tangential_vecs assert abs(N.dot(v1)) <= 1e-7 assert abs(N.dot(v2)) <= 1e-7 def test_visible(): el_cords = [38.43 * u.deg, 41.2 * u.deg, 0 * u.m] p = SpheroidLocation(*el_cords, Earth) cords = p.cartesian_cords p1 = [cords[i] + 10 * p.N[i] * u.m for i in range(3)] p2 = [cords[i] - 10 * p.N[i] * u.m for i in range(3)] assert p.is_visible(*p1) assert not p.is_visible(*p2) def test_cartesian_conversion_approximate(): el_cords = [0.670680 * u.rad, 0.7190227 * u.rad, 0 * u.m] c_cords = [3764258.64785411 * u.m, 3295359.33856106 * u.m, 3942945.28570563 * u.m] p = SpheroidLocation(*el_cords, Earth) cords = p.cartesian_to_ellipsoidal(*c_cords) # TODO: Find ways to improve error margin assert_quantity_allclose(el_cords[0], cords[0], 1e-4) assert_quantity_allclose(el_cords[1], cords[1], 1e-4) assert_quantity_allclose(el_cords[2], cords[2], 1) poliastro-0.13.1/src/poliastro/tests/test_stumpff.py000644 001750 001750 00000001575 13576506121 023526 0ustar00juanlujuanlu000000 000000 from numpy import cos, cosh, sin, sinh from numpy.testing import assert_allclose, assert_equal from poliastro.core.stumpff import c2, c3 def test_stumpff_functions_near_zero(): psi = 0.5 expected_c2 = (1 - cos(psi ** 0.5)) / psi expected_c3 = (psi ** 0.5 - sin(psi ** 0.5)) / psi ** 1.5 assert_allclose(c2(psi), expected_c2) assert_allclose(c3(psi), expected_c3) def test_stumpff_functions_above_zero(): psi = 3.0 expected_c2 = (1 - cos(psi ** 0.5)) / psi expected_c3 = (psi ** 0.5 - sin(psi ** 0.5)) / psi ** 1.5 assert_equal(c2(psi), expected_c2) assert_equal(c3(psi), expected_c3) def test_stumpff_functions_under_zero(): psi = -3.0 expected_c2 = (cosh((-psi) ** 0.5) - 1) / (-psi) expected_c3 = (sinh((-psi) ** 0.5) - (-psi) ** 0.5) / (-psi) ** 1.5 assert_equal(c2(psi), expected_c2) assert_equal(c3(psi), expected_c3) poliastro-0.13.1/src/poliastro/tests/test_util.py000644 001750 001750 00000004237 13577127432 023022 0ustar00juanlujuanlu000000 000000 import pytest from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from astropy.time import Time from poliastro import util def test_simple_circular_velocity(): k = 398600 * u.km ** 3 / u.s ** 2 a = 7000 * u.km expected_V = 7.5460491 * u.km / u.s V = util.circular_velocity(k, a) assert_quantity_allclose(V, expected_V) def test_rotate_vector_with_units(): vector = [1, 0, 0] * u.m angle = 90 * u.deg axis = "y" expected_vector = [0, 0, -1] * u.m result = util.rotate(vector, angle, axis) assert_quantity_allclose(result, expected_vector, atol=1e-16 * u.m) def test_time_range_spacing_periods(): start_time = "2017-10-12 00:00:00" end_time = "2017-10-12 00:04:00" spacing = 1 * u.minute periods = 5 expected_scale = "utc" expected_duration = 4 * u.min result_1 = util.time_range(start_time, spacing=spacing, periods=periods) result_2 = util.time_range(start_time, end=end_time, periods=periods) result_3 = util.time_range(Time(start_time), end=Time(end_time), periods=periods) assert len(result_1) == len(result_2) == len(result_3) == periods assert result_1.scale == result_2.scale == result_3.scale == expected_scale assert_quantity_allclose((result_1[-1] - result_1[0]).to(u.s), expected_duration) assert_quantity_allclose((result_2[-1] - result_2[0]).to(u.s), expected_duration) assert_quantity_allclose((result_3[-1] - result_3[0]).to(u.s), expected_duration) def test_time_range_requires_keyword_arguments(): with pytest.raises(TypeError) as excinfo: util.time_range(0, 0) assert ( "TypeError: time_range() takes 1 positional argument but" in excinfo.exconly() ) def test_time_range_raises_error_wrong_arguments(): exception_message = "ValueError: Either 'end' or 'spacing' must be specified" with pytest.raises(ValueError) as excinfo_1: util.time_range("2017-10-12 00:00") with pytest.raises(ValueError) as excinfo_2: util.time_range("2017-10-12 00:00", spacing=0, end=0, periods=0) assert exception_message in excinfo_1.exconly() assert exception_message in excinfo_2.exconly() poliastro-0.13.1/src/poliastro/tests/tests_core/000755 001750 001750 00000000000 13577132207 022574 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/tests/tests_core/test_core_util.py000644 001750 001750 00000001425 13576506121 026173 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core import util def test_rotation_matrix_x(): result = util.rotation_matrix(0.218, 0) expected = np.array( [[1.0, 0.0, 0.0], [0.0, 0.97633196, -0.21627739], [0.0, 0.21627739, 0.97633196]] ) assert np.allclose(expected, result) def test_rotation_matrix_y(): result = util.rotation_matrix(0.218, 1) expected = np.array( [[0.97633196, 0.0, 0.21627739], [0.0, 1.0, 0.0], [0.21627739, 0.0, 0.97633196]] ) assert np.allclose(expected, result) def test_rotation_matrix_z(): result = util.rotation_matrix(0.218, 2) expected = np.array( [[0.97633196, -0.21627739, 0.0], [0.21627739, 0.97633196, 0.0], [0.0, 0.0, 1.0]] ) assert np.allclose(expected, result) poliastro-0.13.1/src/poliastro/tests/tests_neos/000755 001750 001750 00000000000 13577132207 022610 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/tests/tests_neos/center.html000644 001750 001750 00000002215 13576506121 024755 0ustar00juanlujuanlu000000 000000
Your search string '*alley' matched the following 6 records:
Please select from the list below.

903 Nealley (1918 EM)
2688 Halley (1982 HG1)
14182 Alley (1998 WG12)
21651 Mission Valley (1999 OF1)
<36>445 Smalley (2000 QU)
1P/Halley

poliastro-0.13.1/src/poliastro/tests/tests_neos/none.html000644 001750 001750 00000001240 13576506121 024431 0ustar00juanlujuanlu000000 000000

Error Condition

  • No Matching Record: search string = "foobaz"
  • If you've entered an object designation, please make sure you've capitalized appropriately as designations are case-sensitive. For example, use "1999 AN10" instead of "1999 an10". Names are not case-sensitive.
  • It is possible you've requested a newly discovered asteroid which hasn't been added to our database yet. For new objects, please check back later.
  • More help, tips, and tricks are available via the help link.
poliastro-0.13.1/src/poliastro/tests/tests_neos/table.html000644 001750 001750 00000002300 13576506121 024557 0ustar00juanlujuanlu000000 000000
433 Eros (1898 DQ)
Classification: Amor [ NEO ]           SPK-ID: 2000433
poliastro-0.13.1/src/poliastro/tests/tests_neos/test_dastcom5.py000644 001750 001750 00000010635 13576506121 025744 0ustar00juanlujuanlu000000 000000 import os from unittest import mock import numpy as np import pytest from poliastro.examples import iss from poliastro.neos import dastcom5 @mock.patch("poliastro.neos.dastcom5.np.fromfile") @mock.patch("poliastro.neos.dastcom5.open") def test_asteroid_db_is_called_with_right_path(mock_open, mock_np_fromfile): dastcom5.asteroid_db() mock_open.assert_called_with(dastcom5.AST_DB_PATH, "rb") @mock.patch("poliastro.neos.dastcom5.np.fromfile") @mock.patch("poliastro.neos.dastcom5.open") def test_comet_db_is_called_with_right_path(mock_open, mock_np_fromfile): dastcom5.comet_db() mock_open.assert_called_with(dastcom5.COM_DB_PATH, "rb") @mock.patch("poliastro.neos.dastcom5.orbit_from_record") @mock.patch("poliastro.neos.dastcom5.record_from_name") def test_orbit_from_name(mock_record_from_name, mock_orbit_from_record): name = "example_name" mock_orbit_from_record.return_value = iss mock_record_from_name.return_value = [1] assert dastcom5.orbit_from_name(name) == [iss] mock_record_from_name.assert_called_with(name) @mock.patch("poliastro.neos.dastcom5.orbit_from_record") @mock.patch("poliastro.neos.dastcom5.record_from_name") def test_record_from_name(mock_record_from_name, mock_orbit_from_record): name = "example_name" mock_orbit_from_record.return_value = iss mock_record_from_name.return_value = [1] assert dastcom5.orbit_from_name(name) == [iss] mock_record_from_name.assert_called_with(name) @mock.patch("poliastro.neos.dastcom5.np.fromfile") @mock.patch("poliastro.neos.dastcom5.open") def test_read_headers(mock_open, mock_np_fromfile): dastcom5.read_headers() mock_open.assert_any_call( os.path.join(dastcom5.DBS_LOCAL_PATH, "dast5_le.dat"), "rb" ) mock_open.assert_any_call( os.path.join(dastcom5.DBS_LOCAL_PATH, "dcom5_le.dat"), "rb" ) @mock.patch("poliastro.neos.dastcom5.read_headers") @mock.patch("poliastro.neos.dastcom5.np.fromfile") @mock.patch("poliastro.neos.dastcom5.open") def test_read_record(mock_open, mock_np_fromfile, mock_read_headers): mocked_ast_headers = np.array( [(3184, -1, b"00740473", b"00496815")], dtype=[ ("IBIAS1", np.int32), ("IBIAS0", np.int32), ("ENDPT2", "|S8"), ("ENDPT1", "|S8"), ], ) mocked_com_headers = np.array([(99999,)], dtype=[("IBIAS2", " 0.99: pytest.xfail() _a = 0.0 * u.rad tof = 1.0 * u.min ss0 = Orbit.from_classical( Earth, 10000 * u.km, ecc * u.one, _a, _a, _a, 1.0 * u.rad ) ss_cowell = ss0.propagate(tof, method=cowell) ss_propagator = ss0.propagate(tof, method=propagator) assert_quantity_allclose(ss_propagator.r, ss_cowell.r) assert_quantity_allclose(ss_propagator.v, ss_cowell.v) @pytest.mark.parametrize("ecc", [1.0001, 1.001, 1.01, 1.1]) @pytest.mark.parametrize("propagator", HYPERBOLIC_PROPAGATORS) def test_hyperbolic_near_parabolic(ecc, propagator): # Still not implemented. Refer to issue #714. if propagator in [pimienta, gooding]: pytest.skip() _a = 0.0 * u.rad tof = 1.0 * u.min ss0 = Orbit.from_classical( Earth, -10000 * u.km, ecc * u.one, _a, _a, _a, 1.0 * u.rad ) ss_cowell = ss0.propagate(tof, method=cowell) ss_propagator = ss0.propagate(tof, method=propagator) assert_quantity_allclose(ss_propagator.r, ss_cowell.r) assert_quantity_allclose(ss_propagator.v, ss_cowell.v) @pytest.mark.parametrize("propagator", [markley]) def test_near_equatorial(propagator): r = [8.0e3, 1.0e3, 0.0] * u.km v = [-0.5, -0.5, 0.0001] * u.km / u.s tof = 1.0 * u.h ss0 = Orbit.from_vectors(Earth, r, v) ss_cowell = ss0.propagate(tof, method=cowell) ss_propagator = ss0.propagate(tof, method=propagator) assert_quantity_allclose(ss_propagator.r, ss_cowell.r, rtol=1e-4) assert_quantity_allclose(ss_propagator.v, ss_cowell.v, rtol=1e-4) @pytest.mark.parametrize("propagator", ALL_PROPAGATORS) def test_propagation(propagator): # Data from Vallado, example 2.4 r0 = [1131.340, -2282.343, 6672.423] * u.km v0 = [-5.64305, 4.30333, 2.42879] * u.km / u.s expected_r = [-4219.7527, 4363.0292, -3958.7666] * u.km expected_v = [3.689866, -1.916735, -6.112511] * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0) tof = 40 * u.min ss1 = ss0.propagate(tof, method=propagator) r, v = ss1.rv() assert_quantity_allclose(r, expected_r, rtol=1e-5) assert_quantity_allclose(v, expected_v, rtol=1e-4) def test_propagating_to_certain_nu_is_correct(): # take an elliptic orbit a = 1.0 * u.AU ecc = 1.0 / 3.0 * u.one _a = 0.0 * u.rad nu = 10 * u.deg elliptic = Orbit.from_classical(Sun, a, ecc, _a, _a, _a, nu) elliptic_at_perihelion = elliptic.propagate_to_anomaly(0.0 * u.rad) r_per, _ = elliptic_at_perihelion.rv() elliptic_at_aphelion = elliptic.propagate_to_anomaly(np.pi * u.rad) r_ap, _ = elliptic_at_aphelion.rv() assert_quantity_allclose(norm(r_per), a * (1.0 - ecc)) assert_quantity_allclose(norm(r_ap), a * (1.0 + ecc)) # TODO: Test specific values assert elliptic_at_perihelion.epoch > elliptic.epoch assert elliptic_at_aphelion.epoch > elliptic.epoch # test 10 random true anomaly values for _ in range(10): nu = np.random.uniform(low=0.0, high=2 * np.pi) elliptic = elliptic.propagate_to_anomaly(nu * u.rad) r, _ = elliptic.rv() assert_quantity_allclose(norm(r), a * (1.0 - ecc ** 2) / (1 + ecc * np.cos(nu))) def test_propagate_accepts_timedelta(): # Data from Vallado, example 2.4 r0 = [1131.340, -2282.343, 6672.423] * u.km v0 = [-5.64305, 4.30333, 2.42879] * u.km / u.s expected_r = [-4219.7527, 4363.0292, -3958.7666] * u.km expected_v = [3.689866, -1.916735, -6.112511] * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0) tof = time.TimeDelta(40 * u.min) ss1 = ss0.propagate(tof) r, v = ss1.rv() assert_quantity_allclose(r, expected_r, rtol=1e-5) assert_quantity_allclose(v, expected_v, rtol=1e-4) def test_propagation_hyperbolic(): # Data from Curtis, example 3.5 r0 = [Earth.R.to(u.km).value + 300, 0, 0] * u.km v0 = [0, 15, 0] * u.km / u.s expected_r_norm = 163180 * u.km expected_v_norm = 10.51 * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0) tof = 14941 * u.s ss1 = ss0.propagate(tof) r, v = ss1.rv() assert_quantity_allclose(norm(r), expected_r_norm, rtol=1e-4) assert_quantity_allclose(norm(v), expected_v_norm, rtol=1e-3) @pytest.mark.parametrize("propagator", PARABOLIC_PROPAGATORS) def test_propagation_parabolic(propagator): # example from Howard Curtis (3rd edition), section 3.5, problem 3.15 # TODO: add parabolic solver in some parabolic propagators, refer to #417 if propagator in [mikkola, gooding]: pytest.skip() p = 2.0 * 6600 * u.km _a = 0.0 * u.deg orbit = Orbit.parabolic(Earth, p, _a, _a, _a, _a) orbit = orbit.propagate(0.8897 / 2.0 * u.h, method=propagator) _, _, _, _, _, nu0 = rv2coe( Earth.k.to(u.km ** 3 / u.s ** 2).value, orbit.r.to(u.km).value, orbit.v.to(u.km / u.s).value, ) assert_quantity_allclose(nu0, np.deg2rad(90.0), rtol=1e-4) orbit = Orbit.parabolic(Earth, p, _a, _a, _a, _a) orbit = orbit.propagate(36.0 * u.h, method=propagator) assert_quantity_allclose(norm(orbit.r), 304700.0 * u.km, rtol=1e-4) def test_propagation_zero_time_returns_same_state(): # Bug #50 r0 = [1131.340, -2282.343, 6672.423] * u.km v0 = [-5.64305, 4.30333, 2.42879] * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0) tof = 0 * u.s ss1 = ss0.propagate(tof) r, v = ss1.rv() assert_allclose(r.value, r0.value) assert_allclose(v.value, v0.value) def test_propagation_hyperbolic_zero_time_returns_same_state(): ss0 = Orbit.from_classical( Earth, -27112.5464 * u.km, 1.25 * u.one, 0 * u.deg, 0 * u.deg, 0 * u.deg, 0 * u.deg, ) r0, v0 = ss0.rv() tof = 0 * u.s ss1 = ss0.propagate(tof) r, v = ss1.rv() assert_allclose(r.value, r0.value) assert_allclose(v.value, v0.value) def test_apply_zero_maneuver_returns_equal_state(): _d = 1.0 * u.AU # Unused distance _ = 0.5 * u.one # Unused dimensionless value _a = 1.0 * u.deg # Unused angle ss = Orbit.from_classical(Sun, _d, _, _a, _a, _a, _a) dt = 0 * u.s dv = [0, 0, 0] * u.km / u.s orbit_new = ss.apply_maneuver([(dt, dv)]) assert_allclose(orbit_new.r.to(u.km).value, ss.r.to(u.km).value) assert_allclose(orbit_new.v.to(u.km / u.s).value, ss.v.to(u.km / u.s).value) def test_cowell_propagation_with_zero_acceleration_equals_kepler(): # Data from Vallado, example 2.4 r0 = np.array([1131.340, -2282.343, 6672.423]) * u.km v0 = np.array([-5.64305, 4.30333, 2.42879]) * u.km / u.s tofs = [40 * 60.0] * u.s orbit = Orbit.from_vectors(Earth, r0, v0) expected_r = np.array([-4219.7527, 4363.0292, -3958.7666]) * u.km expected_v = np.array([3.689866, -1.916735, -6.112511]) * u.km / u.s r, v = cowell(Earth.k, orbit.r, orbit.v, tofs, ad=None) assert_quantity_allclose(r[0], expected_r, rtol=1e-5) assert_quantity_allclose(v[0], expected_v, rtol=1e-4) def test_cowell_propagation_circle_to_circle(): # From [Edelbaum, 1961] accel = 1e-7 def constant_accel(t0, u, k): v = u[3:] norm_v = (v[0] ** 2 + v[1] ** 2 + v[2] ** 2) ** 0.5 return accel * v / norm_v ss = Orbit.circular(Earth, 500 * u.km) tofs = [20] * ss.period r, v = cowell(Earth.k, ss.r, ss.v, tofs, ad=constant_accel) ss_final = Orbit.from_vectors(Earth, r[0], v[0]) da_a0 = (ss_final.a - ss.a) / ss.a dv_v0 = abs(norm(ss_final.v) - norm(ss.v)) / norm(ss.v) assert_quantity_allclose(da_a0, 2 * dv_v0, rtol=1e-2) dv = abs(norm(ss_final.v) - norm(ss.v)) accel_dt = accel * u.km / u.s ** 2 * tofs[0] assert_quantity_allclose(dv, accel_dt, rtol=1e-2) def test_propagate_to_date_has_proper_epoch(): # Data from Vallado, example 2.4 r0 = [1131.340, -2282.343, 6672.423] * u.km v0 = [-5.64305, 4.30333, 2.42879] * u.km / u.s init_epoch = J2000 final_epoch = time.Time("2000-01-01 12:40:00", scale="tdb") expected_r = [-4219.7527, 4363.0292, -3958.7666] * u.km expected_v = [3.689866, -1.916735, -6.112511] * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0, epoch=init_epoch) ss1 = ss0.propagate(final_epoch) r, v = ss1.rv() assert_quantity_allclose(r, expected_r, rtol=1e-5) assert_quantity_allclose(v, expected_v, rtol=1e-4) # Tolerance should be higher, see https://github.com/astropy/astropy/issues/6638 assert (ss1.epoch - final_epoch).sec == approx(0.0, abs=1e-6) @pytest.mark.parametrize("propagator", [danby, markley, gooding]) def test_propagate_long_times_keeps_geometry(propagator): # See https://github.com/poliastro/poliastro/issues/265 time_of_flight = 100 * u.year res = iss.propagate(time_of_flight, method=propagator) assert_quantity_allclose(iss.a, res.a) assert_quantity_allclose(iss.ecc, res.ecc) assert_quantity_allclose(iss.inc, res.inc) assert_quantity_allclose(iss.raan, res.raan) assert_quantity_allclose(iss.argp, res.argp) assert_quantity_allclose( (res.epoch - iss.epoch).to(time_of_flight.unit), time_of_flight ) @pytest.mark.filterwarnings("ignore::astropy._erfa.core.ErfaWarning") def test_long_propagations_kepler_agrees_mean_motion(): tof = 100 * u.year r_mm, v_mm = iss.propagate(tof, method=mean_motion).rv() r_k, v_k = iss.propagate(tof, method=kepler).rv() assert_quantity_allclose(r_mm, r_k) assert_quantity_allclose(v_mm, v_k) r_halleys = [-9018878.63569932, -94116054.79839276, 22619058.69943215] # km v_halleys = [-49.95092305, -12.94843055, -4.29251577] # km/s halleys = Orbit.from_vectors(Sun, r_halleys * u.km, v_halleys * u.km / u.s) r_mm, v_mm = halleys.propagate(tof, method=mean_motion).rv() r_k, v_k = halleys.propagate(tof, method=kepler).rv() assert_quantity_allclose(r_mm, r_k) assert_quantity_allclose(v_mm, v_k) @pytest.mark.parametrize("method", [mean_motion, kepler]) def test_long_propagation_preserves_orbit_elements(method): tof = 100 * u.year r_halleys = np.array( [-9018878.63569932, -94116054.79839276, 22619058.69943215] ) # km v_halleys = np.array([-49.95092305, -12.94843055, -4.29251577]) # km/s halleys = Orbit.from_vectors(Sun, r_halleys * u.km, v_halleys * u.km / u.s) params_ini = rv2coe(Sun.k.to(u.km ** 3 / u.s ** 2).value, r_halleys, v_halleys)[:-1] r_new, v_new = halleys.propagate(tof, method=method).rv() params_final = rv2coe( Sun.k.to(u.km ** 3 / u.s ** 2).value, r_new.to(u.km).value, v_new.to(u.km / u.s).value, )[:-1] assert_quantity_allclose(params_ini, params_final) def test_propagation_sets_proper_epoch(): expected_epoch = time.Time("2017-09-01 12:05:50", scale="tdb") r = [-2.76132873e08, -1.71570015e08, -1.09377634e08] * u.km v = [13.17478674, -9.82584125, -1.48126639] * u.km / u.s florence = Orbit.from_vectors(Sun, r, v, plane=Planes.EARTH_ECLIPTIC) propagated = florence.propagate(expected_epoch) assert propagated.epoch == expected_epoch def test_sample_custom_body_raises_warning_and_returns_coords(): # See https://github.com/poliastro/poliastro/issues/649 orbit = Orbit.circular(Moon, 100 * u.km) with pytest.warns( UserWarning, match="returning only cartesian coordinates instead" ): coords = orbit.sample(10) assert isinstance(coords, CartesianRepresentation) assert len(coords) == 10 def test_propagation_custom_body_works(): # See https://github.com/poliastro/poliastro/issues/649 orbit = Orbit.circular(Moon, 100 * u.km) orbit.propagate(1 * u.h) poliastro-0.13.1/src/poliastro/tests/tests_twobody/test_states.py000644 001750 001750 00000002076 13576506121 026253 0ustar00juanlujuanlu000000 000000 from astropy import units as u from poliastro.bodies import Sun from poliastro.twobody._states import ClassicalState, RVState def test_state_has_attractor_given_in_constructor(): _d = 1.0 * u.AU # Unused distance _ = 0.5 * u.one # Unused dimensionless value _a = 1.0 * u.deg # Unused angle ss = ClassicalState(Sun, _d, _, _a, _a, _a, _a) assert ss.attractor == Sun def test_classical_state_has_elements_given_in_constructor(): # Mars data from HORIZONS at J2000 a = 1.523679 * u.AU ecc = 0.093315 * u.one inc = 1.85 * u.deg raan = 49.562 * u.deg argp = 286.537 * u.deg nu = 23.33 * u.deg ss = ClassicalState(Sun, a * (1 - ecc ** 2), ecc, inc, raan, argp, nu) assert ss.p == a * (1 - ecc ** 2) assert ss.ecc == ecc assert ss.inc == inc assert ss.raan == raan assert ss.argp == argp assert ss.nu == nu def test_rv_state_has_rv_given_in_constructor(): r = [1.0, 0.0, 0.0] * u.AU v = [0.0, 1.0e-6, 0.0] * u.AU / u.s ss = RVState(Sun, r, v) assert all(ss.r == r) assert all(ss.v == v) poliastro-0.13.1/src/poliastro/tests/tests_twobody/test_thrust.py000644 001750 001750 00000014713 13576506121 026302 0ustar00juanlujuanlu000000 000000 import numpy as np import pytest from astropy import units as u from numpy.testing import assert_allclose from poliastro.bodies import Earth from poliastro.twobody import Orbit from poliastro.twobody.propagation import cowell from poliastro.twobody.thrust import ( change_a_inc, change_argp, change_ecc_quasioptimal, change_inc_ecc, ) @pytest.mark.slow @pytest.mark.parametrize( "inc_0", [ np.radians(28.5), pytest.param( np.radians(90.0), marks=pytest.mark.skip(reason="too long for now") ), ], ) def test_leo_geo_numerical(inc_0): f = 3.5e-7 # km / s2 a_0 = 7000.0 # km a_f = 42166.0 # km inc_f = 0.0 # rad k = Earth.k.to(u.km ** 3 / u.s ** 2).value a_d, _, t_f = change_a_inc(k, a_0, a_f, inc_0, inc_f, f) # Retrieve r and v from initial orbit s0 = Orbit.circular(Earth, a_0 * u.km - Earth.R, inc_0 * u.rad) # Propagate orbit sf = s0.propagate(t_f * u.s, method=cowell, ad=a_d, rtol=1e-6) assert_allclose(sf.a.to(u.km).value, a_f, rtol=1e-3) assert_allclose(sf.ecc.value, 0.0, atol=1e-2) assert_allclose(sf.inc.to(u.rad).value, inc_f, atol=2e-3) @pytest.mark.slow @pytest.mark.parametrize( "ecc_0,ecc_f", [[0.0, 0.1245], [0.1245, 0.0]] # Reverse-engineered from results ) def test_sso_disposal_time_and_delta_v(ecc_0, ecc_f): a_0 = Earth.R.to(u.km).value + 900 # km f = 2.4e-7 # km / s2, assumed constant expected_t_f = 29.697 # days, reverse-engineered expected_delta_V = 0.6158 # km / s, lower than actual result s0 = Orbit.from_classical( Earth, a_0 * u.km, ecc_0 * u.one, 0 * u.deg, 0 * u.deg, 0 * u.deg, 0 * u.deg ) _, delta_V, t_f = change_ecc_quasioptimal(s0, ecc_f, f) assert_allclose(delta_V, expected_delta_V, rtol=1e-4) assert_allclose(t_f / 86400, expected_t_f, rtol=1e-4) @pytest.mark.slow @pytest.mark.parametrize( "ecc_0,ecc_f", [[0.0, 0.1245], [0.1245, 0.0]] # Reverse-engineered from results ) def test_sso_disposal_numerical(ecc_0, ecc_f): a_0 = Earth.R.to(u.km).value + 900 # km f = 2.4e-7 # km / s2, assumed constant # Retrieve r and v from initial orbit s0 = Orbit.from_classical( Earth, a_0 * u.km, ecc_0 * u.one, 0 * u.deg, 0 * u.deg, 0 * u.deg, 0 * u.deg ) a_d, _, t_f = change_ecc_quasioptimal(s0, ecc_f, f) # Propagate orbit sf = s0.propagate(t_f * u.s, method=cowell, ad=a_d, rtol=1e-8) assert_allclose(sf.ecc.value, ecc_f, rtol=1e-4, atol=1e-4) @pytest.mark.slow @pytest.mark.parametrize( "ecc_0,inc_f,expected_beta,expected_delta_V", [ [0.1, 20.0, 83.043, 1.6789], [0.2, 20.0, 76.087, 1.6890], [0.4, 20.0, 61.522, 1.7592], [0.6, 16.0, 40.0, 1.7241], [0.8, 10.0, 16.304, 1.9799], ], ) def test_geo_cases_beta_dnd_delta_v(ecc_0, inc_f, expected_beta, expected_delta_V): a = 42164 # km ecc_f = 0.0 inc_0 = 0.0 # rad, baseline argp = 0.0 # rad, the method is efficient for 0 and 180 f = 2.4e-7 # km / s2, unused inc_f = np.radians(inc_f) expected_beta = np.radians(expected_beta) s0 = Orbit.from_classical( Earth, a * u.km, ecc_0 * u.one, inc_0 * u.deg, 0 * u.deg, argp * u.deg, 0 * u.deg, ) _, delta_V, beta, _ = change_inc_ecc(s0, ecc_f, inc_f, f) assert_allclose(delta_V, expected_delta_V, rtol=1e-2) assert_allclose(beta, expected_beta, rtol=1e-2) @pytest.mark.slow @pytest.mark.parametrize("ecc_0,ecc_f", [[0.4, 0.0], [0.0, 0.4]]) def test_geo_cases_numerical(ecc_0, ecc_f): a = 42164 # km inc_0 = 0.0 # rad, baseline inc_f = (20.0 * u.deg).to(u.rad).value # rad argp = 0.0 # rad, the method is efficient for 0 and 180 f = 2.4e-7 # km / s2 # Retrieve r and v from initial orbit s0 = Orbit.from_classical( Earth, a * u.km, ecc_0 * u.one, inc_0 * u.deg, 0 * u.deg, argp * u.deg, 0 * u.deg, ) a_d, _, _, t_f = change_inc_ecc(s0, ecc_f, inc_f, f) # Propagate orbit sf = s0.propagate(t_f * u.s, method=cowell, ad=a_d, rtol=1e-8) assert_allclose(sf.ecc.value, ecc_f, rtol=1e-2, atol=1e-2) assert_allclose(sf.inc.to(u.rad).value, inc_f, rtol=1e-1) @pytest.mark.slow def test_soyuz_standard_gto_delta_v(): # Data from Soyuz Users Manual, issue 2 revision 0 r_a = (Earth.R + 35950 * u.km).to(u.km).value r_p = (Earth.R + 250 * u.km).to(u.km).value a = (r_a + r_p) / 2 # km ecc = r_a / a - 1 argp_0 = (178 * u.deg).to(u.rad).value # rad argp_f = (178 * u.deg + 5 * u.deg).to(u.rad).value # rad f = 2.4e-7 # km / s2 k = Earth.k.to(u.km ** 3 / u.s ** 2).value _, delta_V, t_f = change_argp(k, a, ecc, argp_0, argp_f, f) expected_t_f = 12.0 # days, approximate expected_delta_V = 0.2489 # km / s assert_allclose(delta_V, expected_delta_V, rtol=1e-2) assert_allclose(t_f / 86400, expected_t_f, rtol=1e-2) @pytest.mark.slow def test_soyuz_standard_gto_numerical(): # Data from Soyuz Users Manual, issue 2 revision 0 r_a = (Earth.R + 35950 * u.km).to(u.km).value r_p = (Earth.R + 250 * u.km).to(u.km).value a = (r_a + r_p) / 2 # km ecc = r_a / a - 1 argp_0 = (178 * u.deg).to(u.rad).value # rad argp_f = (178 * u.deg + 5 * u.deg).to(u.rad).value # rad f = 2.4e-7 # km / s2 k = Earth.k.to(u.km ** 3 / u.s ** 2).value a_d, _, t_f = change_argp(k, a, ecc, argp_0, argp_f, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical( Earth, a * u.km, (r_a / a - 1) * u.one, 6 * u.deg, 188.5 * u.deg, 178 * u.deg, 0 * u.deg, ) # Propagate orbit sf = s0.propagate(t_f * u.s, method=cowell, ad=a_d, rtol=1e-8) assert_allclose(sf.argp.to(u.rad).value, argp_f, rtol=1e-4) @pytest.mark.slow @pytest.mark.parametrize( "inc_0, expected_t_f, expected_delta_V, rtol", [ [28.5, 191.26295, 5.78378, 1e-5], [90.0, 335.0, 10.13, 1e-3], [114.591, 351.0, 10.61, 1e-2], ], ) def test_leo_geo_time_and_delta_v(inc_0, expected_t_f, expected_delta_V, rtol): f = 3.5e-7 # km / s2 a_0 = 7000.0 # km a_f = 42166.0 # km inc_f = 0.0 # rad k = Earth.k.to(u.km ** 3 / u.s ** 2).value inc_0 = np.radians(inc_0) # rad _, delta_V, t_f = change_a_inc(k, a_0, a_f, inc_0, inc_f, f) assert_allclose(delta_V, expected_delta_V, rtol=rtol) assert_allclose((t_f * u.s).to(u.day).value, expected_t_f, rtol=rtol) poliastro-0.13.1/src/poliastro/threebody/000755 001750 001750 00000000000 13577132207 021245 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/threebody/__init__.py000644 001750 001750 00000000000 13525576227 023353 0ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/threebody/flybys.py000644 001750 001750 00000004771 13525576227 023147 0ustar00juanlujuanlu000000 000000 import numpy as np from astropy import units as u from poliastro.util import norm @u.quantity_input( v_spacecraft=u.km / u.s, v_body=u.km / u.s, k=u.km ** 3 / u.s ** 2, r_p=u.km, theta=u.deg, ) def compute_flyby(v_spacecraft, v_body, k, r_p, theta=0 * u.deg): """Computes outbound velocity after a flyby. Parameters ---------- v_spacecraft : ~astropy.units.Quantity Velocity of the spacecraft, relative to the attractor of the body. v_body : ~astropy.units.Quantity Velocity of the body, relative to its attractor. k : ~astropy.units.Quantity Standard gravitational parameter of the body. r_p : ~astropy.units.Quantity Radius of periapsis, measured from the center of the body. theta : ~astropy.units.Quantity, optional Aim angle of the B vector, default to 0. Returns ------- v_spacecraft_out : ~astropy.units.Quantity Outbound velocity of the spacecraft. delta : ~astropy.units.Quantity Turn angle. """ v_inf_1 = v_spacecraft - v_body # Hyperbolic excess velocity v_inf = norm(v_inf_1) ecc = 1 + r_p * v_inf ** 2 / k # Eccentricity of the entry hyperbola delta = 2 * np.arcsin(1 / ecc) # Turn angle b = k / v_inf ** 2 * np.sqrt(ecc ** 2 - 1) # Magnitude of the B vector # Now we compute the unit vectors in which to return the outbound hyperbolic excess velocity: # * S goes along the hyperbolic excess velocity and is perpendicular to the B-Plane, # * T goes along the B-Plane and is parallel to _some_ fundamental plane - in this case, the plane in which # the velocities are computed # * R completes the orthonormal set S_vec = v_inf_1 / v_inf c_vec = np.array([0, 0, 1]) * u.one T_vec = np.cross(S_vec, c_vec) * u.one T_vec = T_vec / norm(T_vec) R_vec = np.cross(S_vec, T_vec) * u.one # This vector defines the B-Plane B_vec = b * (np.cos(theta) * T_vec + np.sin(theta) * R_vec) # We have to rotate the inbound hyperbolic excess velocity # an angle delta (turn angle) around a vector that is orthogonal to # the B-Plane and trajectory plane rot_v = np.cross(B_vec / b, S_vec) * u.one # And now we rotate the outbound hyperbolic excess velocity # u_vec = v_inf_1 / norm(v_inf) = S_vec v_vec = np.cross(rot_v, v_inf_1) * u.one v_vec = v_vec / norm(v_vec) v_inf_2 = v_inf * (np.cos(delta) * S_vec + np.sin(delta) * v_vec) v_spacecraft_out = v_inf_2 + v_body return v_spacecraft_out, delta poliastro-0.13.1/src/poliastro/threebody/restricted.py000644 001750 001750 00000007226 13525576227 024005 0ustar00juanlujuanlu000000 000000 """Circular Restricted 3-Body Problem (CR3BP) Includes the computation of Lagrange points """ import numpy as np from astropy import units as u from scipy.optimize import brentq from poliastro.util import norm @u.quantity_input(r12=u.km, m1=u.kg, m2=u.kg) def lagrange_points(r12, m1, m2): """Computes the Lagrangian points of CR3BP. Computes the Lagrangian points of CR3BP given the distance between two bodies and their masses. It uses the formulation found in Eq. (2.204) of Curtis, Howard. 'Orbital mechanics for engineering students'. Elsevier, 3rd Edition. Parameters ---------- r12 : ~astropy.units.Quantity Distance between the two bodies m1 : ~astropy.units.Quantity Mass of the main body m2 : ~astropy.units.Quantity Mass of the secondary body Returns ------- ~astropy.units.Quantity Distance of the Lagrangian points to the main body, projected on the axis main body - secondary body """ pi2 = (m2 / (m1 + m2)).value def eq_L123(xi): aux = (1 - pi2) * (xi + pi2) / abs(xi + pi2) ** 3 aux += pi2 * (xi + pi2 - 1) / abs(xi + pi2 - 1) ** 3 aux -= xi return aux lp = np.zeros((5,)) # L1 tol = 1e-11 # `brentq` uses a xtol of 2e-12, so it should be covered a = -pi2 + tol b = 1 - pi2 - tol xi = brentq(eq_L123, a, b) lp[0] = xi + pi2 # L2 xi = brentq(eq_L123, 1, 1.5) lp[1] = xi + pi2 # L3 xi = brentq(eq_L123, -1.5, -1) lp[2] = xi + pi2 # L4, L5 # L4 and L5 are points in the plane of rotation which form an equilateral # triangle with the two masses (Battin) # (0.5 = cos(60 deg)) lp[3] = lp[4] = 0.5 return lp * r12 @u.quantity_input(m1=u.kg, r1=u.km, m2=u.kg, r2=u.km, n=u.one) def lagrange_points_vec(m1, r1, m2, r2, n): """Computes the five Lagrange points in the CR3BP. Returns the positions in the same frame of reference as `r1` and `r2` for the five Lagrangian points. Parameters ---------- m1 : ~astropy.units.Quantity Mass of the main body. This body is the one with the biggest mass. r1 : ~astropy.units.Quantity Position of the main body. m2 : ~astropy.units.Quantity Mass of the secondary body. r2 : ~astropy.units.Quantity Position of the secondary body. n : ~astropy.units.Quantity Normal vector to the orbital plane. Returns ------- list: Position of the Lagrange points: [L1, L2, L3, L4, L5] The positions are of type ~astropy.units.Quantity """ # Check Body 1 is the main body assert ( m1 > m2 ), "Body 1 is not the main body: it has less mass than the 'secondary' body" # Define local frame of reference: # Center: main body, NOT the barycenter # x-axis: points to the secondary body ux = r2 - r1 r12 = norm(ux) ux = ux / r12 # y-axis: contained in the orbital plane, perpendicular to x-axis def cross(x, y): return np.cross(x, y) * x.unit * y.unit uy = cross(n, ux) uy = uy / norm(uy) # position in x-axis x1, x2, x3, x4, x5 = lagrange_points(r12, m1, m2) # position in y-axis # L1, L2, L3 are located in the x-axis, so y123 = 0 # L4 and L5 are points in the plane of rotation which form an equilateral # triangle with the two masses (Battin) # sqrt(3)/2 = sin(60 deg) y4 = np.sqrt(3) / 2 * r12 y5 = -y4 # Convert L points coordinates (x,y) to original vectorial base [r1 r2] L1 = r1 + ux * x1 L2 = r1 + ux * x2 L3 = r1 + ux * x3 L4 = r1 + ux * x4 + uy * y4 L5 = r1 + ux * x5 + uy * y5 return [L1, L2, L3, L4, L5] poliastro-0.13.1/src/poliastro/threebody/soi.py000644 001750 001750 00000007375 13525576227 022434 0ustar00juanlujuanlu000000 000000 """Sphere of Influence Contains methods to compute radius of sphere of influence. Laplace Radius: A laplace sphere of influence (SOI) in astrodynamics and astronomy is the oblate-spheroid-shaped region around a celestial body where the primary gravitational influence on an orbiting object is that body. This is usually used to describe the areas in the Solar System where planets dominate the orbits of surrounding objects such as moons, despite the presence of the much more massive but distant Sun. In the patched conic approximation, used in estimating the trajectories of bodies moving between the neighbourhoods of different masses using a two body approximation, ellipses and hyperbolae, the laplace radius is taken as the boundary where the trajectory switches which mass field it is influenced by. The result is: .. math:: a\\left(\\frac{m}{M}\\right)^{\\frac{2}{5}} Hill Radius: In the three body problem, if that third object stays within an extremely complex boundary called the Roche lobe, the orbit of that third object about the smaller body will be stable for at least some amount of time. The Roche lobe just touches the L1 and L2 points and fans out from there. George Hill used the L1 point to define a sphere that approximated the Roche lobe. This is still intractable; the L1 point is defined by a fifth order polynomial that cannot be solved in terms of the elementary functions. Hill further simplified things by realizing that a simple cubic equation yields a very good approximation of that intractable fifth order equation. The result is: .. math:: a\\left(\\frac{m}{3M}\\right)^{\\frac{1}{3}} """ from astropy import units as u from poliastro.constants import J2000_TDB @u.quantity_input(a=u.m) def laplace_radius(body, a=None): """Approximated radius of the Laplace Sphere of Influence (SOI) for a body. Parameters ---------- body : `~poliastro.bodies.Body` Astronomical body which the SOI's radius is computed for. a : float, optional Semimajor axis of the body's orbit, default to None (will be computed from ephemerides). Returns ------- astropy.units.quantity.Quantity Approximated radius of the Sphere of Influence (SOI) [m] """ # Compute semimajor axis at epoch J2000 for the body if it was not # introduced by the user if a is None: # https://github.com/poliastro/poliastro/pull/679#issuecomment-503902597 from poliastro.twobody.orbit import Orbit a = Orbit.from_body_ephem(body, J2000_TDB).a r_SOI = a * (body.k / body.parent.k) ** (2 / 5) return r_SOI.decompose() @u.quantity_input(a=u.m, e=u.one) def hill_radius(body, a=None, e=None): """Approximated radius of the Hill Sphere of Influence (SOI) for a body. Parameters ---------- body : `~poliastro.bodies.Body` Astronomical body which the SOI's radius is computed for. a : float, optional Semimajor axis of the body's orbit, default to None (will be computed from ephemerides). e : float, optional Eccentricity of the body's orbit, default to 0 (will be computed from ephemerides). Returns ------- astropy.units.quantity.Quantity Approximated radius of the Sphere of Influence (SOI) [m] """ # Compute semimajor and eccentricity axis at epoch J2000 for the body if it was not # introduced by the user if a is None or e is None: # https://github.com/poliastro/poliastro/pull/679#issuecomment-503902597 from poliastro.twobody.orbit import Orbit ss = Orbit.from_body_ephem(body, J2000_TDB) a = a if a is not None else ss.a e = e if e is not None else ss.ecc mass_ratio = body.k / (3 * body.parent.k) r_SOI = a * (1 - e) * (mass_ratio ** (1 / 3)) return r_SOI.decompose() poliastro-0.13.1/src/poliastro/twobody/000755 001750 001750 00000000000 13577132207 020747 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/twobody/__init__.py000644 001750 001750 00000000056 13525576227 023070 0ustar00juanlujuanlu000000 000000 from .orbit import Orbit __all__ = ["Orbit"] poliastro-0.13.1/src/poliastro/twobody/_states.py000644 001750 001750 00000012337 13525576227 023000 0ustar00juanlujuanlu000000 000000 from astropy import units as u from poliastro.core.elements import coe2mee, coe2rv, mee2coe, rv2coe class BaseState(object): """Base State class, meant to be subclassed. """ def __init__(self, attractor): """Constructor. Parameters ---------- attractor : Body Main attractor. """ self._attractor = attractor @property def attractor(self): """Main attractor. """ return self._attractor def to_vectors(self): """Converts to position and velocity vector representation. Returns ------- RVState """ raise NotImplementedError def to_classical(self): """Converts to classical orbital elements representation. Returns ------- ClassicalState """ raise NotImplementedError def to_equinoctial(self): """Converts to modified equinoctial elements representation. Returns ------- ModifiedEquinoctialState """ raise NotImplementedError class ClassicalState(BaseState): """State defined by its classical orbital elements. """ def __init__(self, attractor, p, ecc, inc, raan, argp, nu): super().__init__(attractor) self._p = p self._ecc = ecc self._inc = inc self._raan = raan self._argp = argp self._nu = nu @property def p(self): """Semilatus rectum. """ return self._p @property def ecc(self): """Eccentricity. """ return self._ecc @property def inc(self): """Inclination. """ return self._inc @property def raan(self): """Right ascension of the ascending node. """ return self._raan @property def argp(self): """Argument of the perigee. """ return self._argp @property def nu(self): """True anomaly. """ return self._nu def to_vectors(self): """Converts to position and velocity vector representation. """ r, v = coe2rv( self.attractor.k.to(u.km ** 3 / u.s ** 2).value, self.p.to(u.km).value, self.ecc.value, self.inc.to(u.rad).value, self.raan.to(u.rad).value, self.argp.to(u.rad).value, self.nu.to(u.rad).value, ) return RVState(self.attractor, r * u.km, v * u.km / u.s) def to_classical(self): """Converts to classical orbital elements representation. """ return self def to_equinoctial(self): """Converts to modified equinoctial elements representation. """ p, f, g, h, k, L = coe2mee( self.p.to(u.km).value, self.ecc.value, self.inc.to(u.rad).value, self.raan.to(u.rad).value, self.argp.to(u.rad).value, self.nu.to(u.rad).value, ) return ModifiedEquinoctialState( self.attractor, p * u.km, f * u.rad, g * u.rad, h * u.rad, k * u.rad, L * u.rad, ) class RVState(BaseState): """State defined by its position and velocity vectors. """ def __init__(self, attractor, r, v): super().__init__(attractor) self._r = r self._v = v @property def r(self): """Position vector. """ return self._r @property def v(self): """Velocity vector. """ return self._v def to_vectors(self): """Converts to position and velocity vector representation. """ return self def to_classical(self): """Converts to classical orbital elements representation. """ (p, ecc, inc, raan, argp, nu) = rv2coe( self.attractor.k.to(u.km ** 3 / u.s ** 2).value, self.r.to(u.km).value, self.v.to(u.km / u.s).value, ) return ClassicalState( self.attractor, p * u.km, ecc * u.one, inc * u.rad, raan * u.rad, argp * u.rad, nu * u.rad, ) class ModifiedEquinoctialState(BaseState): def __init__(self, attractor, p, f, g, h, k, L): super().__init__(attractor) self._p = p self._f = f self._g = g self._h = h self._k = k self._L = L @property def p(self): return self._p @property def f(self): return self._f @property def g(self): return self._g @property def h(self): return self._h @property def k(self): return self._k @property def L(self): return self._L def to_classical(self): p, ecc, inc, raan, argp, nu = mee2coe( self.p.to(u.km).value, self.f.to(u.rad).value, self.g.to(u.rad).value, self.h.to(u.rad).value, self.k.to(u.rad).value, self.L.to(u.rad).value, ) return ClassicalState( self.attractor, p * u.km, ecc * u.one, inc * u.rad, raan * u.rad, argp * u.rad, nu * u.rad, ) poliastro-0.13.1/src/poliastro/twobody/angles.py000644 001750 001750 00000017267 13577127432 022613 0ustar00juanlujuanlu000000 000000 """Angles and anomalies. """ from astropy import units as u from poliastro.core.angles import ( D_to_M as D_to_M_fast, D_to_nu as D_to_nu_fast, E_to_M as E_to_M_fast, E_to_nu as E_to_nu_fast, F_to_M as F_to_M_fast, F_to_nu as F_to_nu_fast, M_to_D as M_to_D_fast, M_to_E as M_to_E_fast, M_to_F as M_to_F_fast, M_to_nu as M_to_nu_fast, fp_angle as fp_angle_fast, nu_to_D as nu_to_D_fast, nu_to_E as nu_to_E_fast, nu_to_F as nu_to_F_fast, nu_to_M as nu_to_M_fast, ) @u.quantity_input(D=u.rad) def D_to_nu(D): """True anomaly from parabolic eccentric anomaly. Parameters ---------- D : ~astropy.units.Quantity Eccentric anomaly. Returns ------- nu : ~astropy.units.Quantity True anomaly. Notes ----- Taken from Farnocchia, Davide, Davide Bracali Cioci, and Andrea Milani. "Robust resolution of Kepler’s equation in all eccentricity regimes." Celestial Mechanics and Dynamical Astronomy 116, no. 1 (2013): 21-34. """ return (D_to_nu_fast(D.to(u.rad).value) * u.rad).to(D.unit) @u.quantity_input(nu=u.rad) def nu_to_D(nu): """Parabolic eccentric anomaly from true anomaly. Parameters ---------- nu : ~astropy.units.Quantity True anomaly. Returns ------- D : ~astropy.units.Quantity Hyperbolic eccentric anomaly. Notes ----- Taken from Farnocchia, Davide, Davide Bracali Cioci, and Andrea Milani. "Robust resolution of Kepler’s equation in all eccentricity regimes." Celestial Mechanics and Dynamical Astronomy 116, no. 1 (2013): 21-34. """ return (nu_to_D_fast(nu.to(u.rad).value) * u.rad).to(nu.unit) @u.quantity_input(nu=u.rad, ecc=u.one) def nu_to_E(nu, ecc): """Eccentric anomaly from true anomaly. .. versionadded:: 0.4.0 Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- E : ~astropy.units.Quantity Eccentric anomaly. """ return (nu_to_E_fast(nu.to(u.rad).value, ecc.value) * u.rad).to(nu.unit) @u.quantity_input(nu=u.rad, ecc=u.one) def nu_to_F(nu, ecc): """Hyperbolic eccentric anomaly from true anomaly. Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. Note ----- Taken from Curtis, H. (2013). *Orbital mechanics for engineering students*. 167 """ return (nu_to_F_fast(nu.to(u.rad).value, ecc.value) * u.rad).to(nu.unit) @u.quantity_input(E=u.rad, ecc=u.one) def E_to_nu(E, ecc): """True anomaly from eccentric anomaly. .. versionadded:: 0.4.0 Parameters ---------- E : ~astropy.units.Quantity Eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- nu : ~astropy.units.Quantity True anomaly. """ return (E_to_nu_fast(E.to(u.rad).value, ecc.value) * u.rad).to(E.unit) @u.quantity_input(F=u.rad, ecc=u.one) def F_to_nu(F, ecc): """True anomaly from hyperbolic eccentric anomaly. Parameters ---------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- nu : ~astropy.units.Quantity True anomaly. """ return (F_to_nu_fast(F.to(u.rad).value, ecc.value) * u.rad).to(F.unit) @u.quantity_input(M=u.rad, ecc=u.one) def M_to_E(M, ecc): """Eccentric anomaly from mean anomaly. .. versionadded:: 0.4.0 Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- E : ~astropy.units.Quantity Eccentric anomaly. """ return (M_to_E_fast(M.to(u.rad).value, ecc.value) * u.rad).to(M.unit) @u.quantity_input(M=u.rad, ecc=u.one) def M_to_F(M, ecc): """Hyperbolic eccentric anomaly from mean anomaly. Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. """ return (M_to_F_fast(M.to(u.rad).value, ecc.value) * u.rad).to(M.unit) @u.quantity_input(M=u.rad, ecc=u.one) def M_to_D(M, ecc): """Parabolic eccentric anomaly from mean anomaly. Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- D : ~astropy.units.Quantity Parabolic eccentric anomaly. """ return (M_to_D_fast(M.to(u.rad).value, ecc.value) * u.rad).to(M.unit) @u.quantity_input(E=u.rad, ecc=u.one) def E_to_M(E, ecc): """Mean anomaly from eccentric anomaly. .. versionadded:: 0.4.0 Parameters ---------- E : ~astropy.units.Quantity Eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ return (E_to_M_fast(E.to(u.rad).value, ecc.value) * u.rad).to(E.unit) @u.quantity_input(F=u.rad, ecc=u.one) def F_to_M(F, ecc): """Mean anomaly from eccentric anomaly. Parameters ---------- F : ~astropy.units.Quantity Hyperbolic eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity (>1). Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ return (F_to_M_fast(F.to(u.rad).value, ecc.value) * u.rad).to(F.unit) @u.quantity_input(D=u.rad, ecc=u.one) def D_to_M(D, ecc): """Mean anomaly from eccentric anomaly. Parameters ---------- D : ~astropy.units.Quantity Parabolic eccentric anomaly. ecc : ~astropy.units.Quantity Eccentricity. Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ return (D_to_M_fast(D.to(u.rad).value, ecc.value) * u.rad).to(D.unit) @u.quantity_input(M=u.rad, ecc=u.one) def M_to_nu(M, ecc, delta=1e-2): """True anomaly from mean anomaly. .. versionadded:: 0.4.0 Parameters ---------- M : ~astropy.units.Quantity Mean anomaly. ecc : ~astropy.units.Quantity Eccentricity. delta : float (optional) threshold of near-parabolic regime definition (from Davide Farnocchia et al) Returns ------- nu : ~astropy.units.Quantity True anomaly. Examples -------- >>> M_to_nu(30.0 * u.deg, 0.06 * u.one) """ return (M_to_nu_fast(M.to(u.rad).value, ecc.value, delta) * u.rad).to(M.unit) @u.quantity_input(nu=u.rad, ecc=u.one) def nu_to_M(nu, ecc, delta=1e-2): """Mean anomaly from true anomaly. .. versionadded:: 0.4.0 Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity. delta : float (optional) threshold of near-parabolic regime definition (from Davide Farnocchia et al) Returns ------- M : ~astropy.units.Quantity Mean anomaly. """ return (nu_to_M_fast(nu.to(u.rad).value, ecc.value, delta) * u.rad).to(nu.unit) @u.quantity_input(nu=u.rad, ecc=u.one) def fp_angle(nu, ecc): """Flight path angle. .. versionadded:: 0.4.0 Parameters ---------- nu : ~astropy.units.Quantity True anomaly. ecc : ~astropy.units.Quantity Eccentricity. Note ----- Algorithm taken from Vallado 2007, pp. 113. """ return (fp_angle_fast(nu.to(u.rad).value, ecc.value) * u.rad).to(nu.unit) poliastro-0.13.1/src/poliastro/twobody/decorators.py000644 001750 001750 00000001625 13525576227 023501 0ustar00juanlujuanlu000000 000000 """Decorators. """ from functools import wraps from astropy import units as u from poliastro.bodies import Body from ._states import RVState u.kms = u.km / u.s u.km3s2 = u.km ** 3 / u.s ** 2 def state_from_vector(func): """Changes signature to receive Orbit instead of state array. Examples -------- >>> from poliastro.twobody.decorators import state_from_vector >>> @state_from_vector ... def func(_, ss): ... return ss.r, ss.v ... >>> func(0.0, [1, 2, 3, -1, -2, -3], 1.0) (, ) Notes ----- Functions decorated with this will have poor performance. """ @wraps(func) def wrapper(t, u_, k, *args, **kwargs): r, v = u_[:3], u_[3:] ss = RVState(Body(None, k * u.km3s2, "_Dummy"), r * u.km, v * u.kms) return func(t, ss, *args, **kwargs) return wrapper poliastro-0.13.1/src/poliastro/twobody/orbit.py000644 001750 001750 00000127033 13577127436 022456 0ustar00juanlujuanlu000000 000000 from typing import List, Union from warnings import warn import numpy as np from astropy import time, units as u from astropy.coordinates import ( GCRS, ICRS, Angle, CartesianDifferential, CartesianRepresentation, get_body_barycentric, get_body_barycentric_posvel, ) from astroquery.jplhorizons import Horizons from astroquery.jplsbdb import SBDB from poliastro.bodies import Earth, Moon, Sun from poliastro.constants import J2000 from poliastro.core.angles import nu_to_M as nu_to_M_fast from poliastro.core.elements import rv2coe from poliastro.frames import Planes, get_frame from poliastro.threebody.soi import laplace_radius from poliastro.twobody.angles import E_to_nu, M_to_nu, nu_to_M from poliastro.twobody.propagation import mean_motion, propagate from poliastro.util import ( find_closest_value, get_eccentricity_critical_argp, get_eccentricity_critical_inc, get_inclination_critical_argp, hyp_nu_limit, norm, ) from ._states import BaseState, ClassicalState, ModifiedEquinoctialState, RVState ORBIT_FORMAT = "{r_p:.0f} x {r_a:.0f} x {inc:.1f} ({frame}) orbit around {body} at epoch {epoch} ({scale})" # String representation for orbits around bodies without predefined # reference frame ORBIT_NO_FRAME_FORMAT = ( "{r_p:.0f} x {r_a:.0f} x {inc:.1f} orbit around {body} at epoch {epoch} ({scale})" ) class TimeScaleWarning(UserWarning): pass class OrbitSamplingWarning(UserWarning): pass class PatchedConicsWarning(UserWarning): pass class Orbit(object): """Position and velocity of a body with respect to an attractor at a given time (epoch). Regardless of how the Orbit is created, the implicit reference system is an inertial one. For the specific case of the Solar System, this can be assumed to be the International Celestial Reference System or ICRS. """ def __init__(self, state, epoch, plane): """Constructor. Parameters ---------- state : BaseState Position and velocity or orbital elements. epoch : ~astropy.time.Time Epoch of the orbit. """ self._state = state # type: BaseState self._epoch = epoch # type: time.Time self._plane = plane self._frame = None @property def attractor(self): """Main attractor. """ return self._state.attractor @property def epoch(self): """Epoch of the orbit. """ return self._epoch @property def plane(self): """Fundamental plane of the frame. """ return self._plane @property def frame(self): """Reference frame of the orbit. .. versionadded:: 0.11.0 """ if self._frame is None: self._frame = get_frame(self.attractor, self._plane, self.epoch) return self._frame @property def r(self): """Position vector. """ return self._state.to_vectors().r @property def v(self): """Velocity vector. """ return self._state.to_vectors().v @property def a(self): """Semimajor axis. """ return self.p / (1 - self.ecc ** 2) @property def p(self): """Semilatus rectum. """ return self._state.to_classical().p @property def r_p(self): """Radius of pericenter. """ return self.a * (1 - self.ecc) @property def r_a(self): """Radius of apocenter. """ return self.a * (1 + self.ecc) @property def ecc(self): """Eccentricity. """ return self._state.to_classical().ecc @property def inc(self): """Inclination. """ return self._state.to_classical().inc @property def raan(self): """Right ascension of the ascending node. """ return self._state.to_classical().raan @property def argp(self): """Argument of the perigee. """ return self._state.to_classical().argp @property def nu(self): """True anomaly. """ return self._state.to_classical().nu @property def f(self): """Second modified equinoctial element. """ return self._state.to_equinoctial().f @property def g(self): """Third modified equinoctial element. """ return self._state.to_equinoctial().g @property def h(self): """Fourth modified equinoctial element. """ return self._state.to_equinoctial().h @property def k(self): """Fifth modified equinoctial element. """ return self._state.to_equinoctial().k @property def L(self): """True longitude. """ return self.raan + self.argp + self.nu @property def period(self): """Period of the orbit. """ return 2 * np.pi * u.rad / self.n @property def n(self): """Mean motion. """ return (np.sqrt(self.attractor.k / abs(self.a ** 3)) * u.rad).decompose() @property def energy(self): """Specific energy. """ return self.v.dot(self.v) / 2 - self.attractor.k / np.sqrt(self.r.dot(self.r)) @property def e_vec(self): """Eccentricity vector. """ r, v = self.rv() k = self.attractor.k e_vec = ((v.dot(v) - k / (norm(r))) * r - r.dot(v) * v) / k return e_vec.decompose() @property def h_vec(self): """Specific angular momentum vector. """ h_vec = np.cross(self.r.to(u.km).value, self.v.to(u.km / u.s)) * u.km ** 2 / u.s return h_vec @property def h_mag(self): """Specific angular momentum. """ h_mag = norm(self.h_vec) return h_mag @property def arglat(self): """Argument of latitude. """ arglat = (self.argp + self.nu) % (360 * u.deg) return arglat @property def M(self): """Mean anomaly. """ return nu_to_M(self.nu, self.ecc) @property def t_p(self): """Elapsed time since latest perifocal passage. """ t_p = self.period * self.M / (360 * u.deg) return t_p @classmethod @u.quantity_input(r=u.m, v=u.m / u.s) def from_vectors(cls, attractor, r, v, epoch=J2000, plane=Planes.EARTH_EQUATOR): """Return `Orbit` from position and velocity vectors. Parameters ---------- attractor : Body Main attractor. r : ~astropy.units.Quantity Position vector wrt attractor center. v : ~astropy.units.Quantity Velocity vector. epoch : ~astropy.time.Time, optional Epoch, default to J2000. plane : ~poliastro.frames.Planes Fundamental plane of the frame. """ assert np.any(r.value), "Position vector must be non zero" if r.ndim > 1 or v.ndim > 1: raise ValueError( f"Vectors must have dimension 1, got {r.ndim} and {v.ndim}" ) ss = RVState(attractor, r, v) return cls(ss, epoch, plane) @classmethod def from_coords(cls, attractor, coord, plane=Planes.EARTH_EQUATOR): """Creates an `Orbit` from an attractor and astropy `SkyCoord` or `BaseCoordinateFrame` instance. This method accepts position and velocity in any reference frame unlike `Orbit.from_vector` which can accept inputs in only inertial reference frame centred at attractor. Also note that the frame information is lost after creation of the orbit and only the inertial reference frame at body centre will be used for all purposes. Parameters ---------- attractor: Body Main attractor coord: astropy.coordinates.SkyCoord or BaseCoordinateFrame Position and velocity vectors in any reference frame. Note that coord must have a representation and its differential with respect to time. plane : ~poliastro.frames.Planes, optional Final orbit plane, default to Earth Equator. """ if "s" not in coord.cartesian.differentials: raise ValueError( "Coordinate instance doesn't have a differential with respect to time" ) if coord.size != 1: raise ValueError( "Coordinate instance must represents exactly 1 position, found: %d" % coord.size ) # Reshape coordinate to 0 dimension if it is not already dimensionless. coord = coord.reshape(()) # Get an inertial reference frame parallel to ICRS and centered at # attractor inertial_frame_at_body_centre = get_frame(attractor, plane, coord.obstime) if not coord.is_equivalent_frame(inertial_frame_at_body_centre): coord_in_irf = coord.transform_to(inertial_frame_at_body_centre) else: coord_in_irf = coord pos = coord_in_irf.cartesian.xyz vel = coord_in_irf.cartesian.differentials["s"].d_xyz return cls.from_vectors(attractor, pos, vel, epoch=coord.obstime, plane=plane) @classmethod @u.quantity_input(a=u.m, ecc=u.one, inc=u.rad, raan=u.rad, argp=u.rad, nu=u.rad) def from_classical( cls, attractor, a, ecc, inc, raan, argp, nu, epoch=J2000, plane=Planes.EARTH_EQUATOR, ): """Return `Orbit` from classical orbital elements. Parameters ---------- attractor : Body Main attractor. a : ~astropy.units.Quantity Semi-major axis. ecc : ~astropy.units.Quantity Eccentricity. inc : ~astropy.units.Quantity Inclination raan : ~astropy.units.Quantity Right ascension of the ascending node. argp : ~astropy.units.Quantity Argument of the pericenter. nu : ~astropy.units.Quantity True anomaly. epoch : ~astropy.time.Time, optional Epoch, default to J2000. plane : ~poliastro.frames.Planes Fundamental plane of the frame. """ if ecc == 1.0 * u.one: raise ValueError("For parabolic orbits use Orbit.parabolic instead") if not 0 * u.deg <= inc <= 180 * u.deg: raise ValueError("Inclination must be between 0 and 180 degrees") if ecc > 1 and a > 0: raise ValueError("Hyperbolic orbits have negative semimajor axis") ss = ClassicalState(attractor, a * (1 - ecc ** 2), ecc, inc, raan, argp, nu) return cls(ss, epoch, plane) @classmethod @u.quantity_input(p=u.m, f=u.one, g=u.rad, h=u.rad, k=u.rad, L=u.rad) def from_equinoctial( cls, attractor, p, f, g, h, k, L, epoch=J2000, plane=Planes.EARTH_EQUATOR ): """Return `Orbit` from modified equinoctial elements. Parameters ---------- attractor : Body Main attractor. p : ~astropy.units.Quantity Semilatus rectum. f : ~astropy.units.Quantity Second modified equinoctial element. g : ~astropy.units.Quantity Third modified equinoctial element. h : ~astropy.units.Quantity Fourth modified equinoctial element. k : ~astropy.units.Quantity Fifth modified equinoctial element. L : ~astropy.units.Quantity True longitude. epoch : ~astropy.time.Time, optional Epoch, default to J2000. plane : ~poliastro.frames.Planes Fundamental plane of the frame. """ ss = ModifiedEquinoctialState(attractor, p, f, g, h, k, L) return cls(ss, epoch, plane) @classmethod def from_body_ephem(cls, body, epoch=None): """Return osculating `Orbit` of a body at a given time. """ # TODO: https://github.com/poliastro/poliastro/issues/445 if not epoch: epoch = time.Time.now().tdb elif epoch.scale != "tdb": epoch = epoch.tdb warn( "Input time was converted to scale='tdb' with value " f"{epoch.tdb.value}. Use Time(..., scale='tdb') instead.", TimeScaleWarning, ) try: r, v = get_body_barycentric_posvel(body.name, epoch) except KeyError: raise RuntimeError( """To compute the position and velocity of the Moon and Pluto use the JPL ephemeris: >>> from astropy.coordinates import solar_system_ephemeris >>> solar_system_ephemeris.set('jpl') """ ) if body == Moon: # TODO: The attractor is in fact the Earth-Moon Barycenter icrs_cart = r.with_differentials(v.represent_as(CartesianDifferential)) gcrs_cart = ( ICRS(icrs_cart) .transform_to(GCRS(obstime=epoch)) .represent_as(CartesianRepresentation) ) ss = cls.from_vectors( Earth, gcrs_cart.xyz.to(u.km), gcrs_cart.differentials["s"].d_xyz.to(u.km / u.day), epoch, ) else: # TODO: The attractor is not really the Sun, but the Solar System # Barycenter ss = cls.from_vectors(Sun, r.xyz.to(u.km), v.xyz.to(u.km / u.day), epoch) ss._frame = ICRS() # Hack! return ss def change_attractor(self, new_attractor, force=False): """Changes orbit attractor. Only changes from attractor to parent or the other way around are allowed. Parameters ---------- new_attractor: poliastro.bodies.Body Desired new attractor. force: boolean If `True`, changes attractor even if physically has no-sense. Returns ------- ss: poliastro.twobody.orbit.Orbit Orbit with new attractor """ if self.attractor == new_attractor: return self elif self.attractor == new_attractor.parent: # "Sun -> Earth" r_soi = laplace_radius(new_attractor) distance = norm( self.r - get_body_barycentric(new_attractor.name, self.epoch).xyz ) elif self.attractor.parent == new_attractor: # "Earth -> Sun" r_soi = laplace_radius(self.attractor) distance = norm(self.r) else: raise ValueError("Cannot change to unrelated attractor") if distance > r_soi and not force: raise ValueError( "Orbit is out of new attractor's SOI. If required, use 'force=True'." ) elif self.ecc < 1.0 and not force: raise ValueError("Orbit will never leave the SOI of its current attractor") else: warn("Leaving the SOI of the current attractor", PatchedConicsWarning) new_frame = get_frame(new_attractor, self.plane, obstime=self.epoch) coords = self.frame.realize_frame(self.represent_as(CartesianRepresentation)) ss = Orbit.from_coords(new_attractor, coords.transform_to(new_frame)) return ss @classmethod def from_horizons( cls, name, epoch=None, plane=Planes.EARTH_EQUATOR, id_type="smallbody" ): """Return osculating `Orbit` of a body using JPLHorizons module of Astroquery. Parameters ---------- name : string Name of the body to query for. epoch : ~astropy.time.Time, optional Epoch, default to None. plane : ~poliastro.frames.Planes Fundamental plane of the frame. id_type : string, optional Use "smallbody" for Asteroids and Comets, and "majorbody" for Planets and Satellites. """ if not epoch: epoch = time.Time.now() if plane == Planes.EARTH_EQUATOR: refplane = "earth" elif plane == Planes.EARTH_ECLIPTIC: refplane = "ecliptic" obj = Horizons(id=name, epochs=epoch.jd, id_type=id_type).elements( refplane=refplane ) a = obj["a"][0] * u.au ecc = obj["e"][0] * u.one inc = obj["incl"][0] * u.deg raan = obj["Omega"][0] * u.deg argp = obj["w"][0] * u.deg nu = obj["nu"][0] * u.deg ss = cls.from_classical( Sun, a, ecc, inc, raan, argp, nu, epoch=epoch.tdb, plane=plane ) return ss @classmethod def from_sbdb(cls, name, **kargs): """Return osculating `Orbit` by using `SBDB` from Astroquery. Parameters ---------- body_name: string Name of the body to make the request. Returns ------- ss: poliastro.twobody.orbit.Orbit Orbit corresponding to body_name Examples -------- >>> from poliastro.twobody.orbit import Orbit >>> apophis_orbit = Orbit.from_sbdb('apophis') # doctest: +REMOTE_DATA """ obj = SBDB.query(name, full_precision=True, **kargs) if "count" in obj: # no error till now ---> more than one object has been found # contains all the name of the objects objects_name = obj["list"]["name"] objects_name_in_str = ( "" # used to store them in string form each in new line ) for i in objects_name: objects_name_in_str += i + "\n" raise ValueError( str(obj["count"]) + " different objects found: \n" + objects_name_in_str ) a = obj["orbit"]["elements"]["a"].to(u.AU) * u.AU ecc = float(obj["orbit"]["elements"]["e"]) * u.one inc = obj["orbit"]["elements"]["i"].to(u.deg) * u.deg raan = obj["orbit"]["elements"]["om"].to(u.deg) * u.deg argp = obj["orbit"]["elements"]["w"].to(u.deg) * u.deg # Since JPL provides Mean Anomaly (M) we need to make # the conversion to the true anomaly (\nu) nu = M_to_nu(obj["orbit"]["elements"]["ma"].to(u.deg) * u.deg, ecc) epoch = time.Time(obj["orbit"]["epoch"].to(u.d), format="jd") ss = cls.from_classical( Sun, a, ecc, inc, raan, argp, nu, epoch=epoch.tdb, plane=Planes.EARTH_ECLIPTIC, ) return ss @classmethod @u.quantity_input(alt=u.m, inc=u.rad, raan=u.rad, arglat=u.rad) def circular( cls, attractor, alt, inc=0 * u.deg, raan=0 * u.deg, arglat=0 * u.deg, epoch=J2000, plane=Planes.EARTH_EQUATOR, ): """Return circular `Orbit`. Parameters ---------- attractor : Body Main attractor. alt : ~astropy.units.Quantity Altitude over surface. inc : ~astropy.units.Quantity, optional Inclination, default to 0 deg (equatorial orbit). raan : ~astropy.units.Quantity, optional Right ascension of the ascending node, default to 0 deg. arglat : ~astropy.units.Quantity, optional Argument of latitude, default to 0 deg. epoch: ~astropy.time.Time, optional Epoch, default to J2000. plane : ~poliastro.frames.Planes Fundamental plane of the frame. """ a = attractor.R + alt ecc = 0 * u.one argp = 0 * u.deg return cls.from_classical( attractor, a, ecc, inc, raan, argp, arglat, epoch, plane ) @classmethod @u.quantity_input(angular_velocity=u.rad / u.s, period=u.s, hill_radius=u.m) def geostationary( cls, attractor, angular_velocity=None, period=None, hill_radius=None ): """Return the geostationary orbit for the given attractor and its rotational speed. Parameters ---------- attractor : Body Main attractor. angular_velocity : ~astropy.units.Quantity Rotational angular velocity of the attractor. period : ~astropy.units.Quantity Attractor's rotational period, ignored if angular_velocity is passed. hill_radius : ~astropy.units.Quantity Radius of Hill sphere of the attractor (optional). Hill sphere radius(in contrast with Laplace's SOI) is used here to validate the stability of the geostationary orbit, that is to make sure that the orbital radius required for the geostationary orbit is not outside of the gravitational sphere of influence of the attractor. Hill SOI of parent(if exists) of the attractor is ignored if hill_radius is not provided. """ if angular_velocity is None and period is None: raise ValueError( "At least one among angular_velocity or period must be passed" ) if angular_velocity is None: angular_velocity = 2 * np.pi / period # Find out geostationary radius using r = cube_root(GM/(angular # velocity)^2) with u.set_enabled_equivalencies(u.dimensionless_angles()): geo_radius = np.cbrt(attractor.k / np.square(angular_velocity.to(1 / u.s))) if hill_radius is not None and geo_radius > hill_radius: raise ValueError( "Geostationary orbit for the given parameters doesn't exist" ) altitude = geo_radius - attractor.R return cls.circular(attractor, altitude) @classmethod @u.quantity_input(p=u.m, inc=u.rad, raan=u.rad, argp=u.rad, nu=u.rad) def parabolic( cls, attractor, p, inc, raan, argp, nu, epoch=J2000, plane=Planes.EARTH_EQUATOR ): """Return parabolic `Orbit`. Parameters ---------- attractor : Body Main attractor. p : ~astropy.units.Quantity Semilatus rectum or parameter. inc : ~astropy.units.Quantity, optional Inclination. raan : ~astropy.units.Quantity Right ascension of the ascending node. argp : ~astropy.units.Quantity Argument of the pericenter. nu : ~astropy.units.Quantity True anomaly. epoch: ~astropy.time.Time, optional Epoch, default to J2000. plane : ~poliastro.frames.Planes Fundamental plane of the frame. """ ss = ClassicalState(attractor, p, 1.0 * u.one, inc, raan, argp, nu) return cls(ss, epoch, plane) @classmethod @u.quantity_input( alt=u.m, inc=u.rad, argp=u.rad, raan=u.rad, arglat=u.rad, ecc=u.one ) def frozen( cls, attractor, alt, inc=None, argp=None, raan=0 * u.deg, arglat=0 * u.deg, ecc=None, epoch=J2000, plane=Planes.EARTH_EQUATOR, ): r"""Return frozen Orbit. If any of the given arguments results in an impossibility, some values will be overwritten To achieve frozen orbit these two equations have to be set to zero. .. math:: \dfrac {d\overline {e}}{dt}=\dfrac {-3\overline {n}J_{3}R^{3}_{E}\sin \left( \overline {i}\right) }{2a^{3}\left( 1-\overline {e}^{2}\right) ^{2}}\left( 1-\dfrac {5}{4}\sin ^{2}\overline {i}\right) \cos \overline {w} .. math:: \dfrac {d\overline {\omega }}{dt}=\dfrac {3\overline {n}J_{2}R^{2}_{E}}{a^{2}\left( 1-\overline {e}^{2}\right) ^{2}}\left( 1-\dfrac {5}{4}\sin ^{2}\overline {i}\right) \left[ 1+\dfrac {J_{3}R_{E}}{2J_{2}\overline {a}\left( 1-\overline {e}^{2}\right) }\left( \dfrac {\sin ^{2}\overline {i}-\overline {e}\cos ^{2}\overline {i}}{\sin \overline {i}}\right) \dfrac {\sin \overline {w}}{\overline {e}}\right] The first approach would be to nullify following term to zero: .. math:: ( 1-\dfrac {5}{4}\sin ^{2}) For which one obtains the so-called critical inclinations: i = 63.4349 or 116.5651 degrees. To escape the inclination requirement, the argument of periapsis can be set to w = 90 or 270 degrees to nullify the second equation. Then, one should nullify the right-hand side of the first equation, which yields an expression that correlates the inclination of the object and the eccentricity of the orbit: .. math:: \overline {e}=-\dfrac {J_{3}R_{E}}{2J_{2}\overline {a}\left( 1-\overline {e}^{2}\right) }\left( \dfrac {\sin ^{2}\overline {i}-\overline {e}\cos ^{2} \overline {i}}{\sin \overline {i}}\right) Assuming that e is negligible compared to J2, it can be shown that: .. math:: \overline {e}\approx -\dfrac {J_{3}R_{E}}{2J_{2}\overline {a}}\sin \overline {i} The implementation is divided in the following cases: 1. When the user gives a negative altitude, the method will raise a ValueError 2. When the attractor has not defined J2 or J3, the method will raise an AttributeError 3. When the attractor has J2/J3 outside of range 1 to 10 , the method will raise an NotImplementedError. Special case for Venus.See "Extension of the critical inclination" by Xiaodong Liu, Hexi Baoyin, and Xingrui Ma 4. If argp is not given or the given argp is a critical value: * if eccentricity is none and inclination is none, the inclination is set with a critical value and the eccentricity is obtained from the last formula mentioned * if only eccentricity is none, we calculate this value with the last formula mentioned * if only inclination is none ,we calculate this value with the formula for eccentricity with critical argp. 5. If inc is not given or the given inc is critical: * if the argp and the eccentricity is given we keep these values to create the orbit * if the eccentricity is given we keep this value, if not, default to the eccentricity of the Moon's orbit around the Earth 6. if it's not possible to create an orbit with the the argp and the inclination given, both of them are set to the critical values and the eccentricity is calculate with the last formula Parameters ---------- attractor : Body Main attractor. alt : ~astropy.units.Quantity Altitude over surface. inc : ~astropy.units.Quantity, optional Inclination, default to critical value. argp : ~astropy.units.Quantity, optional Argument of the pericenter, default to critical value. raan : ~astropy.units.Quantity, optional Right ascension of the ascending node, default to 0 deg. arglat : ~astropy.units.Quantity, optional Argument of latitude, default to 0 deg. ecc : ~astropy.units.Quantity Eccentricity, default to the eccentricity of the Moon's orbit around the Earth epoch: ~astropy.time.Time, optional Epoch, default to J2000. plane : ~poliastro.frames.Planes Fundamental plane of the frame. """ critical_argps = [np.pi / 2, 3 * np.pi / 2] * u.rad critical_inclinations = [63.4349 * np.pi / 180, 116.5651 * np.pi / 180] * u.rad try: if 1 <= np.abs(attractor.J2 / attractor.J3) <= 10: raise NotImplementedError( f"This has not been implemented for {attractor.name}" ) assert alt > 0 argp = critical_argps[0] if argp is None else argp a = attractor.R + alt critical_argp = find_closest_value(argp, critical_argps) if np.isclose(argp, critical_argp, 1e-8, 1e-5 * u.rad): if inc is None and ecc is None: inc = critical_inclinations[0] ecc = get_eccentricity_critical_argp(attractor, a, inc) elif ecc is None: ecc = get_eccentricity_critical_argp(attractor, a, inc) else: inc = get_inclination_critical_argp(attractor, a, ecc) return cls.from_classical( attractor, a, ecc, inc, raan, argp, arglat, epoch, plane ) inc = critical_inclinations[0] if inc is None else inc critical_inclination = find_closest_value(inc, critical_inclinations) if np.isclose(inc, critical_inclination, 1e-8, 1e-5 * u.rad): ecc = get_eccentricity_critical_inc(ecc) return cls.from_classical( attractor, a, ecc, inc, raan, argp, arglat, epoch, plane ) argp = critical_argps[0] inc = critical_inclinations[0] ecc = get_eccentricity_critical_argp(attractor, a, inc) return cls.from_classical( attractor, a, ecc, inc, raan, argp, arglat, epoch, plane ) except AttributeError: raise AttributeError( f"Attractor {attractor.name} has not spherical harmonics implemented" ) except AssertionError: raise ValueError( f"The semimajor axis may not be smaller that {attractor.name}'s radius" ) def represent_as(self, representation): """Converts the orbit to a specific representation. .. versionadded:: 0.11.0 Parameters ---------- representation : ~astropy.coordinates.BaseRepresentation Representation object to use. It must be a class, not an instance. Examples -------- >>> from poliastro.examples import iss >>> from astropy.coordinates import CartesianRepresentation, SphericalRepresentation >>> iss.represent_as(CartesianRepresentation) >>> iss.represent_as(CartesianRepresentation).xyz >>> iss.represent_as(CartesianRepresentation).differentials['s'] >>> iss.represent_as(CartesianRepresentation).differentials['s'].d_xyz >>> iss.represent_as(SphericalRepresentation) """ # As we do not know the differentials, we first convert to cartesian, # then let the frame represent_as do the rest # TODO: Perhaps this should be public API as well? cartesian = CartesianRepresentation( *self.r, differentials=CartesianDifferential(*self.v) ) # See the propagate function for reasoning about the usage of a # protected method coords = self.frame._replicate(cartesian, representation_type="cartesian") return coords.represent_as(representation) def to_icrs(self): """Creates a new Orbit object with its coordinates transformed to ICRS. Notice that, strictly speaking, the center of ICRS is the Solar System Barycenter and not the Sun, and therefore these orbits cannot be propagated in the context of the two body problem. Therefore, this function exists merely for practical purposes. .. versionadded:: 0.11.0 """ coords = self.frame.realize_frame(self.represent_as(CartesianRepresentation)) coords.representation_type = CartesianRepresentation icrs_cart = coords.transform_to(ICRS).represent_as(CartesianRepresentation) # TODO: The attractor is in fact the Solar System Barycenter ss = self.from_vectors( Sun, r=icrs_cart.xyz, v=icrs_cart.differentials["s"].d_xyz, epoch=self.epoch ) ss._frame = ICRS() # Hack! return ss def rv(self): """Position and velocity vectors. """ return self.r, self.v def classical(self): """Classical orbital elements. """ return ( self.a, self.ecc, self.inc.to(u.deg), self.raan.to(u.deg), self.argp.to(u.deg), self.nu.to(u.deg), ) def pqw(self): """Perifocal frame (PQW) vectors. """ if self.ecc < 1e-8: if abs(self.inc.to(u.rad).value) > 1e-8: node = np.cross([0, 0, 1], self.h_vec) / norm(self.h_vec) p_vec = node / norm(node) # Circular inclined else: p_vec = [1, 0, 0] * u.one # Circular equatorial else: p_vec = self.e_vec / self.ecc w_vec = self.h_vec / norm(self.h_vec) q_vec = np.cross(w_vec, p_vec) * u.one return p_vec, q_vec, w_vec def __str__(self): if self.a > 1e7 * u.km: unit = u.au else: unit = u.km try: return ORBIT_FORMAT.format( r_p=self.r_p.to(unit).value, r_a=self.r_a.to(unit), inc=self.inc.to(u.deg), frame=self.frame.__class__.__name__, body=self.attractor, epoch=self.epoch, scale=self.epoch.scale.upper(), ) except NotImplementedError: return ORBIT_NO_FRAME_FORMAT.format( r_p=self.r_p.to(unit).value, r_a=self.r_a.to(unit), inc=self.inc.to(u.deg), body=self.attractor, epoch=self.epoch, scale=self.epoch.scale.upper(), ) def __repr__(self): return self.__str__() def propagate(self, value, method=mean_motion, rtol=1e-10, **kwargs): """Propagates an orbit a specified time. If value is true anomaly, propagate orbit to this anomaly and return the result. Otherwise, if time is provided, propagate this `Orbit` some `time` and return the result. Parameters ---------- value : ~astropy.units.Quantity, ~astropy.time.Time, ~astropy.time.TimeDelta Scalar time to propagate. rtol : float, optional Relative tolerance for the propagation algorithm, default to 1e-10. method : function, optional Method used for propagation **kwargs parameters used in perturbation models Returns ------- Orbit New orbit after propagation. """ if isinstance(value, time.Time) and not isinstance(value, time.TimeDelta): time_of_flight = value - self.epoch else: # Works for both Quantity and TimeDelta objects time_of_flight = time.TimeDelta(value) cartesian = propagate(self, time_of_flight, method=method, rtol=rtol, **kwargs) new_epoch = self.epoch + time_of_flight # TODO: Unify with sample # If the frame supports obstime, set the time values try: kwargs = {} if "obstime" in self.frame.frame_attributes: kwargs["obstime"] = new_epoch # Use of a protected method instead of frame.realize_frame # because the latter does not let the user choose the representation type # in one line despite its parameter names, see # https://github.com/astropy/astropy/issues/7784 coords = self.frame._replicate( cartesian, representation_type="cartesian", **kwargs ) return self.from_coords(self.attractor, coords, plane=self.plane) except NotImplementedError: return self.from_vectors( self.attractor, cartesian[0].xyz, cartesian[0].differentials["s"].d_xyz, new_epoch, ) @u.quantity_input(value=u.rad) def propagate_to_anomaly(self, value): """Propagates an orbit to a specific true anomaly. Parameters ---------- value : ~astropy.units.Quantity Returns ------- Orbit Resulting orbit after propagation. """ # TODO: Avoid repeating logic with mean_motion? p, ecc, inc, raan, argp, _ = rv2coe( self.attractor.k.to(u.km ** 3 / u.s ** 2).value, self.r.to(u.km).value, self.v.to(u.km / u.s).value, ) # Compute time of flight for correct epoch M = nu_to_M(self.nu, self.ecc) new_M = nu_to_M(value, self.ecc) time_of_flight = Angle(new_M - M).wrap_at(360 * u.deg) / self.n return self.from_classical( self.attractor, p / (1.0 - ecc ** 2) * u.km, ecc * u.one, inc * u.rad, raan * u.rad, argp * u.rad, value, epoch=self.epoch + time_of_flight, plane=self.plane, ) def _sample_closed(self, values, min_anomaly, max_anomaly): min_anomaly = ( min_anomaly.to(u.rad).value if min_anomaly is not None else self.nu.to(u.rad).value ) max_anomaly = ( max_anomaly.to(u.rad).value if max_anomaly is not None else self.nu.to(u.rad).value + 2 * np.pi ) limits = [min_anomaly, max_anomaly] * u.rad # First sample eccentric anomaly, then transform into true anomaly # to minimize error in the apocenter, see # http://www.dtic.mil/dtic/tr/fulltext/u2/a605040.pdf # Start from pericenter E_values = np.linspace(*limits, values) nu_values = E_to_nu(E_values, self.ecc) return nu_values def _sample_open(self, values, min_anomaly, max_anomaly): # Select a sensible limiting value for non-closed orbits # This corresponds to max(r = 3p, r = self.r) # We have to wrap nu in [-180, 180) to compare it with the output of # the arc cosine, which is in the range [0, 180) # Start from -nu_limit wrapped_nu = Angle(self.nu).wrap_at(180 * u.deg) nu_limit = max(hyp_nu_limit(self.ecc, 3.0), abs(wrapped_nu)).to(u.rad).value limits = [ min_anomaly.to(u.rad).value if min_anomaly is not None else -nu_limit, max_anomaly.to(u.rad).value if max_anomaly is not None else nu_limit, ] * u.rad # type: u.Quantity # Now we check that none of the provided values # is outside of the hyperbolic range nu_max = hyp_nu_limit(self.ecc) - 1e-3 * u.rad # Arbitrary delta if not Angle(limits).is_within_bounds(-nu_max, nu_max): warn("anomaly outside range, clipping", OrbitSamplingWarning) limits = limits.clip(-nu_max, nu_max) nu_values = np.linspace(*limits, values) return nu_values def sample(self, values=100, *, min_anomaly=None, max_anomaly=None): r"""Samples an orbit to some specified time values. .. versionadded:: 0.8.0 Parameters ---------- values : int Number of interval points (default to 100). min_anomaly, max_anomaly : ~astropy.units.Quantity, optional Anomaly limits to sample the orbit. For elliptic orbits the default will be :math:`E \in \left[0, 2 \pi \right]`, and for hyperbolic orbits it will be :math:`\nu \in \left[-\nu_c, \nu_c \right]`, where :math:`\nu_c` is either the current true anomaly or a value that corresponds to :math:`r = 3p`. Returns ------- positions: ~astropy.coordinates.BaseCoordinateFrame Array of x, y, z positions, with proper times as the frame attributes if supported. Notes ----- When specifying a number of points, the initial and final position is present twice inside the result (first and last row). This is more useful for plotting. Examples -------- >>> from astropy import units as u >>> from poliastro.examples import iss >>> iss.sample() # doctest: +ELLIPSIS >>> iss.sample(10) # doctest: +ELLIPSIS """ if self.ecc < 1: nu_values = self._sample_closed(values, min_anomaly, max_anomaly) else: nu_values = self._sample_open(values, min_anomaly, max_anomaly) time_values = time.TimeDelta(self._generate_time_values(nu_values)) cartesian = propagate(self, time_values) # TODO: Unify with propagate # If the frame supports obstime, set the time values try: kwargs = {} if "obstime" in self.frame.frame_attributes: kwargs["obstime"] = self.epoch + time_values else: warn( f"Frame {self.frame.__class__} does not support 'obstime', time values were not returned" ) # Use of a protected method instead of frame.realize_frame # because the latter does not let the user choose the representation type # in one line despite its parameter names, see # https://github.com/astropy/astropy/issues/7784 coords = self.frame._replicate( cartesian, representation_type="cartesian", **kwargs ) return coords except NotImplementedError: warn( f"No frame found for attractor {self.attractor}, returning only cartesian coordinates instead" ) return cartesian def _generate_time_values(self, nu_vals): # Subtract current anomaly to start from the desired point ecc = self.ecc.value nu = self.nu.to(u.rad).value M_vals = [ nu_to_M_fast(nu_val, ecc) - nu_to_M_fast(nu, ecc) for nu_val in nu_vals.to(u.rad).value ] * u.rad time_values = (M_vals / self.n).decompose() return time_values def apply_maneuver(self, maneuver, intermediate=False): """Returns resulting `Orbit` after applying maneuver to self. Optionally return intermediate states (default to False). Parameters ---------- maneuver : Maneuver Maneuver to apply. intermediate : bool, optional Return intermediate states, default to False. """ orbit_new = self # Initialize states = [] attractor = self.attractor for delta_t, delta_v in maneuver: if not delta_t == 0 * u.s: orbit_new = orbit_new.propagate(delta_t) r, v = orbit_new.rv() vnew = v + delta_v orbit_new = self.from_vectors(attractor, r, vnew, orbit_new.epoch) states.append(orbit_new) if intermediate: res = states # type: Union[Orbit, List[Orbit]] else: res = orbit_new return res def plot(self, label=None, use_3d=False, interactive=False): """Plots the orbit as an interactive widget. Parameters ---------- label : str, optional Label for the orbit, defaults to empty. use_3d : bool, optional Produce a 3D plot, default to False. interactive : bool, optional Produce an interactive (rather than static) image of the orbit, default to False. This option requires Plotly properly installed and configured for your environment. """ if not interactive and use_3d: raise ValueError( "The static plotter does not support 3D, use `interactive=True`" ) elif not interactive: from poliastro.plotting.static import StaticOrbitPlotter return StaticOrbitPlotter().plot(self, label=label) elif use_3d: from poliastro.plotting.core import OrbitPlotter3D return OrbitPlotter3D().plot(self, label=label) else: from poliastro.plotting.core import OrbitPlotter2D return OrbitPlotter2D().plot(self, label=label) poliastro-0.13.1/src/poliastro/twobody/propagation.py000644 001750 001750 00000035176 13577127432 023664 0ustar00juanlujuanlu000000 000000 """The following script holds the different high level functions for the different propagators available at poliastro: +-------------+------------+-----------------+-----------------+ | Propagator | Elliptical | Parabolic | Hyperbolic | +-------------+------------+-----------------+-----------------+ | mean_motion | ✓ | ✓ | ✓ | +-------------+------------+-----------------+-----------------+ | kepler | ✓ | ✓ | ✓ | +-------------+------------+-----------------+-----------------+ | mikkola | ✓ | NOT IMPLEMENTED | ✓ | +-------------+------------+-----------------+-----------------+ | markley | ✓ | x | x | +-------------+------------+-----------------+-----------------+ | pimienta | ✓ | ✓ | NOT IMPLEMENTED | +-------------+------------+-----------------+-----------------+ | gooding | ✓ | NOT IMPLEMENTED | NOT IMPLEMENTED | +-------------+------------+-----------------+-----------------+ | danby | ✓ | x | ✓ | +-------------+------------+-----------------+-----------------+ | cowell | ✓ | ✓ | ✓ | +-------------+------------+-----------------+-----------------+ """ import functools import numpy as np from astropy import time, units as u from astropy.coordinates import CartesianDifferential, CartesianRepresentation from scipy.integrate import solve_ivp from poliastro.core.propagation import ( danby as danby_fast, func_twobody, gooding as gooding_fast, kepler as kepler_fast, markley as markley_fast, mean_motion as mean_motion_fast, mikkola as mikkola_fast, pimienta as pimienta_fast, ) from poliastro.integrators import DOP835 def cowell(k, r, v, tofs, rtol=1e-11, *, ad=None, **ad_kwargs): """Propagates orbit using Cowell's formulation. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol : float, optional Maximum relative error permitted, default to 1e-10. ad : function(t0, u, k), optional Non Keplerian acceleration (km/s2), default to None. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Propagated velocity vectors. Raises ------ RuntimeError If the algorithm didn't converge. Note ----- This method uses a Dormand & Prince method of order 8(5,3) available in the :py:class:`poliastro.integrators` module. If multiple tofs are provided, the method propagates to the maximum value and calculates the other values via dense output """ k = k.to(u.km ** 3 / u.s ** 2).value x, y, z = r.to(u.km).value vx, vy, vz = v.to(u.km / u.s).value tofs = tofs.to(u.s).value u0 = np.array([x, y, z, vx, vy, vz]) # Set the non Keplerian acceleration if ad is None: def ad(t0, u_, k_): return 0, 0, 0 f_with_ad = functools.partial(func_twobody, k=k, ad=ad, ad_kwargs=ad_kwargs) result = solve_ivp( f_with_ad, (0, max(tofs)), u0, rtol=rtol, atol=1e-12, method=DOP835, dense_output=True, ) if not result.success: raise RuntimeError("Integration failed") rrs = [] vvs = [] for i in range(len(tofs)): y = result.sol(tofs[i]) rrs.append(y[:3]) vvs.append(y[3:]) return rrs * u.km, vvs * u.km / u.s def mean_motion(k, r, v, tofs, **kwargs): """Propagates orbit using Cowell's formulation. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Propagated velocity vectors. """ k = k.to(u.km ** 3 / u.s ** 2).value r0 = r.to(u.km).value v0 = v.to(u.km / u.s).value tofs = tofs.to(u.s).value results = [mean_motion_fast(k, r0, v0, tof) for tof in tofs] # TODO: Rewrite to avoid iterating twice return ( [result[0] for result in results] * u.km, [result[1] for result in results] * u.km / u.s, ) def kepler(k, r, v, tofs, numiter=350, **kwargs): """Propagates Keplerian orbit. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. numiter : int, optional Maximum number of iterations, default to 35. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Propagated velocity vectors. Raises ------ RuntimeError If the algorithm didn't converge. Note ----- This algorithm is based on Vallado implementation, and does basic Newton iteration on the Kepler equation written using universal variables. Battin claims his algorithm uses the same amount of memory but is between 40 % and 85 % faster. """ k = k.to(u.km ** 3 / u.s ** 2).value r0 = r.to(u.km).value v0 = v.to(u.km / u.s).value tofs = tofs.to(u.s).value results = [_kepler(k, r0, v0, tof, numiter=numiter) for tof in tofs] # TODO: Rewrite to avoid iterating twice return ( [result[0] for result in results] * u.km, [result[1] for result in results] * u.km / u.s, ) def _kepler(k, r0, v0, tof, *, numiter): # Compute Lagrange coefficients f, g, fdot, gdot = kepler_fast(k, r0, v0, tof, numiter) assert np.abs(f * gdot - fdot * g - 1) < 1e-5 # Fixed tolerance # Return position and velocity vectors r = f * r0 + g * v0 v = fdot * r0 + gdot * v0 return r, v def mikkola(k, r, v, tofs, rtol=None): """ Solves Kepler Equation by a cubic approximation. This method is valid no mater the orbit's nature. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol: float This method does not require of tolerance since it is non iterative. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Note ---- This method was derived by Seppo Mikola in his paper *A Cubic Approximation For Kepler's Equation* with DOI: https://doi.org/10.1007/BF01235850 """ k = k.to(u.m ** 3 / u.s ** 2).value r0 = r.to(u.m).value v0 = v.to(u.m / u.s).value tofs = tofs.to(u.s).value results = [mikkola_fast(k, r0, v0, tof) for tof in tofs] return ( [result[0] for result in results] * u.m, [result[1] for result in results] * u.m / u.s, ) def markley(k, r, v, tofs, rtol=None): """ Elliptical Kepler Equation solver based on a fifth-order refinement of the solution of a cubic equation. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol: float This method does not require of tolerance since it is non iterative. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Propagated velocity vectors. Note ---- This method was originally presented by Markley in his paper *Kepler Equation Solver* with DOI: https://doi.org/10.1007/BF00691917 """ k = k.to(u.m ** 3 / u.s ** 2).value r0 = r.to(u.m).value v0 = v.to(u.m / u.s).value tofs = tofs.to(u.s).value results = [markley_fast(k, r0, v0, tof) for tof in tofs] return ( [result[0] for result in results] * u.m, [result[1] for result in results] * u.m / u.s, ) def pimienta(k, r, v, tofs, rtol=None): """ Kepler solver for both elliptic and parabolic orbits based on a 15th order polynomial with accuracies around 10e-5 for elliptic case and 10e-13 in the hyperbolic regime. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol: float This method does not require of tolerance since it is non iterative. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Propagated velocity vectors. Note ---- This algorithm was developed by Pimienta-Peñalver and John L. Crassidis in their paper *Accurate Kepler Equation solver without trascendental function evaluations*. Original paper is on Buffalo's UBIR repository: http://hdl.handle.net/10477/50522 """ k = k.to(u.m ** 3 / u.s ** 2).value r0 = r.to(u.m).value v0 = v.to(u.m / u.s).value tofs = tofs.to(u.s).value results = [pimienta_fast(k, r0, v0, tof) for tof in tofs] return ( [result[0] for result in results] * u.m, [result[1] for result in results] * u.m / u.s, ) def gooding(k, r, v, tofs, numiter=150, rtol=1e-8): """ Solves the Elliptic Kepler Equation with a cubic convergence and accuracy better than 10e-12 rad is normally achieved. It is not valid for eccentricities equal or greater than 1.0. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol: float This method does not require of tolerance since it is non iterative. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Note ---- This method was developed by Gooding and Odell in their paper *The hyperbolic Kepler equation (and the elliptic equation revisited)* with DOI: https://doi.org/10.1007/BF01235540 """ k = k.to(u.m ** 3 / u.s ** 2).value r0 = r.to(u.m).value v0 = v.to(u.m / u.s).value tofs = tofs.to(u.s).value results = [gooding_fast(k, r0, v0, tof, numiter=numiter, rtol=rtol) for tof in tofs] return ( [result[0] for result in results] * u.m, [result[1] for result in results] * u.m / u.s, ) def danby(k, r, v, tofs, rtol=1e-8): """ Kepler solver for both elliptic and parabolic orbits based on Danby's algorithm. Parameters ---------- k : ~astropy.units.Quantity Standard gravitational parameter of the attractor. r : ~astropy.units.Quantity Position vector. v : ~astropy.units.Quantity Velocity vector. tofs : ~astropy.units.Quantity Array of times to propagate. rtol: float Relative error for accuracy of the method. Returns ------- rr : ~astropy.units.Quantity Propagated position vectors. vv : ~astropy.units.Quantity Propagated velocity vectors. Note ---- This algorithm was developed by Danby in his paper *The solution of Kepler Equation* with DOI: https://doi.org/10.1007/BF01686811 """ k = k.to(u.m ** 3 / u.s ** 2).value r0 = r.to(u.m).value v0 = v.to(u.m / u.s).value tofs = tofs.to(u.s).value results = [danby_fast(k, r0, v0, tof) for tof in tofs] return ( [result[0] for result in results] * u.m, [result[1] for result in results] * u.m / u.s, ) def propagate(orbit, time_of_flight, *, method=mean_motion, rtol=1e-10, **kwargs): """Propagate an orbit some time and return the result. Parameters ---------- orbit : ~poliastro.twobody.Orbit Orbit object to propagate. time_of_flight : ~astropy.time.TimeDelta Time of propagation. method : callable, optional Propagation method, default to mean_motion. rtol : float, optional Relative tolerance, default to 1e-10. """ # Check if propagator fulfills orbit requirements if orbit.ecc < 1.0 and method not in ELLIPTIC_PROPAGATORS: raise ValueError( "Can not use an parabolic/hyperbolic propagator for elliptical orbits." ) elif orbit.ecc == 1.0 and method not in PARABOLIC_PROPAGATORS: raise ValueError( "Can not use an elliptic/hyperbolic propagator for parabolic orbits." ) elif orbit.ecc > 1.0 and method not in HYPERBOLIC_PROPAGATORS: raise ValueError( "Can not use an elliptic/parabolic propagator for hyperbolic orbits." ) else: pass # Use the highest precision we can afford # np.atleast_1d does not work directly on TimeDelta objects jd1 = np.atleast_1d(time_of_flight.jd1) jd2 = np.atleast_1d(time_of_flight.jd2) time_of_flight = time.TimeDelta(jd1, jd2, format="jd", scale=time_of_flight.scale) rr, vv = method( orbit.attractor.k, orbit.r, orbit.v, time_of_flight.to(u.s), rtol=rtol, **kwargs ) # TODO: Turn these into unit tests assert rr.ndim == 2 assert vv.ndim == 2 cartesian = CartesianRepresentation( rr, differentials=CartesianDifferential(vv, xyz_axis=1), xyz_axis=1 ) return cartesian ELLIPTIC_PROPAGATORS = [ mean_motion, kepler, mikkola, markley, pimienta, gooding, danby, cowell, ] PARABOLIC_PROPAGATORS = [mean_motion, kepler, mikkola, pimienta, gooding, cowell] HYPERBOLIC_PROPAGATORS = [ mean_motion, kepler, mikkola, kepler, pimienta, gooding, danby, cowell, ] ALL_PROPAGATORS = list( set(ELLIPTIC_PROPAGATORS) & set(PARABOLIC_PROPAGATORS) & set(HYPERBOLIC_PROPAGATORS) ) poliastro-0.13.1/src/poliastro/twobody/thrust/000755 001750 001750 00000000000 13577132207 022300 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro/twobody/thrust/__init__.py000644 001750 001750 00000000414 13525576227 024417 0ustar00juanlujuanlu000000 000000 from .change_a_inc import change_a_inc from .change_argp import change_argp from .change_ecc_quasioptimal import change_ecc_quasioptimal from .change_inc_ecc import change_inc_ecc __all__ = ["change_a_inc", "change_argp", "change_ecc_quasioptimal", "change_inc_ecc"] poliastro-0.13.1/src/poliastro/twobody/thrust/change_a_inc.py000644 001750 001750 00000003057 13577127432 025241 0ustar00juanlujuanlu000000 000000 import numpy as np from poliastro.core.thrust.change_a_inc import ( beta, compute_parameters, extra_quantities, ) from poliastro.core.util import cross, norm def change_a_inc(k, a_0, a_f, inc_0, inc_f, f): """Guidance law from the Edelbaum/Kéchichian theory, optimal transfer between circular inclined orbits (a_0, i_0) --> (a_f, i_f), ecc = 0. Parameters ---------- k : float Gravitational parameter. a_0 : float Initial semimajor axis. a_f : float Final semimajor axis. inc_0 : float Initial inclination. inc_f : float Final inclination. f : float Magnitude of constant acceleration Notes ----- Edelbaum theory, reformulated by Kéchichian. References ---------- * Edelbaum, T. N. "Propulsion Requirements for Controllable Satellites", 1961. * Kéchichian, J. A. "Reformulation of Edelbaum's Low-Thrust Transfer Problem Using Optimal Control Theory", 1997. """ V_0, beta_0_, _ = compute_parameters(k, a_0, a_f, inc_0, inc_f) def a_d(t0, u_, k): r = u_[:3] v = u_[3:] # Change sign of beta with the out-of-plane velocity beta_ = beta(t0, V_0=V_0, f=f, beta_0=beta_0_) * np.sign(r[0] * (inc_f - inc_0)) t_ = v / norm(v) w_ = cross(r, v) / norm(cross(r, v)) # n_ = cross(t_, w_) accel_v = f * (np.cos(beta_) * t_ + np.sin(beta_) * w_) return accel_v delta_V, t_f = extra_quantities(k, a_0, a_f, inc_0, inc_f, f) return a_d, delta_V, t_f poliastro-0.13.1/src/poliastro/twobody/thrust/change_argp.py000644 001750 001750 00000002176 13577127432 025122 0ustar00juanlujuanlu000000 000000 """Argument of perigee change, with formulas developed by Pollard. References ---------- * Pollard, J. E. "Simplified Approach for Assessment of Low-Thrust Elliptical Orbit Transfers", 1997. * Pollard, J. E. "Evaluation of Low-Thrust Orbital Maneuvers", 1998. """ import numpy as np from poliastro.core.elements import rv2coe from poliastro.core.thrust.change_argp import extra_quantities from poliastro.core.util import cross, norm def change_argp(k, a, ecc, argp_0, argp_f, f): """Guidance law from the model. Thrust is aligned with an inertially fixed direction perpendicular to the semimajor axis of the orbit. Parameters ---------- f : float Magnitude of constant acceleration """ def a_d(t0, u_, k): r = u_[:3] v = u_[3:] nu = rv2coe(k, r, v)[-1] alpha_ = nu - np.pi / 2 r_ = r / norm(r) w_ = cross(r, v) / norm(cross(r, v)) s_ = cross(w_, r_) accel_v = f * (np.cos(alpha_) * s_ + np.sin(alpha_) * r_) return accel_v delta_V, t_f = extra_quantities(k, a, ecc, argp_0, argp_f, f, A=0.0) return a_d, delta_V, t_f poliastro-0.13.1/src/poliastro/twobody/thrust/change_ecc_quasioptimal.py000644 001750 001750 00000002523 13577127432 027507 0ustar00juanlujuanlu000000 000000 """Quasi optimal eccentricity-only change, with formulas developed by Pollard. References ---------- * Pollard, J. E. "Simplified Approach for Assessment of Low-Thrust Elliptical Orbit Transfers", 1997. """ import astropy.units as u import numpy as np from poliastro.core.thrust.change_ecc_quasioptimal import extra_quantities from poliastro.core.util import cross, norm def change_ecc_quasioptimal(ss_0, ecc_f, f): """Guidance law from the model. Thrust is aligned with an inertially fixed direction perpendicular to the semimajor axis of the orbit. Parameters ---------- ss_0 : Orbit Initial orbit, containing all the information. ecc_f : float Final eccentricity. f : float Magnitude of constant acceleration """ # We fix the inertial direction at the beginning k = ss_0.attractor.k.to(u.km ** 3 / u.s ** 2).value a = ss_0.a.to(u.km).value ecc_0 = ss_0.ecc.value if ecc_0 > 0.001: # Arbitrary tolerance ref_vec = ss_0.e_vec / ecc_0 else: ref_vec = ss_0.r / norm(ss_0.r) h_unit = ss_0.h_vec / norm(ss_0.h_vec) thrust_unit = cross(h_unit, ref_vec) * np.sign(ecc_f - ecc_0) def a_d(t0, u_, k): accel_v = f * thrust_unit return accel_v delta_V, t_f = extra_quantities(k, a, ecc_0, ecc_f, f) return a_d, delta_V, t_f poliastro-0.13.1/src/poliastro/twobody/thrust/change_inc_ecc.py000644 001750 001750 00000003543 13577127432 025553 0ustar00juanlujuanlu000000 000000 """Simultaneous eccentricity and inclination changes. References ---------- * Pollard, J. E. "Simplified Analysis of Low-Thrust Orbital Maneuvers", 2000. """ import numpy as np from astropy import units as u from poliastro.core.elements import rv2coe from poliastro.core.thrust.change_inc_ecc import beta, extra_quantities from poliastro.core.util import cross, norm def change_inc_ecc(ss_0, ecc_f, inc_f, f): """Guidance law from the model. Thrust is aligned with an inertially fixed direction perpendicular to the semimajor axis of the orbit. Parameters ---------- ss_0 : Orbit Initial orbit, containing all the information. ecc_f : float Final eccentricity. inc_f : float Final inclination. f : float Magnitude of constant acceleration. """ # We fix the inertial direction at the beginning ecc_0 = ss_0.ecc.value if ecc_0 > 0.001: # Arbitrary tolerance ref_vec = ss_0.e_vec / ecc_0 else: ref_vec = ss_0.r / norm(ss_0.r) h_unit = ss_0.h_vec / norm(ss_0.h_vec) thrust_unit = cross(h_unit, ref_vec) * np.sign(ecc_f - ecc_0) inc_0 = ss_0.inc.to(u.rad).value argp = ss_0.argp.to(u.rad).value beta_0_ = beta(ecc_0, ecc_f, inc_0, inc_f, argp) def a_d(t0, u_, k): r = u_[:3] v = u_[3:] nu = rv2coe(k, r, v)[-1] beta_ = beta_0_ * np.sign( np.cos(nu) ) # The sign of ß reverses at minor axis crossings w_ = cross(r, v) / norm(cross(r, v)) accel_v = f * (np.cos(beta_) * thrust_unit + np.sin(beta_) * w_) return accel_v delta_V, beta_, t_f = extra_quantities( ss_0.attractor.k.to(u.km ** 3 / u.s ** 2).value, ss_0.a.to(u.km).value, ecc_0, ecc_f, inc_0, inc_f, argp, f, ) return a_d, delta_V, beta_, t_f poliastro-0.13.1/src/poliastro/util.py000644 001750 001750 00000012231 13577127432 020612 0ustar00juanlujuanlu000000 000000 """Function helpers. """ import numpy as np from astropy import units as u from astropy.time import Time from poliastro.core.util import ( circular_velocity as circular_velocity_fast, norm as norm_fast, rotate as rotate_fast, ) u.kms = u.km / u.s u.km3s2 = u.km ** 3 / u.s ** 2 def circular_velocity(k, a): """Compute circular velocity for a given body (k) and semimajor axis (a). """ return circular_velocity_fast(k.to(u.km3s2).value, a.to(u.km).value) * u.kms def rotate(vector, angle, axis="z"): """Rotates a vector around axis a right-handed positive angle. This is just a convenience function around :py:func:`astropy.coordinates.matrix_utilities.rotation_matrix`. Parameters ---------- vector : ~astropy.units.Quantity Dimension 3 vector. angle : ~astropy.units.Quantity Angle of rotation. axis : str, optional Either 'x', 'y' or 'z'. Note ----- 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, unit)`` or use the convenience function :py:func:`transform`, see [1]_. References ---------- .. [1] http://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities """ return ( rotate_fast(vector.value, angle.to(u.rad).value, ["x", "y", "z"].index(axis)) * vector.unit ) def transform(vector, angle, axis="z"): """Rotates a coordinate system around axis a positive right-handed angle. Note ----- This is a convenience function, equivalent to ``rotate(vec, -angle, axis, unit)``. Refer to the documentation of :py:func:`rotate` for further information. """ return rotate(vector, -angle, axis) def norm(vec): """Norm of a Quantity vector that respects units. Parameters ---------- vec : ~astropy.units.Quantity Vector with units. """ return norm_fast(vec.value) * vec.unit def time_range(start, *, periods=50, spacing=None, end=None, format=None, scale=None): """Generates range of astronomical times. .. versionadded:: 0.8.0 Parameters ---------- periods : int, optional Number of periods, default to 50. spacing : Time or Quantity, optional Spacing between periods, optional. end : Time or equivalent, optional End date. Returns ------- Time Array of time values. """ start = Time(start, format=format, scale=scale) if spacing is not None and end is None: result = start + spacing * np.arange(0, periods) elif end is not None and spacing is None: end = Time(end, format=format, scale=scale) result = start + (end - start) * np.linspace(0, 1, periods) else: raise ValueError("Either 'end' or 'spacing' must be specified") return result @u.quantity_input(ecc=u.one) def hyp_nu_limit(ecc, r_max_ratio=np.inf): r"""Limit true anomaly for hyperbolic orbits. Parameters ---------- ecc : ~astropy.units.Quantity Eccentricity, should be larger than 1. r_max_ratio : float, optional Value of :math:`r_{\text{max}} / p` for this angle, default to infinity. """ return np.arccos(-(1 - 1 / r_max_ratio) / ecc) @u.quantity_input(a=u.m, inc=u.rad) def get_eccentricity_critical_argp(attractor, a, inc): """Calculates the eccentricity for frozen orbits when the argument of perigee is critical Parameters ---------- attractor : Body Main attractor. a : ~astropy.units.Quantity Orbit's semimajor axis inc : ~astropy.units.Quantity, optional Inclination, default to critical value. """ ecc = -attractor.J3 * attractor.R * np.sin(inc) / 2 / attractor.J2 / a return ecc @u.quantity_input(a=u.m, ecc=u.one) def get_inclination_critical_argp(attractor, a, ecc): """Calculates the inclination for frozen orbits when the argument of perigee is critical and the eccentricity is given Parameters ---------- attractor : Body Main attractor. a : ~astropy.units.Quantity Orbit's semimajor axis ecc : ~astropy.units.Quantity, optional Eccentricity """ inc = np.arcsin(-ecc * a * attractor.J2 * 2 / attractor.R / attractor.J3) * u.rad return inc @u.quantity_input(ecc=u.one) def get_eccentricity_critical_inc(ecc=None): """Calculates the eccentricity when a frozen orbit has critical inclination If ecc is None we set an arbitrary value which is the Moon ecc because it seems reasonable Parameters ---------- ecc: : ~astropy.units.Quantity, optional Eccentricity, or None if it was not defined """ moon_ecc = 0.0549 * u.one ecc = moon_ecc if ecc is None else ecc return ecc @u.quantity_input(value=u.rad, values=u.rad) def find_closest_value(value, values): """Calculates the closest value in the given values Parameters ---------- value : ~astropy.units.Quantity values : ~astropy.units.Quantity """ index = np.abs(np.asarray(values) * u.rad - value).argmin() return values[index] poliastro-0.13.1/src/poliastro.egg-info/000755 001750 001750 00000000000 13577132207 020752 5ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro.egg-info/PKG-INFO000644 001750 001750 00000026105 13577132207 022053 0ustar00juanlujuanlu000000 000000 Metadata-Version: 2.1 Name: poliastro Version: 0.13.1 Summary: Python package for Orbital Mechanics Home-page: https://blog.poliastro.space/ Author: Juan Luis Cano Author-email: hello@juanlu.space License: MIT Download-URL: https://github.com/poliastro/poliastro Project-URL: Source, https://github.com/poliastro/poliastro Project-URL: Tracker, https://github.com/poliastro/poliastro/issues Description: .. poliastro .. image:: http://poliastro.github.io/images/logo_text.png :target: http://poliastro.github.io/ :alt: poliastro logo :width: 675px :align: center .. |orcid| image:: https://img.shields.io/badge/id-0000--0002--2187--161X-a6ce39.svg :target: http://orcid.org/0000-0002-2187-161X :Name: poliastro :Website: https://poliastro.github.io/ :Author: Juan Luis Cano Rodríguez |orcid| :Version: 0.13.1 .. |circleci| image:: https://img.shields.io/circleci/project/github/poliastro/poliastro/0.13.x.svg?style=flat-square&logo=circleci :target: https://circleci.com/gh/poliastro/poliastro .. |appveyor| image:: https://img.shields.io/appveyor/ci/Juanlu001/poliastro/0.13.x.svg?style=flat-square&logo=appveyor :target: https://ci.appveyor.com/project/Juanlu001/poliastro/branch/0.13.x .. |codecov| image:: https://img.shields.io/codecov/c/github/poliastro/poliastro.svg?style=flat-square :target: https://codecov.io/github/poliastro/poliastro?branch=0.13.x .. |codeclimate| image:: https://api.codeclimate.com/v1/badges/fd2aa5bf8c4b7984d11b/maintainability :target: https://codeclimate.com/github/poliastro/poliastro/maintainability .. |docs| image:: https://img.shields.io/badge/docs-v0.13.1-brightgreen.svg?style=flat-square :target: https://docs.poliastro.space/en/v0.13.1/?badge=v0.13.1 .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square :target: https://github.com/poliastro/poliastro/raw/0.13.x/COPYING .. |doi| image:: https://zenodo.org/badge/11178845.svg?style=flat-square :target: https://zenodo.org/badge/latestdoi/11178845 .. |astropy| image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat-square :target: http://www.astropy.org/ .. |mailing| image:: https://img.shields.io/badge/mailing%20list-groups.io-8cbcd1.svg?style=flat-square :target: https://groups.io/g/poliastro-dev .. |matrix| image:: https://img.shields.io/matrix/poliastro:matrix.org.svg?style=flat-square :alt: Join the chat at https://chat.openastronomy.org/#/room/#poliastro:matrix.org :target: https://chat.openastronomy.org/#/room/#poliastro:matrix.org |circleci| |appveyor| |codecov| |codeclimate| |docs| |license| |doi| |astropy| |mailing| |matrix| poliastro is an open source pure Python package dedicated to problems arising in Astrodynamics and Orbital Mechanics, such as orbit propagation, solution of the Lambert's problem, conversion between position and velocity vectors and classical orbital elements and orbit plotting, focusing on interplanetary applications. It is released under the MIT license. .. code-block:: python from poliastro.examples import molniya molniya.plot() .. image:: https://github.com/poliastro/poliastro/raw/0.13.x/docs/source/examples/molniya.png :align: center Documentation ============= |docs| Complete documentation, including a user guide and an API reference, can be read on the wonderful `Read the Docs`_. https://docs.poliastro.space/ .. _`Read the Docs`: https://readthedocs.org/ Examples ======== .. |mybinder| image:: https://img.shields.io/badge/launch-binder-e66581.svg?style=flat-square :target: https://beta.mybinder.org/v2/gh/poliastro/poliastro/0.13.x?filepath=index.ipynb |mybinder| In the examples directory you can find several Jupyter notebooks with specific applications of poliastro. You can launch a cloud Jupyter server using `binder`_ to edit the notebooks without installing anything. Try it out! https://beta.mybinder.org/v2/gh/poliastro/poliastro/0.13.x?filepath=index.ipynb .. _binder: https://beta.mybinder.org/ Requirements ============ poliastro requires the following Python packages: * NumPy, for basic numerical routines * Astropy, for physical units and time handling * numba (optional), for accelerating the code * jplephem, for the planetary ephemerides using SPICE kernels * matplotlib, for orbit plotting * plotly, for 2D and 3D interactive orbit plotting * SciPy, for root finding and numerical propagation poliastro is usually tested on Linux and Windows on Python 3.6 and 3.7 against latest NumPy. It should work on OS X without problems. ============== ============ =================== Platform Site Status ============== ============ =================== Linux CircleCI |circleci| Windows x64 Appveyor |appveyor| ============== ============ =================== Installation ============ The easiest and fastest way to get the package up and running is to install poliastro using `conda `_:: $ conda install poliastro --channel conda-forge Please check out the `documentation for alternative installation methods`_. .. _`documentation for alternative installation methods`: https://docs.poliastro.space/en/v0.13.1/getting_started.html#alternative-installation-methods Testing ======= |codecov| If installed correctly, the tests can be run using pytest:: $ python -c "import poliastro.testing; poliastro.testing.test()" ===================================== test session starts ===================================== platform linux -- Python 3.7.1, pytest-4.2.0, py-1.7.0, pluggy-0.8.1 rootdir: /home/juanlu/.miniconda36/envs/_test37/lib/python3.7/site-packages/poliastro, inifile: collected 747 items [...] ========= 738 passed, 3 skipped, 5 xfailed, 1 xpassed, 13 warnings in 392.12 seconds ========== $ Problems ======== If the installation fails or you find something that doesn't work as expected, please open an issue in the `issue tracker`_. .. _`issue tracker`: https://github.com/poliastro/poliastro/issues Contributing ============ .. image:: https://img.shields.io/waffle/label/poliastro/poliastro/1%20-%20Ready.svg?style=flat-square :target: https://waffle.io/poliastro/poliastro :alt: 'Stories in Ready' poliastro is a community project, hence all contributions are more than welcome! For more information, head to `CONTRIBUTING.rst`_. .. _`CONTRIBUTING.rst`: https://github.com/poliastro/poliastro/blob/0.13.x/CONTRIBUTING.rst Support ======= |mailing| |matrix| Release announcements and general discussion take place on our `Mailing List`_ . For further clarifications and discussions, feel free to join Poliastro `Chat Room`_. .. _`Chat Room`: https://chat.openastronomy.org/#/room/#poliastro:matrix.org .. _`Mailing List`: https://groups.io/g/poliastro-dev Citing ====== If you use poliastro on your project, please `drop me a line `_. You can also use the DOI to cite it in your publications. This is the latest one: |doi| And this is an example citation format:: Juan Luis Cano Rodríguez et al.. (2015). poliastro: poliastro 0.4.0. Zenodo. 10.5281/zenodo.17462 License ======= |license| poliastro is released under the MIT license, hence allowing commercial use of the library. Please refer to the COPYING file. FAQ === What's up with the name? ------------------------ poliastro comes from Polimi, which is the shortened name of the Politecnico di Milano, the Italian university where I was studying while writing this software. It's my tiny tribute to a place I came to love. *Grazie mille!* Can I do with poliastro? ----------------------------------------------- poliastro is focused on interplanetary applications. This has two consequences: * It tries to be more general than other Flight Dynamics core libraries more focused on Earth satellites (see `Related software`_ for a brief list), allowing the algorithms to work also for orbits around non-Earth bodies. * It leaves out certain features that would be too Earth-specific, such as TLE reading, SGP4 propagation, groundtrack plotting and others. .. _`Related software`: https://docs.poliastro.space/en/v0.13.1/about.html#related-software What's the future of the project? --------------------------------- poliastro is actively maintained and receiving an influx of new contributors thanks to the generous sponsorship of Google and the European Space Agency. The best way to get an idea of the roadmap is to see the `Milestones`_ of the project. .. _`Milestones`: https://github.com/poliastro/poliastro/milestones Keywords: aero,aerospace,engineering,astrodynamics,orbits,kepler,orbital mechanics Platform: any Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Scientific/Engineering :: Physics Classifier: Topic :: Scientific/Engineering :: Astronomy Requires-Python: <3.8,>=3.6 Provides-Extra: jupyter Provides-Extra: cesium Provides-Extra: dev poliastro-0.13.1/src/poliastro.egg-info/SOURCES.txt000644 001750 001750 00000006772 13577132207 022652 0ustar00juanlujuanlu000000 000000 AUTHORS COPYING MANIFEST.in README.rst setup.cfg setup.py src/poliastro/__init__.py src/poliastro/bodies.py src/poliastro/cli.py src/poliastro/constants.py src/poliastro/coordinates.py src/poliastro/ephem.py src/poliastro/examples.py src/poliastro/frames.py src/poliastro/integrators.py src/poliastro/maneuver.py src/poliastro/spheroid_location.py src/poliastro/testing.py src/poliastro/util.py src/poliastro.egg-info/PKG-INFO src/poliastro.egg-info/SOURCES.txt src/poliastro.egg-info/dependency_links.txt src/poliastro.egg-info/entry_points.txt src/poliastro.egg-info/not-zip-safe src/poliastro.egg-info/requires.txt src/poliastro.egg-info/top_level.txt src/poliastro/core/__init__.py src/poliastro/core/_jit.py src/poliastro/core/angles.py src/poliastro/core/elements.py src/poliastro/core/hyper.py src/poliastro/core/iod.py src/poliastro/core/perturbations.py src/poliastro/core/propagation.py src/poliastro/core/stumpff.py src/poliastro/core/util.py src/poliastro/core/thrust/__init__.py src/poliastro/core/thrust/change_a_inc.py src/poliastro/core/thrust/change_argp.py src/poliastro/core/thrust/change_ecc_quasioptimal.py src/poliastro/core/thrust/change_inc_ecc.py src/poliastro/czml/__init__.py src/poliastro/czml/extract_czml.py src/poliastro/czml/utils.py src/poliastro/iod/__init__.py src/poliastro/iod/izzo.py src/poliastro/iod/vallado.py src/poliastro/neos/__init__.py src/poliastro/neos/dastcom5.py src/poliastro/plotting/__init__.py src/poliastro/plotting/_base.py src/poliastro/plotting/core.py src/poliastro/plotting/misc.py src/poliastro/plotting/porkchop.py src/poliastro/plotting/static.py src/poliastro/plotting/util.py src/poliastro/tests/test_bodies.py src/poliastro/tests/test_coordinates.py src/poliastro/tests/test_czml.py src/poliastro/tests/test_examples.py src/poliastro/tests/test_frames.py src/poliastro/tests/test_hyper.py src/poliastro/tests/test_iod.py src/poliastro/tests/test_jit.py src/poliastro/tests/test_maneuver.py src/poliastro/tests/test_spheroid_location.py src/poliastro/tests/test_stumpff.py src/poliastro/tests/test_util.py src/poliastro/tests/tests_core/test_core_util.py src/poliastro/tests/tests_neos/center.html src/poliastro/tests/tests_neos/none.html src/poliastro/tests/tests_neos/table.html src/poliastro/tests/tests_neos/test_dastcom5.py src/poliastro/tests/tests_plotting/test_core.py src/poliastro/tests/tests_plotting/test_misc.py src/poliastro/tests/tests_plotting/test_porkchop.py src/poliastro/tests/tests_plotting/test_static.py src/poliastro/tests/tests_threebody/test_flybys.py src/poliastro/tests/tests_threebody/test_restricted.py src/poliastro/tests/tests_threebody/test_soi.py src/poliastro/tests/tests_twobody/test_angles.py src/poliastro/tests/tests_twobody/test_decorators.py src/poliastro/tests/tests_twobody/test_orbit.py src/poliastro/tests/tests_twobody/test_perturbations.py src/poliastro/tests/tests_twobody/test_propagation.py src/poliastro/tests/tests_twobody/test_states.py src/poliastro/tests/tests_twobody/test_thrust.py src/poliastro/threebody/__init__.py src/poliastro/threebody/flybys.py src/poliastro/threebody/restricted.py src/poliastro/threebody/soi.py src/poliastro/twobody/__init__.py src/poliastro/twobody/_states.py src/poliastro/twobody/angles.py src/poliastro/twobody/decorators.py src/poliastro/twobody/orbit.py src/poliastro/twobody/propagation.py src/poliastro/twobody/thrust/__init__.py src/poliastro/twobody/thrust/change_a_inc.py src/poliastro/twobody/thrust/change_argp.py src/poliastro/twobody/thrust/change_ecc_quasioptimal.py src/poliastro/twobody/thrust/change_inc_ecc.pypoliastro-0.13.1/src/poliastro.egg-info/dependency_links.txt000644 001750 001750 00000000001 13577132207 025020 0ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro.egg-info/entry_points.txt000644 001750 001750 00000000062 13577132207 024246 0ustar00juanlujuanlu000000 000000 [console_scripts] poliastro = poliastro.cli:main poliastro-0.13.1/src/poliastro.egg-info/not-zip-safe000644 001750 001750 00000000001 13577132140 023174 0ustar00juanlujuanlu000000 000000 poliastro-0.13.1/src/poliastro.egg-info/requires.txt000644 001750 001750 00000000760 13577132207 023355 0ustar00juanlujuanlu000000 000000 astropy~=3.2 astroquery>=0.3.9 jplephem matplotlib!=3.0.1,>=2.0 numpy pandas plotly~=4.0 scipy>=1.0 [:implementation_name == "cpython"] numba>=0.39 [cesium] czml3~=0.1.2 [dev] black coverage ipykernel ipython>=5.0 ipywidgets>=7.0 isort jupyter-client nbsphinx>=0.5.0 pycodestyle pytest>=3.2 pytest-cov<2.6.0 pytest-doctestplus pytest-mpl sphinx sphinx_rtd_theme@ https://github.com/Juanlu001/sphinx_rtd_theme/archive/avoid-require-redefinition.zip sphinx-notfound-page tox [jupyter] notebook poliastro-0.13.1/src/poliastro.egg-info/top_level.txt000644 001750 001750 00000000012 13577132207 023475 0ustar00juanlujuanlu000000 000000 poliastro