├── plyades ├── tests │ ├── __init__.py │ ├── test_state.py │ ├── test_util.py │ ├── test_core.py │ └── test_orbit.py ├── constants.py ├── config.py ├── __init__.py ├── examples.py ├── state.py ├── forces.py ├── propagator.py ├── ephemerides.py ├── visualization.py ├── util.py ├── frames.py ├── kepler.py ├── time.py ├── bodies.py ├── orbit.py └── core.py ├── requirements-docs.txt ├── requirements.txt ├── ext └── plyades_logo.key ├── docs ├── _static │ └── plyades_logo.jpg ├── api.rst ├── user.rst ├── index.rst ├── make.bat ├── Makefile └── conf.py ├── .travis.yml ├── setup.py ├── .gitignore ├── README.rst └── LICENSE /plyades/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | numpydoc 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | astropy 3 | jplephem 4 | -------------------------------------------------------------------------------- /ext/plyades_logo.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helgee/plyades/HEAD/ext/plyades_logo.key -------------------------------------------------------------------------------- /docs/_static/plyades_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helgee/plyades/HEAD/docs/_static/plyades_logo.jpg -------------------------------------------------------------------------------- /plyades/constants.py: -------------------------------------------------------------------------------- 1 | DELTA_JD2000 = 2451544.5 2 | DELTA_JD1950 = 2433282.5 3 | DELTA_MJD = 2400000.5 4 | -------------------------------------------------------------------------------- /plyades/tests/test_state.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import unittest 3 | import numpy as np 4 | import plyades as pl 5 | 6 | -------------------------------------------------------------------------------- /plyades/config.py: -------------------------------------------------------------------------------- 1 | from .ephemerides import AnalyticalEphemeris, NumericalEphemeris 2 | import astropy.units as u 3 | 4 | config = {'ephemeris': AnalyticalEphemeris()} 5 | 6 | def load_kernel(file, units=(u.km, u.km/u.s)): 7 | config['ephemeris'] = NumericalEphemeris(file, units) 8 | 9 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | High-level wrapper 4 | ================== 5 | 6 | .. automodule:: plyades.core 7 | :members: 8 | 9 | Low-level routines 10 | ================== 11 | 12 | Utility Functions 13 | ----------------- 14 | 15 | .. automodule:: plyades.util 16 | :members: 17 | -------------------------------------------------------------------------------- /plyades/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import State, Orbit 2 | import plyades.bodies 3 | import plyades.config 4 | import plyades.constants 5 | import plyades.ephemerides 6 | import plyades.examples 7 | import plyades.forces 8 | # import plyades.frames 9 | import plyades.orbit 10 | import plyades.util 11 | import astropy.units as units 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | before_install: 5 | - sudo apt-get install -qq python-numpy python-scipy python-matplotlib 6 | virtualenv: 7 | system_site_packages: true 8 | install: pip install -r requirements.txt --use-mirrors 9 | # command to run tests, e.g. python setup.py test 10 | script: nosetests 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name="Plyades", 6 | version="0.0.1", 7 | description="A Python Astrodynamics Library", 8 | author="Helge Eichhorn", 9 | author_email="mail@helgeeichhorn.de", 10 | url="https://github.com/helgee/plyades", 11 | packages=["plyades", "plyades.tests"], 12 | license="MIT License", 13 | ) 14 | -------------------------------------------------------------------------------- /plyades/examples.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from plyades.core import State 3 | from plyades.bodies import EARTH 4 | from astropy.time import Time 5 | import astropy.units as u 6 | 7 | iss_r = np.array([ 8 | 8.59072560e+02, 9 | -4.13720368e+03, 10 | 5.29556871e+03, 11 | ])*u.km 12 | iss_v = np.array([ 13 | 7.37289205e+00, 14 | 2.08223573e+00, 15 | 4.39999794e-01, 16 | ])*u.km/u.s 17 | iss_t = Time("2013-03-18T12:00:00.000") 18 | iss = State(iss_r, iss_v, iss_t) 19 | -------------------------------------------------------------------------------- /plyades/state.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | #import numpy as np 3 | #import util 4 | # from time import Epoch 5 | 6 | 7 | # class State: 8 | # def __init__(self, s, epoch=Epoch(2000, 1, 1, 0, 0, 0), frame="MEE2000"): 9 | # self.s = s 10 | # self.t = epoch 11 | # self.frame = frame 12 | # 13 | # @property 14 | # def r(self): 15 | # return self.s[0:3] 16 | # 17 | # @property 18 | # def v(self): 19 | # return self.s[3:6] 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | *.swp 37 | .DS_Store 38 | 39 | docs/_build/ 40 | .idea/ 41 | .ipynb_checkpoints/ 42 | *.ipynb 43 | *.bsp 44 | env/ 45 | venv/ 46 | -------------------------------------------------------------------------------- /plyades/forces.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def newton(f, t, y, params): 4 | r = np.linalg.norm(y[:3]) 5 | f[:3] += y[3:] 6 | f[3:] += -params['body'].mu*y[:3]/r**3 7 | 8 | def newton_j2(f, t, y, params): 9 | r = np.sqrt(np.square(y[:3]).sum()) 10 | mu = params['body'].mu.value 11 | j2 = params['body'].j2 12 | r_m = params['body'].mean_radius.value 13 | rx, ry, rz = y[:3] 14 | f[:3] += y[3:] 15 | pj = -3/2*mu*j2*r_m**2/r**5 16 | f[3] += -mu*rx/r**3 + pj*rx*(1-5*rz**2/r**2) 17 | f[4] += -mu*ry/r**3 + pj*ry*(1-5*rz**2/r**2) 18 | f[5] += -mu*rz/r**3 + pj*rz*(3-5*rz**2/r**2) 19 | -------------------------------------------------------------------------------- /plyades/tests/test_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import unittest 3 | import numpy as np 4 | import plyades as pl 5 | 6 | class UtilTest(unittest.TestCase): 7 | def test_dms(self): 8 | self.assertEqual(pl.util.dms2rad(1,0,0), np.radians(1)) 9 | self.assertEqual(pl.util.dms2rad(0,1,0), np.radians(1)/60) 10 | self.assertEqual(pl.util.dms2rad(0,0,1), np.radians(1)/3600) 11 | 12 | def test_hms(self): 13 | self.assertEqual(pl.util.hms2rad(1,0,0), np.radians(15)) 14 | self.assertEqual(pl.util.hms2rad(0,1,0), np.radians(15)/60) 15 | self.assertEqual(pl.util.hms2rad(0,0,1), np.radians(15)/3600) -------------------------------------------------------------------------------- /plyades/tests/test_core.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import unittest 3 | import datetime 4 | import numpy as np 5 | import plyades as pl 6 | 7 | 8 | class StateTest(unittest.TestCase): 9 | def setUp(self): 10 | self.state = pl.State(np.array([1000,1000,1000,1,1,1]),t=datetime.datetime(2000, 1, 1, 0, 0, 0)) 11 | 12 | def test_jd(self): 13 | self.assertEqual(self.state.jd, pl.const.epoch["jd2000"]) 14 | 15 | def test_mjd2000(self): 16 | self.assertEqual(self.state.jd2000, 0) 17 | 18 | def test_mjd1950(self): 19 | self.assertEqual(self.state.jd1950, abs(pl.const.epoch["jd1950"] - pl.const.epoch["jd2000"])) 20 | 21 | def test_mjd(self): 22 | self.assertEqual(self.state.mjd, abs(pl.const.epoch["mjd"] - pl.const.epoch["jd2000"])) 23 | -------------------------------------------------------------------------------- /docs/user.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Dependencies 5 | ------------ 6 | Plyades depends on the following third-party libraries: 7 | * Astropy 8 | 9 | Getting started 10 | =============== 11 | 12 | Create an Epoch object 13 | ---------------------- 14 | 15 | Create a State object 16 | --------------------- 17 | 18 | Transform to a different reference frame 19 | ---------------------------------------- 20 | 21 | Create an Orbit object 22 | ------------------------ 23 | 24 | Propagate by solving Kepler's equation 25 | -------------------------------------- 26 | 27 | Reference frame transformations 28 | =============================== 29 | 30 | Orbit propagation 31 | ================= 32 | 33 | Semi-analytical solver 34 | ---------------------- 35 | 36 | Numerical solver 37 | ---------------- 38 | 39 | Force model 40 | ^^^^^^^^^^^ 41 | 42 | Visualization 43 | ============= 44 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **DEPRECATED** 2 | 3 | - For a spiritual successor in Python have a look at `poliastro `_. 4 | - If you are interested in Julia have a look at `Astrodynamics.jl `_. 5 | 6 | Plyades 7 | ======= 8 | 9 | .. image:: https://travis-ci.org/helgee/plyades.png 10 | :target: https://travis-ci.org/helgee/plyades 11 | 12 | Plyades is an MIT-licensed astrodynamics library written in Python. 13 | It aims to provide a comprehensive toolset for fast development of 14 | high-performance mission analysis applications. 15 | The API provides powerful high-level objects for pythonic ease-of-use while the 16 | low-level functional building blocks can also be used independently. 17 | 18 | .. code-block:: python 19 | 20 | import plyades as pl 21 | 22 | state = pl.examples.iss 23 | 24 | p = pl.Orbit(state) 25 | p.propagate(revolutions=1) 26 | p.plot() 27 | 28 | 29 | Documentation 30 | ------------- 31 | Read the documentation at `http://plyades.readthedocs.org `_. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Helge Eichhorn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the 'Software'), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Plyades: A Python Astrodynamics Library 2 | ======================================= 3 | 4 | Plyades is an MIT-licensed astrodynamics library written in Python. 5 | It aims to provide a comprehensive toolset for fast development of performant 6 | mission analysis applications. 7 | The API provides powerful high-level objects for pythonic ease-of-use while the 8 | low-level functional building blocks can also be used independently. 9 | 10 | .. warning:: 11 | This library is currently a proof of concept and has not been 12 | validated or used within an operational context. As soon as this changes 13 | the validation results will be documented here. 14 | 15 | Features 16 | -------- 17 | .. - High-level objects representing the state of a spacecraft 18 | - Reference frame transformations 19 | - Numerical orbit propagation 20 | 21 | User Guide 22 | ---------- 23 | 24 | .. toctree:: 25 | user 26 | 27 | API Documentation 28 | ----------------- 29 | 30 | .. toctree:: 31 | api 32 | 33 | 34 | .. Indices and tables 35 | .. ================== 36 | .. 37 | .. * :ref:`genindex` 38 | .. * :ref:`modindex` 39 | .. * :ref:`search` 40 | .. 41 | -------------------------------------------------------------------------------- /plyades/propagator.py: -------------------------------------------------------------------------------- 1 | from astropy.time import TimeDelta 2 | import numpy as np 3 | from scipy.integrate import ode 4 | import warnings 5 | 6 | class Propagator: 7 | def __init__(self, s0, dt, **kwargs): 8 | self.s0 = s0 9 | self.dt = dt 10 | self.forces = [] 11 | self.params = {'body': s0.body, 'frame': s0.frame} 12 | self.solver = ode(self._rhs).set_integrator('dop853', nsteps=1, **kwargs) 13 | self.solver.set_initial_value(np.copy(s0), 0.0) 14 | self.solver.set_f_params(self.params) 15 | self.solver._integrator.iwork[2] = -1 16 | 17 | def _rhs(self, t, y, params): 18 | f = np.zeros_like(y) 19 | # s = type(self.s0)( 20 | # y[:3], y[3:], self.s0.t + TimeDelta(t, format='sec'), 21 | # frame=self.s0.frame, 22 | # body=self.s0.body, 23 | # vars=y[6:], 24 | # ) 25 | for fn in self.forces: 26 | fn(f, t, y, params) 27 | return f 28 | 29 | def step(self): 30 | warnings.filterwarnings("ignore", category=UserWarning) 31 | self.solver.integrate(self.dt, step=True) 32 | warnings.resetwarnings() 33 | return self.solver.t, self.solver.y 34 | 35 | def __iter__(self): 36 | while not np.isclose(self.solver.t, self.dt): 37 | yield self.step() 38 | -------------------------------------------------------------------------------- /plyades/ephemerides.py: -------------------------------------------------------------------------------- 1 | from jplephem.spk import SPK 2 | import networkx as nx 3 | import numpy as np 4 | 5 | class AnalyticalEphemeris: 6 | def __str__(self): 7 | return "Analytical Ephemeris." 8 | def rv(self, id, tdb, tdb2=0.0): 9 | raise NotImplementedError 10 | 11 | 12 | class NumericalEphemeris: 13 | def __init__(self, spk, units): 14 | self.kernel = SPK.open(spk) 15 | self.r_unit = units[0] 16 | self.v_unit = units[1] 17 | self.graph = nx.Graph() 18 | for edge in self.kernel.pairs: 19 | self.graph.add_edge(*edge) 20 | self.paths = nx.shortest_path(self.graph) 21 | def __str__(self): 22 | return str(self.kernel) 23 | def rv(self, id, tdb, tdb2=0.0): 24 | if id not in self.graph: 25 | raise ValueError("Unknown body ID: {}".format(id)) 26 | path = self.paths[0][id] 27 | if len(path) == 2: 28 | segment = self.kernel[0, id] 29 | r, v = segment.compute_and_differentiate(tdb, tdb2) 30 | else: 31 | r = np.zeros(3) 32 | v = np.zeros(3) 33 | for i1, i2 in zip(path, path[1:]): 34 | segment = self.kernel[i1, i2] 35 | rs, vs = segment.compute_and_differentiate(tdb, tdb2) 36 | r += rs 37 | v += vs 38 | return r * self.r_unit, v * self.v_unit 39 | -------------------------------------------------------------------------------- /plyades/tests/test_orbit.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import unittest 3 | import numpy as np 4 | import plyades as pl 5 | 6 | 7 | values = {"vector": np.array([6524.834, 6862.875, 6448.296, 4.901327, 5.533756, -1.976341]), 8 | "elements": np.array([36127.343, 0.832853, np.radians(87.870), np.radians(227.89), 9 | np.radians(53.38), np.radians(92.335)])} 10 | mu = 398600.4415 11 | 12 | 13 | class OrbitTest(unittest.TestCase): 14 | def test_elements(self): 15 | elements = pl.orbit.elements(values["vector"], mu) 16 | for i in range(len(elements)): 17 | self.assertAlmostEqual(elements[i], values["elements"][i], delta=1e-2) 18 | 19 | def test_elements_vectorized(self): 20 | elements = pl.orbit.elements(np.vstack((values["vector"], values["vector"])), mu) 21 | for el in elements: 22 | for i in range(len(el)): 23 | self.assertAlmostEqual(el[i], values["elements"][i], delta=1e-2) 24 | 25 | # The input values seem to be inaccurate. As the results have been cross-checked. 26 | # TODO: Find better input values. 27 | def test_vector(self): 28 | vector = pl.orbit.vector(values["elements"], mu) 29 | for i in range(len(vector)): 30 | self.assertAlmostEqual(vector[i], values["vector"][i], delta=2) 31 | 32 | def test_vector_vectorized(self): 33 | vector = pl.orbit.vector(np.vstack((values["elements"], values["elements"])), mu) 34 | for v in vector: 35 | for i in range(len(v)): 36 | self.assertAlmostEqual(v[i], values["vector"][i], delta=2) 37 | 38 | def test_vec2el2vec(self): 39 | elements = pl.orbit.elements(np.vstack((values["vector"], values["vector"])), mu) 40 | vector1 = pl.orbit.vector(np.vstack((elements, elements)),mu) 41 | for v in vector1: 42 | for i in range(len(v)): 43 | self.assertAlmostEqual(v[i], values["vector"][i]) 44 | -------------------------------------------------------------------------------- /plyades/visualization.py: -------------------------------------------------------------------------------- 1 | from bokeh.io import vplot 2 | from bokeh.models import Range 3 | from bokeh.plotting import figure, show 4 | import matplotlib.pyplot as plt 5 | from mpl_toolkits.mplot3d import Axes3D 6 | import numpy as np 7 | 8 | def plot_plane(orb, plane='XY', show_steps=True, show_plot=True, width=500, height=500): 9 | r = orb.s0.body.mean_radius.value 10 | x, y, z = np.array(orb.rx), np.array(orb.ry), np.array(orb.rz) 11 | unit = orb.s0.r.unit 12 | x_label, y_label = plane[0], plane[1] 13 | if plane == 'XY': 14 | x, y, z = x, y, z 15 | x0 = orb.s0.r[0].value 16 | y0 = orb.s0.r[1].value 17 | if orb.interpolate: 18 | xs = orb._states[0,:] 19 | ys = orb._states[1,:] 20 | elif plane == 'XZ': 21 | x, y, z = x, z, y 22 | x0 = orb.s0.r[0].value 23 | y0 = orb.s0.r[2].value 24 | if orb.interpolate: 25 | xs = orb._states[0,:] 26 | ys = orb._states[2,:] 27 | elif plane == 'YZ': 28 | x, y, z = y, z, x 29 | x0 = orb.s0.r[1].value 30 | y0 = orb.s0.r[2].value 31 | if orb.interpolate: 32 | xs = orb._states[1,:] 33 | ys = orb._states[2,:] 34 | 35 | magnitudes = np.sqrt(np.square(x)+np.square(y)) 36 | limit = np.maximum(r, magnitudes.max()) * 1.2 37 | f = figure( 38 | height = height, 39 | width = width, 40 | title = plane, 41 | x_range = (-limit, limit), 42 | y_range = (-limit, limit), 43 | x_axis_label = "{} [{}]".format(x_label, unit), 44 | y_axis_label = "{} [{}]".format(y_label, unit), 45 | ) 46 | ind = (magnitudes < r) & (z < 0) 47 | nan = float('nan') 48 | x_bg = x.copy() 49 | y_bg = y.copy() 50 | x_fg = x.copy() 51 | y_fg = y.copy() 52 | x_bg[~ind] = nan 53 | y_bg[~ind] = nan 54 | x_fg[ind] = nan 55 | y_fg[ind] = nan 56 | f.circle(x=0, y=0, radius=r, alpha=0.5) 57 | f.line(x_fg, y_fg, line_width=2, color='blue') 58 | f.circle(x_bg, y_bg, size=2, color='darkblue') 59 | if orb.interpolate and show_steps: 60 | f.cross(x=xs[1:-1], y=ys[1:-1], size=15, line_width=3, color='darkblue') 61 | f.cross(x=x0, y=y0, size=15, line_width=3, color='red') 62 | if orb.interpolate: 63 | f.x(x=x[-1], y=y[-1], size=12, line_width=3, color='red') 64 | if show_plot: 65 | show(f) 66 | else: 67 | return f 68 | 69 | 70 | def plot3d(orb, show_plot=True): 71 | fig = plt.figure("Plyades Plot", figsize=(8, 8)) 72 | ax = fig.add_subplot(111, projection='3d') 73 | orb.s0.body.plot3d(ax) 74 | ax.plot(orb.rx, orb.ry, zs=orb.rz, color="r", linewidth=3) 75 | unit = orb.s0.r.unit 76 | ax.set_xlabel('x [{}]'.format(unit)) 77 | ax.set_ylabel('y [{}]'.format(unit)) 78 | ax.set_zlabel('z [{}]'.format(unit)) 79 | 80 | if show_plot: 81 | plt.show() 82 | else: 83 | return ax 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /plyades/util.py: -------------------------------------------------------------------------------- 1 | """`plyades.util` contains mathematical and miscellaneous utility functions. 2 | """ 3 | from __future__ import division 4 | import re 5 | import numpy as np 6 | import astropy.units as u 7 | 8 | def istime(t): 9 | try: 10 | t.jd 11 | except AttributeError: 12 | return False 13 | else: 14 | return True 15 | 16 | def nearest_idx(array, value): 17 | return (np.abs(array-value)).argmin() 18 | 19 | def rot(angle, axis=3): 20 | ''' 21 | 22 | ''' 23 | M = np.zeros((3,3)) 24 | if axis == 1: 25 | M[0,0] = 1 26 | M[1,1] = np.cos(angle) 27 | M[1,2] = np.sin(angle) 28 | M[2,1] = -np.sin(angle) 29 | M[2,2] = np.cos(angle) 30 | return M 31 | elif axis == 2: 32 | M[0,0] = np.cos(angle) 33 | M[0,2] = -np.sin(angle) 34 | M[1,1] = 1 35 | M[2,0] = np.sin(angle) 36 | M[2,2] = np.cos(angle) 37 | return M 38 | elif axis == 3: 39 | M[0,0] = np.cos(angle) 40 | M[0,1] = np.sin(angle) 41 | M[1,0] = -np.sin(angle) 42 | M[1,1] = np.cos(angle) 43 | M[2,2] = 1 44 | return M 45 | 46 | def rotd(angle, angular_velocity, axis=3): 47 | M = np.zeros((3,3)) 48 | if axis == 1: 49 | M[1,1] = -angular_velocity * np.sin(angle) 50 | M[1,2] = angular_velocity * np.cos(angle) 51 | M[2,1] = -angular_velocity * np.cos(angle) 52 | M[2,2] = -angular_velocity * np.sin(angle) 53 | return M 54 | elif axis == 2: 55 | M[0,0] = -angular_velocity * np.sin(angle) 56 | M[0,2] = -angular_velocity * np.cos(angle) 57 | M[2,0] = angular_velocity * np.cos(angle) 58 | M[2,2] = -angular_velocity * np.sin(angle) 59 | return M 60 | elif axis == 3: 61 | M[0,0] = -angular_velocity * np.sin(angle) 62 | M[0,1] = angular_velocity * np.cos(angle) 63 | M[1,0] = -angular_velocity * np.cos(angle) 64 | M[1,1] = -angular_velocity * np.sin(angle) 65 | return M 66 | 67 | def euler(alpha, beta, gamma, order="321"): 68 | 69 | # Input checking. 70 | reg = re.compile("[1-3][1-3][1-3]") 71 | if not reg.match(order) and len(order) > 3: 72 | raise ValueError("Incorrect rotation order definition.") 73 | 74 | m_alpha = rot(alpha, axis=int(order[0])) 75 | m_beta = rot(beta, axis=int(order[1])) 76 | m_gamma = rot(gamma, axis=int(order[2])) 77 | return np.dot(m_alpha, np.dot(m_beta, m_gamma)) 78 | 79 | def dms2rad(degrees, minutes, seconds): 80 | return (degrees + minutes/60 + seconds/3600) * np.pi/180 81 | 82 | def hms2rad(degrees, minutes, seconds): 83 | return (degrees + minutes/60 + seconds/3600) * 15 * np.pi/180 84 | 85 | def mag(a): 86 | """Vectorized magnitude calculation for an array of vectors. 87 | 88 | Parameters 89 | ---------- 90 | a: numpy.ndarray 91 | An mxn-array containing m vectors with n elements. 92 | 93 | Returns 94 | ------- 95 | m: numpy.ndarray 96 | An mx1-array containing the magnitude for each vector. 97 | """ 98 | return np.sqrt(np.square(a).sum(axis=1, keepdims=True)) 99 | 100 | def dot(a, b): 101 | """Vectorized dot product for arrays of vectors. 102 | 103 | Parameters 104 | ---------- 105 | a: numpy.ndarray 106 | An mxn-array containing m vectors with n elements. 107 | b: numpy.ndarray 108 | An mxn-array containing m vectors with n elements. 109 | 110 | 111 | Returns 112 | ------- 113 | m: numpy.ndarray 114 | An mx1-array containing of a*b for each vector pair. 115 | """ 116 | return (a * b).sum(axis=1, keepdims=True) 117 | 118 | def getunit(val): 119 | return getattr(val, "unit", None) 120 | 121 | def cross(a, b): 122 | unit_a = getunit(a) 123 | unit_b = getunit(b) 124 | if not unit_a and not unit_b: 125 | return np.cross(a, b) 126 | else: 127 | if not unit_a: 128 | unit_a = u.dimensionless_unscaled 129 | if not unit_b: 130 | unit_b = u.dimensionless_unscaled 131 | return np.cross(a, b)*unit_a*unit_b 132 | 133 | 134 | def mod2pi(val): 135 | unit = getunit(val) 136 | if not unit: 137 | return np.mod(val, 2*np.pi) 138 | else: 139 | return np.mod(val, 2*np.pi*unit) 140 | -------------------------------------------------------------------------------- /plyades/frames.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import numpy as np 3 | import plyades.util as util 4 | 5 | def precession(date): 6 | ''' Vallado 2nd Edition p.215 7 | ''' 8 | # Julian centuries since the epoch 9 | t = (date - 2451545)/36525 10 | zeta = util.dms2rad(0, 0, 2306.2181*t + 0.30188*t**2 + 0.017998*t**3) 11 | theta = util.dms2rad(0, 0, 2004.3109*t - 0.42665*t**2 - 0.041833*t**3) 12 | z = util.dms2rad(0, 0, 2306.2181*t + 1.09468*t**2 + 0.018203*t**3) 13 | # Determine rotational matrices. 14 | M1 = util.rot(-z, axis=3) 15 | M2 = util.rot(theta, axis=2) 16 | M3 = util.rot(-zeta, axis=3) 17 | return np.dot(M1, np.dot(M2, M3)) 18 | 19 | # from scipy import optimize 20 | # 21 | # pvecle = aux.pvecle 22 | # pelvec = aux.pelvec 23 | # 24 | # 25 | # def eci2ecef(s, theta, omega): 26 | # M = np.zeros((6, 6)) 27 | # M[0, 0] = np.cos(theta) 28 | # M[0, 1] = np.sin(theta) 29 | # M[1, 0] = -np.sin(theta) 30 | # M[1, 1] = np.cos(theta) 31 | # M[2, 2] = 1 32 | # M[3, 0] = -omega * np.sin(theta) 33 | # M[3, 1] = omega * np.cos(theta) 34 | # M[4, 0] = -omega * np.cos(theta) 35 | # M[4, 1] = -omega * np.sin(theta) 36 | # M[3, 3] = M[0, 0] 37 | # M[3, 4] = M[0, 1] 38 | # M[4, 3] = M[1, 0] 39 | # M[4, 4] = M[1, 1] 40 | # M[5, 5] = M[2, 2] 41 | # 42 | # st = np.dot(M, s) 43 | # return st, M 44 | # 45 | # def ecef2eci(s, theta, omega): 46 | # M = np.zeros((3, 3)) 47 | # N = np.zeros((3, 3)) 48 | # st = np.zeros(6) 49 | # M[0, 0] = np.cos(theta) 50 | # M[0, 1] = np.sin(theta) 51 | # M[1, 0] = -np.sin(theta) 52 | # M[1, 1] = np.cos(theta) 53 | # M[2, 2] = 1 54 | # N[0, 0] = -omega * np.sin(theta) 55 | # N[0, 1] = omega * np.cos(theta) 56 | # N[1, 0] = -omega * np.cos(theta) 57 | # N[1, 1] = -omega * np.sin(theta) 58 | # 59 | # st[0:3] = np.dot(M.T, s[0:3]) 60 | # st[3:6] = np.dot(M.T, s[3:6]) + np.dot(N.T, s[0:3]) 61 | # return st, M.T 62 | # 63 | # 64 | # 65 | # def lla2ecef(lat, lon, alt): 66 | # e = np.sqrt(2 * earth.f - earth.f ** 2) 67 | # c = earth.r_e / np.sqrt(1 - e ** 2 * np.sin(lat) ** 2) 68 | # s = c * (1 - e ** 2) 69 | # r_delta = (c + alt) * np.cos(lat) 70 | # r_k = (s + alt) * np.sin(lat) 71 | # return np.array([r_delta * np.cos(lon), r_delta * np.sin(lon), r_k]) 72 | # 73 | # 74 | # def ecef2lla(s, tol=1e-10): 75 | # x, y, z = s[0:3] 76 | # r = np.sqrt(x ** 2 + y ** 2 + z ** 2) 77 | # r_delta = np.sqrt(x ** 2 + y ** 2) 78 | # lon = np.arctan2(y, x) 79 | # 80 | # if abs(lon) >= np.pi: 81 | # if lon < 0: 82 | # lon = 2 * np.pi + lon 83 | # else: 84 | # lon = lon - 2 * np.pi 85 | # 86 | # delta = np.arcsin(z / r) 87 | # 88 | # def latitude(lat): 89 | # e = np.sqrt(2 * earth.f - earth.f ** 2) 90 | # c = earth.r_e / np.sqrt(1 - e ** 2 * np.sin(lat) ** 2) 91 | # return (z + c * e ** 2 * np.sin(lat)) / r_delta - np.tan(lat) 92 | # 93 | # lat = optimize.newton(latitude, delta, tol=tol) 94 | # 95 | # e = np.sqrt(2 * earth.f - earth.f ** 2) 96 | # c = earth.r_e / np.sqrt(1 - e ** 2 * np.sin(lat) ** 2) 97 | # alt = r_delta / np.cos(lat) - c 98 | # 99 | # return np.array([lat, lon, alt]) 100 | # 101 | # 102 | # def ecef2sez(s, site=None, lat=None, lon=None, alt=None): 103 | # if not site == None: 104 | # ecef = site 105 | # lat, lon, alt = ecef2lla(ecef) 106 | # elif not lat == None or lon == None or alt == None: 107 | # ecef = lla2ecef(lat, lon, alt) 108 | # else: 109 | # raise SyntaxError("""Site location must be specified in 110 | # either ECEF format or lat/lon/alt!""") 111 | # 112 | # rho_ecef = s[0:3] - ecef 113 | # rhod_ecef = s[3:6] 114 | # 115 | # M = np.zeros((6, 6)) 116 | # M[0, 0] = np.sin(lat) * np.cos(lon) 117 | # M[0, 1] = np.sin(lat) * np.sin(lon) 118 | # M[0, 2] = -np.cos(lat) 119 | # M[1, 0] = -np.sin(lon) 120 | # M[1, 1] = np.cos(lon) 121 | # M[2, 0] = np.cos(lat) * np.cos(lon) 122 | # M[2, 1] = np.cos(lat) * np.sin(lon) 123 | # M[2, 2] = np.sin(lat) 124 | # M[3:6, 3:6] = M[0:3, 0:3] 125 | # 126 | # return np.dot(M, np.append(rho_ecef, rhod_ecef)), M 127 | # 128 | # 129 | # def sez2razel(s): 130 | # ran = np.sqrt(np.dot(s[0:3], s[0:3])) 131 | # rrt = np.dot(s[0:3], s[3:6]) / ran 132 | # el = np.arcsin(s[2] / ran) 133 | # az = np.arctan2(s[1], -s[0]) 134 | # 135 | # return ran, rrt, az, el 136 | # 137 | -------------------------------------------------------------------------------- /plyades/kepler.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | from scipy import optimize 3 | import numpy as np 4 | import plyades.util as util 5 | import astropy.units as units 6 | 7 | 8 | def elements(mu, r, v): 9 | r = np.atleast_2d(r) 10 | v = np.atleast_2d(v) 11 | r_mag = util.mag(r) 12 | v_mag = util.mag(v) 13 | h = util.cross(r, v) 14 | h_mag = util.mag(h) 15 | k = np.array([[0, 0, 1]]).repeat(r.shape[0], axis=0) 16 | n = util.cross(k, h) 17 | n_mag = util.mag(n) 18 | xi = v_mag ** 2 / 2 - mu / r_mag 19 | e = ((v_mag ** 2 - mu / r_mag) * r - v * util.dot(r, v)) / mu 20 | ecc = util.mag(e) 21 | if not (ecc == 1).any(): 22 | sma = - mu / (2 * xi) 23 | p = sma * (1 - ecc ** 2) 24 | else: 25 | p = h_mag ** 2 / mu 26 | sma = p 27 | inc = np.arccos(h[:, 2, np.newaxis] / h_mag) 28 | # node = np.arccos(n[:, 0, np.newaxis] / n_mag) 29 | node = np.arctan2(n[:, 1, np.newaxis]/h_mag, n[:, 0, np.newaxis]/h_mag) 30 | peri = np.arccos(util.dot(n, e) / (ecc * n_mag)) 31 | ano = np.arccos(util.dot(e, r) / (ecc * r_mag)) 32 | # Quadrant checks 33 | node = util.mod2pi(node) 34 | peri = util.mod2pi(peri) 35 | ano = util.mod2pi(ano) 36 | return ( 37 | sma.squeeze(), ecc.squeeze(), inc.squeeze(), 38 | node.squeeze(), peri.squeeze(), ano.squeeze()) 39 | 40 | 41 | def print_elements(ele): 42 | names = ["Semi-major axis:", "Eccentricity:", "Inclination:", 43 | "Ascending node:", "Argument of perigee:", 44 | "True anomaly:"] 45 | for name, element in zip(names[:2], ele[:2]): 46 | print("{:<26}{:>16.5f}".format(name, element)) 47 | for name, element in zip(names[2:], ele[2:]): 48 | print("{:<26}{:>16.5f}".format(name, np.degrees(element))) 49 | 50 | 51 | def cartesian(mu, sma, ecc, inc, node, peri, ano): 52 | u = peri + ano 53 | 54 | p = sma * (1 - np.square(ecc)) 55 | e_ix = ecc == 1 56 | if e_ix.any(): 57 | p[e_ix] = sma[e_ix] 58 | 59 | r = p / (1 + ecc * np.cos(ano)) 60 | x = r*(np.cos(node)*np.cos(u) - np.sin(node)*np.cos(inc)*np.sin(u)) 61 | y = r*(np.sin(node)*np.cos(u) + np.cos(node)*np.cos(inc)*np.sin(u)) 62 | z = r*np.sin(inc)*np.sin(u) 63 | vr = np.sqrt(mu/p)*ecc*np.sin(ano) 64 | vf = np.sqrt(mu*p)/r 65 | vx = ( 66 | vr*(np.cos(node)*np.cos(u) - np.sin(node)*np.cos(inc)*np.sin(u)) - 67 | vf*(np.cos(node)*np.sin(u) + np.sin(node)*np.cos(u)*np.cos(inc))) 68 | vy = ( 69 | vr*(np.sin(node)*np.cos(u) + np.cos(node)*np.cos(inc)*np.sin(u)) - 70 | vf*(np.sin(node)*np.sin(u) - np.cos(node)*np.cos(u)*np.cos(inc))) 71 | vz = vr*np.sin(inc)*np.sin(u) + vf*np.cos(u)*np.sin(inc) 72 | return ( 73 | x.squeeze(), y.squeeze(), z.squeeze(), 74 | vx.squeeze(), vy.squeeze(), vz.squeeze()) 75 | 76 | 77 | def period(a, mu): 78 | return np.sqrt(4 * a**3 * np.pi**2 / mu) 79 | 80 | 81 | def orbital_energy(a, mu): 82 | return -mu/(2*a) 83 | 84 | 85 | def ecc_to_true(E, e): 86 | return 2*np.arctan2(np.sqrt(1 + e)*np.sin(E/2), np.sqrt(1 - e)*np.cos(E/2)) 87 | 88 | 89 | def true_to_ecc(T, e): 90 | return 2*np.arctan2(np.sqrt(1 - e)*np.sin(T/2), np.sqrt(1 + e)*np.cos(T/2)) 91 | 92 | 93 | def ecc_to_mean(E, e): 94 | unit = getattr(E, 'unit', None) 95 | if not unit: 96 | return E - e*np.sin(E) 97 | else: 98 | return (E.value - e*np.sin(E))*unit 99 | 100 | 101 | def mean_to_ecc(M, e): 102 | unit = getattr(M, 'unit', None) 103 | 104 | if unit: 105 | M = M.value 106 | e = e.value 107 | 108 | def kepler_eq(E): 109 | return E - e*np.sin(E) - M 110 | 111 | def kepler_eq_der(E): 112 | return 1 - e*np.cos(E) 113 | 114 | if unit: 115 | return optimize.newton( 116 | kepler_eq, M, kepler_eq_der, args=(), tol=1e-10, maxiter=50)*unit 117 | else: 118 | return optimize.newton( 119 | kepler_eq, M, kepler_eq_der, args=(), tol=1e-10, maxiter=50) 120 | 121 | 122 | def true_to_mean(T, e): 123 | return ecc_to_mean(true_to_ecc(T, e), e) 124 | 125 | 126 | def mean_to_true(M, e): 127 | return ecc_to_true(mean_to_ecc(M, e), e) 128 | 129 | 130 | def kepler(ele, dt, mu): 131 | E0 = true2ecc(ele[5], ele[1]) 132 | M0 = ecc2mean(E0, ele[1]) 133 | n = 2*np.pi/period(ele[0], mu) 134 | M = M0 + n*dt 135 | if not np.isscalar(M): 136 | E = np.zeros(np.shape(M)) 137 | out = np.zeros((len(M), 6)) 138 | for i, m in enumerate(M): 139 | E[i] = mean2ecc(m, ele[1]) 140 | else: 141 | out = np.zeros((1, 6)) 142 | E = mean2ecc(M, ele[1]) 143 | T = ecc2true(E, ele[1]) 144 | out[:, 0:5] = ele[0:5] 145 | out[:, 5] = T 146 | if out.shape == (6, ): 147 | return out.flatten() 148 | else: 149 | return out 150 | -------------------------------------------------------------------------------- /plyades/time.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import datetime 3 | # from collections import namedtuple 4 | import numpy as np 5 | from plyades.constants import JD2000 6 | 7 | def julian_centuries(jd, base=2451545.0): 8 | return (jd - base)/36525.0 9 | 10 | 11 | def sidereal(jd): 12 | jd = np.atleast_1d(jd) 13 | t_ut1 = (jd - 2451545)/36525 14 | sidereal = ( 15 | 67310.54841 + 16 | (876600*3600 + 8640184.812866)*t_ut1 + 17 | .093104*t_ut1**2 - 6.2e-6*t_ut1**3) 18 | sidereal = np.remainder(np.radians(sidereal/240), 2*np.pi) 19 | if len(sidereal) == 1: 20 | return np.asscalar(sidereal) 21 | else: 22 | return sidereal 23 | 24 | 25 | def calendar_jd(year, month, day, hour=0, minute=0, second=0, microsecond=0): 26 | year = np.atleast_1d(year) 27 | month = np.atleast_1d(month) 28 | day = np.atleast_1d(day) 29 | hour = np.atleast_1d(hour) 30 | minute = np.atleast_1d(minute) 31 | second = np.atleast_1d(second) 32 | microsecond = np.atleast_1d(microsecond) 33 | jd = ( 34 | 367 * year - 35 | np.floor((7 * (year + np.floor((month + 9) / 12.0))) * 0.25) + 36 | np.floor(275 * month / 9) + 37 | day + 1721013.5 + 38 | (((microsecond / 1e6 + second) / 60 + minute) / 60 + hour) / 24) 39 | if len(jd) == 1: 40 | return np.asscalar(jd) 41 | else: 42 | return jd 43 | 44 | 45 | def datetime_jd(dt): 46 | try: 47 | years = [d.year for d in dt] 48 | months = [d.month for d in dt] 49 | days = [d.day for d in dt] 50 | hours = [d.hour for d in dt] 51 | minutes = [d.minute for d in dt] 52 | seconds = [d.second for d in dt] 53 | microseconds = [d.microsecond for d in dt] 54 | except TypeError: 55 | years = dt.year 56 | months = dt.month 57 | days = dt.day 58 | hours = dt.hour 59 | minutes = dt.minute 60 | seconds = dt.second 61 | microseconds = dt.microsecond 62 | return calendar_jd( 63 | years, months, days, hours, minutes, seconds, microseconds) 64 | 65 | 66 | def jd_calendar(jd): 67 | jd = np.atleast_1d(jd) + .5 68 | z = np.trunc(jd) 69 | f = jd - z 70 | return f, z 71 | z_smaller = z < 2299161 72 | z_greater = z > 2299161 73 | a = np.zeros(jd.shape) 74 | alpha = np.zeros(jd.shape) 75 | if np.any(z_smaller): 76 | a[z_smaller] = z[z_smaller] 77 | if np.any(z_greater): 78 | alpha[z_greater] = np.trunc((z[z_greater] - 1867216.25)/36524.25) 79 | a[z_greater] = z[z_greater] + 1 + alpha - np.trunc(alpha/4) 80 | b = a + 1524 81 | c = np.trunc((b - 122.1)/365.25) 82 | d = np.trunc(365.25*c) 83 | e = np.trunc((b - d)/30.6001) 84 | day = np.floor(b - d - np.trunc(30.6001*e) + f) 85 | day = day.astype(np.int64, copy=False) 86 | e_smaller = e < 14 87 | e_greater = e > 14 88 | month = np.zeros(jd.shape, dtype=np.int64) 89 | if np.any(e_smaller): 90 | month[e_smaller] = e[e_smaller] - 1 91 | if np.any(e_greater): 92 | month[e_greater] = e[e_greater] - 13 93 | month_smaller = month > 2 94 | month_greater = month < 2 95 | year = np.zeros(jd.shape, dtype=np.int64) 96 | if np.any(month_smaller): 97 | year[month_smaller] = c[month_smaller] - 4716 98 | if np.any(month_greater): 99 | year[month_greater] = c[month_greater] - 4715 100 | hours = f*24 101 | hour = np.floor(hours).astype(np.int64, copy=False) 102 | minutes = (hours - hour) * 60 103 | minute = np.floor(minutes).astype(np.int64, copy=False) 104 | seconds = (minutes - minute) * 60 105 | second = np.floor(seconds).astype(np.int64, copy=False) 106 | microsecond = ((seconds - second) * 1e6).astype(np.int64, copy=False) 107 | # t = f*86400 108 | # hmod = np.mod(t, 3600) 109 | # second = np.mod(hmod, 60) 110 | # hour = np.trunc((t - hmod)/3600) 111 | # hour = hour.astype(np.int64, copy=False) 112 | # minute = np.trunc((hmod - second)/60) 113 | # minute = minute.astype(np.int64, copy=False) 114 | if len(year) == 1: 115 | return (np.asscalar(year), 116 | np.asscalar(month), 117 | np.asscalar(day), 118 | np.asscalar(hour), 119 | np.asscalar(minute), 120 | np.asscalar(second), 121 | np.asscalar(microsecond), 122 | ) 123 | else: 124 | return year, month, day, hour, minute, second, microsecond 125 | 126 | 127 | def jd_datetime(jd): 128 | jd = np.atleast_1d(jd) 129 | cal = jd_calendar(jd) 130 | try: 131 | return [ 132 | datetime.datetime( 133 | year, month, day, hour, minute, second, microsecond) 134 | for year, month, day, hour, minute, second, microsecond in cal] 135 | except TypeError: 136 | return datetime.datetime(*cal) 137 | -------------------------------------------------------------------------------- /plyades/bodies.py: -------------------------------------------------------------------------------- 1 | import astropy.units as u 2 | import matplotlib.pyplot as plt 3 | from mpl_toolkits.mplot3d import Axes3D 4 | import numpy as np 5 | from plyades.config import config 6 | 7 | 8 | # Values taken from: 9 | # Archinal, et al. "Report of the IAU working group on cartographic 10 | # coordinates and rotational elements: 2009." Celestial Mechanics 11 | # and Dynamical Astronomy 109.2 (2011): 101-135. 12 | # 13 | # Archinal, et al. "Erratum to: Reports of the IAU Working Group on 14 | # Cartographic Coordinates and Rotational Elements: 2006 & 2009." 15 | # Celestial Mechanics and Dynamical Astronomy 110.4 (2011): 401-403. 16 | # 17 | # Vallado, David A., and Wayne D. McClain. 18 | # Fundamentals of astrodynamics and applications. 19 | # Fourth Edition. Springer Science & Business Media, 2013. 20 | 21 | # symbols = { 22 | # "Sun": u"\u2609", "Mercury": u"\u263F", "Venus": u"\u2640", 23 | # "Earth": u"\u2641", "Mars": u"\u2642", "Jupiter": u"\u2643", 24 | # "Saturn": u"\u2644", "Uranus": u"\u26E2", "Neptune": u"\u2646", 25 | # "Moon": u"\u263E", "Pluto": u"\u2647"} 26 | 27 | 28 | class Planet: 29 | def __init__( 30 | self, 31 | name, 32 | mu, 33 | mean_radius, 34 | equatorial_radius, 35 | polar_radius, 36 | j2, 37 | jpl_id, 38 | symbol, 39 | # rotational_elements, 40 | ): 41 | self.name = name 42 | self.mu = mu 43 | self.mean_radius = mean_radius 44 | self.equatorial_radius = equatorial_radius 45 | self.polar_radius = polar_radius 46 | self.j2 = j2 47 | self.jpl_id = jpl_id 48 | self.symbol = symbol 49 | # self._rotational_elements = rotational_elements 50 | 51 | def _repr_latex_(self): 52 | strs = [ 53 | "{} {}".format(self.name, self.symbol), 54 | "Gravitational parameter:", 55 | "$\mu={}$ {}".format(self.mu.value, self.mu.unit), 56 | "Mean radius:", 57 | "$r_m={}$: {}".format(self.mean_radius.value, self.mean_radius.unit), 58 | "Equatorial radius:", 59 | "$r_e={}$ {}".format(self.equatorial_radius.value, self.equatorial_radius.unit), 60 | "Polar radius:", 61 | "$r_p={}$ {}".format(self.polar_radius.value, self.polar_radius.unit), 62 | "J2:", 63 | "$J_2={}$".format(self.j2), 64 | ] 65 | return "
".join(strs) 66 | 67 | def rv(self, jd, jd2=0.0): 68 | return config['ephemeris'].rv(self.jpl_id, jd, jd2) 69 | 70 | def wrt(self, body, jd, jd2=0.0): 71 | r_target, v_target = self.rv(jd, jd2) 72 | r_origin, v_origin = body.rv(jd, jd2) 73 | return r_target-r_origin, v_target-v_origin 74 | 75 | def plot3d(self, ax=None): 76 | if not ax: 77 | fig = plt.figure("Plyades Plot") 78 | ax = fig.add_subplot(111, projection='3d') 79 | re = self.equatorial_radius 80 | rp = self.polar_radius 81 | 82 | u = np.linspace(0, 2 * np.pi, 100) 83 | v = np.linspace(0, np.pi, 100) 84 | 85 | x = re * np.outer(np.cos(u), np.sin(v)) 86 | y = re * np.outer(np.sin(u), np.sin(v)) 87 | z = rp * np.outer(np.ones(np.size(u)), np.cos(v)) 88 | ax.plot_surface(x, y, z, 89 | rstride=4, 90 | cstride=4, 91 | color='b', 92 | alpha=.3, 93 | linewidth=1, 94 | edgecolor="b" 95 | ) 96 | 97 | 98 | class Moon: 99 | pass 100 | 101 | 102 | class SmallBody: 103 | pass 104 | 105 | MERCURY = Planet( 106 | 'Mercury', 107 | 2.2032e4*u.km**3/u.s**2, 108 | 2439.7*u.km, 109 | 2439.7*u.km, 110 | 2439.7*u.km, 111 | 0.00006, 112 | 199, 113 | u"\u263F", 114 | ) 115 | VENUS = Planet( 116 | 'Venus', 117 | 3.257e5*u.km**3/u.s**2, 118 | 6051.8*u.km, 119 | 6051.8*u.km, 120 | 6051.8*u.km, 121 | 0.000027, 122 | 299, 123 | u"\u2640", 124 | ) 125 | EARTH = Planet( 126 | 'Earth', 127 | 398600.4418*u.km**3/u.s**2, 128 | 6371.0084*u.km, 129 | 6378.1366*u.km, 130 | 6356.7519*u.km, 131 | 0.0010826269, 132 | 399, 133 | u"\u2641", 134 | ) 135 | MARS = Planet( 136 | 'Mars', 137 | 4.305e4*u.km**3/u.s**2, 138 | 3389.50*u.km, 139 | 3396.19*u.km, 140 | 3376.20*u.km, 141 | 0.001964, 142 | 4, 143 | u"\u2642", 144 | ) 145 | JUPITER = Planet( 146 | 'Jupiter', 147 | 1.268e8*u.km**3/u.s**2, 148 | 69911.0*u.km, 149 | 71492.0*u.km, 150 | 66854.0*u.km, 151 | 0.01475, 152 | 5, 153 | u"\u2643", 154 | ) 155 | SATURN = Planet( 156 | 'Saturn', 157 | 3.794e7*u.km**3/u.s**2, 158 | 58232.0*u.km, 159 | 60268.0*u.km, 160 | 54364.0*u.km, 161 | 0.01645, 162 | 6, 163 | u"\u2644", 164 | ) 165 | URANUS = Planet( 166 | 'Uranus', 167 | 5.794e6*u.km**3/u.s**2, 168 | 25362.0*u.km, 169 | 25559.0*u.km, 170 | 24973.0*u.km, 171 | 0.012, 172 | 7, 173 | u"\u26E2", 174 | ) 175 | NEPTUNE = Planet( 176 | 'Neptune', 177 | 6.809e6*u.km**3/u.s**2, 178 | 24622.0*u.km, 179 | 24764.0*u.km, 180 | 24341.0*u.km, 181 | 0.004, 182 | 8, 183 | u"\u2646", 184 | ) 185 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Plyades.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Plyades.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Plyades.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Plyades.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Plyades" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Plyades" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /plyades/orbit.py: -------------------------------------------------------------------------------- 1 | from astropy.table import Table 2 | from astropy.time import TimeDelta 3 | import astropy.units as units 4 | from bokeh.plotting import show, figure 5 | from bokeh.io import vplot 6 | import numpy as np 7 | import pandas as pd 8 | from scipy.interpolate import interp1d 9 | 10 | import plyades.kepler as kepler 11 | import plyades.util as util 12 | import plyades.visualization as vis 13 | 14 | class Orbit: 15 | def __init__( 16 | self, s0, dt, epochs, states, 17 | elements=None, interpolate=False, 18 | **kwargs 19 | ): 20 | default_names = [ 21 | 'dt', 22 | 'epoch', 23 | 'rx', 24 | 'ry', 25 | 'rz', 26 | 'vx', 27 | 'vy', 28 | 'vz', 29 | 'semi_major_axis', 30 | 'eccentricity', 31 | 'inclination', 32 | 'ascending_node', 33 | 'argument_of_periapsis', 34 | 'true_anomaly', 35 | ] 36 | names = kwargs.get('names', default_names) 37 | self.interpolate = interpolate 38 | self.s0 = s0 39 | self._states = np.array(states) 40 | self.spline = interp1d(dt, self._states, kind='cubic') 41 | if interpolate: 42 | t = np.linspace(0.0, dt[-1], interpolate) 43 | epochs = s0.t + TimeDelta(t, format='sec') 44 | y = self.spline(t) 45 | rx = y[0,:]*s0.r.unit 46 | ry = y[1,:]*s0.r.unit 47 | rz = y[2,:]*s0.r.unit 48 | vx = y[3,:]*s0.v.unit 49 | vy = y[4,:]*s0.v.unit 50 | vz = y[5,:]*s0.v.unit 51 | else: 52 | rx, ry, rz, vx, vy, vz = self._states 53 | t = dt 54 | 55 | if not elements: 56 | elements = kepler.elements( 57 | s0.body.mu, 58 | np.array((rx, ry, rz)).T*s0.r.unit, 59 | np.array((vx, vy, vz)).T*s0.v.unit, 60 | ) 61 | 62 | sma, ecc, inc, node, peri, ano = elements 63 | columns = [t, epochs, rx, ry, rz, vx, vy, vz, sma, ecc, inc, node, peri, ano] 64 | self.table = Table(columns, names=names) 65 | 66 | @property 67 | def epoch(self): 68 | return self.table['epoch'] 69 | 70 | @property 71 | def dt(self): 72 | return self.table['dt'] 73 | 74 | @property 75 | def rx(self): 76 | return self.table['rx'] 77 | 78 | @property 79 | def ry(self): 80 | return self.table['ry'] 81 | 82 | @property 83 | def rz(self): 84 | return self.table['rz'] 85 | 86 | @property 87 | def vx(self): 88 | return self.table['vx'] 89 | 90 | @property 91 | def vy(self): 92 | return self.table['vy'] 93 | 94 | @property 95 | def vz(self): 96 | return self.table['vz'] 97 | 98 | @property 99 | def semi_major_axis(self): 100 | return self.table['semi_major_axis'] 101 | 102 | @property 103 | def eccentricity(self): 104 | return self.table['eccentricity'] 105 | 106 | @property 107 | def inclination(self): 108 | return self.table['inclination'] 109 | 110 | @property 111 | def ascending_node(self): 112 | return self.table['ascending_node'] 113 | 114 | @property 115 | def argument_of_periapsis(self): 116 | return self.table['argument_of_periapsis'] 117 | 118 | @property 119 | def true_anomaly(self): 120 | return self.table['true_anomaly'] 121 | 122 | def interpolate(self, dt): 123 | return self.spline(dt) 124 | 125 | def state(self, t): 126 | arr = self.interpolate(t) 127 | if arr.ndim == 1: 128 | return type(self.s0).from_array(arr, t, self.s0) 129 | else: 130 | return [type(self.s0).from_array(e, t, self.s0) for e in arr.T] 131 | 132 | def plot_plane(self, plane='XY', show_steps=True): 133 | vis.plot_plane(self, plane=plane, show_steps=show_steps) 134 | 135 | def plot(self): 136 | plots = [vis.plot_plane(self, plane=plane, show_plot=False) for plane in ('XY', 'XZ', 'YZ')] 137 | show(vplot(*plots)) 138 | 139 | def plot3d(self): 140 | vis.plot3d(self) 141 | 142 | def plot_element(self, element, show_plot=True): 143 | if element == 'semi_major_axis': 144 | y = self.table[element].quantity 145 | f = figure( 146 | x_axis_type='datetime', 147 | width=500, 148 | height=500, 149 | title = 'Semi-major axis', 150 | ) 151 | elif element == 'eccentricity': 152 | y = self.table[element].quantity 153 | f = figure( 154 | x_axis_type='datetime', 155 | width=500, 156 | height=500, 157 | title = 'Eccentricity', 158 | ) 159 | elif element == 'inclination': 160 | y = self.table[element].to(units.deg) 161 | f = figure( 162 | x_axis_type='datetime', 163 | y_range=(0, 180), 164 | width=500, 165 | height=500, 166 | title = 'Inclination', 167 | ) 168 | elif element == 'ascending_node': 169 | y = self.table[element].to(units.deg) 170 | f = figure( 171 | y_range=(0, 360), 172 | x_axis_type='datetime', 173 | width=500, 174 | height=500, 175 | title = 'Longitude of ascending node', 176 | ) 177 | elif element == 'argument_of_periapsis': 178 | y = self.table[element].to(units.deg) 179 | f = figure( 180 | y_range=(0, 360), 181 | x_axis_type='datetime', 182 | width=500, 183 | height=500, 184 | title = 'Argument of periapsis', 185 | ) 186 | elif element == 'true_anomaly': 187 | y = self.table[element].to(units.deg) 188 | f = figure( 189 | x_axis_type='datetime', 190 | y_range=(0, 180), 191 | width=500, 192 | height=500, 193 | title = 'True anomaly', 194 | ) 195 | 196 | f.line(x=self.epoch.datetime, y=y.value, line_width=2) 197 | if show_plot: 198 | show(f) 199 | else: 200 | return f 201 | 202 | 203 | def plot_elements(self): 204 | elements = ( 205 | 'semi_major_axis', 206 | 'eccentricity', 207 | 'inclination', 208 | 'ascending_node', 209 | 'argument_of_periapsis', 210 | 'true_anomaly', 211 | ) 212 | plots = [self.plot_element(element, show_plot=False) for element in elements] 213 | show(vplot(*plots)) 214 | -------------------------------------------------------------------------------- /plyades/core.py: -------------------------------------------------------------------------------- 1 | from bokeh.io import vplot 2 | from bokeh.plotting import show 3 | from copy import deepcopy 4 | from IPython.display import Latex 5 | import numpy as np 6 | from astropy import units as units 7 | from astropy.time import Time, TimeDelta 8 | 9 | from plyades.bodies import EARTH 10 | from plyades.propagator import Propagator 11 | from plyades.orbit import Orbit 12 | import plyades.kepler as kepler 13 | import plyades.forces as forces 14 | import plyades.util as util 15 | import plyades.visualization as vis 16 | 17 | 18 | class State: 19 | def __init__(self, r, v, t, frame="MEE2000", body=EARTH, vars=None): 20 | r_unit = util.getunit(r) 21 | v_unit = util.getunit(v) 22 | if not r_unit: 23 | self.r = r*units.km 24 | else: 25 | self.r = r 26 | if not v_unit: 27 | self.v = v*units.km/units.s 28 | else: 29 | self.v = v 30 | self.t = Time(t) 31 | self.frame = frame 32 | self.body = body 33 | self._array = np.hstack((np.copy(r), np.copy(v))) 34 | self._gravity = forces.newton 35 | self._forces = [] 36 | if vars: 37 | self.vars = vars 38 | 39 | def force(self, func): 40 | self._forces.append(func) 41 | 42 | def gravity(self, func): 43 | self._gravity = func 44 | 45 | @classmethod 46 | def from_array(cls, arr, t, s0=None): 47 | if s0: 48 | frame = s0.frame 49 | body = s0.body 50 | t = s0.t + TimeDelta(t, format='sec') 51 | if len(arr) > 6: 52 | vars = arr[6:] 53 | else: 54 | vars = None 55 | return cls( 56 | arr[:3], arr[3:], t, 57 | frame, body, vars) 58 | 59 | def __array__(self): 60 | return self._array 61 | 62 | def __iter__(self): 63 | yield from self._array 64 | 65 | def __len__(self): 66 | return len(self._array) 67 | 68 | def __getitem__(self, position): 69 | return self._array[position] 70 | 71 | def __setitem__(self, position, value): 72 | self._array[position] = value 73 | self.r = self._array[:3]*self.r.unit 74 | self.v = self._array[3:]*self.v.unit 75 | 76 | def _repr_latex_(self): 77 | strings = [ 78 | "Time: {}".format(self.t.iso), 79 | "Reference frame: {}
".format(self.frame), 80 | "Central body:
" 81 | "{}
".format(self.body._repr_latex_()), 82 | "$r_x = {}$ {}".format(self.r[0].value, self.r[0].unit._repr_latex_()), 83 | "$r_y = {}$ {}".format(self.r[1].value, self.r[1].unit._repr_latex_()), 84 | "$r_z = {}$ {}".format(self.r[2].value, self.r[2].unit._repr_latex_()), 85 | "$v_x = {}$ {}".format(self.v[0].value, self.v[0].unit._repr_latex_()), 86 | "$v_y = {}$ {}".format(self.v[1].value, self.v[1].unit._repr_latex_()), 87 | "$v_z = {}$ {}".format(self.v[2].value, self.v[2].unit._repr_latex_()), 88 | ] 89 | return "
".join(strings) 90 | 91 | def pprint_elements(self): 92 | inc = self.inclination.to(units.deg) 93 | node = self.ascending_node.to(units.deg) 94 | peri = self.argument_of_periapsis.to(units.deg) 95 | ano = self.true_anomaly.to(units.deg) 96 | strings = [ 97 | "Semi-major axis:", 98 | "$a = {}$ {}".format(self.semi_major_axis.value, self.semi_major_axis.unit), 99 | "Eccentricity:", 100 | "$e = {}$ {}".format(self.eccentricity.value, self.eccentricity.unit), 101 | "Inclination:", 102 | "$i = {}$ {}".format(inc.value, inc.unit), 103 | "Longitude of ascending node:", 104 | "$\Omega = {}$ {}".format(node.value, node.unit), 105 | "Argument of periapsis:", 106 | "$\omega = {}$ {}".format(peri.value, peri.unit), 107 | "True anomaly:", 108 | "$\\nu = {}$ {}".format(ano.value, ano.unit), 109 | ] 110 | return Latex("
".join(strings)) 111 | 112 | def wrt(self, body): 113 | r_origin, v_origin = body.rv(self.t.jd, self.t.jd2) 114 | return self.r-r_origin, self.v-v_origin 115 | 116 | @property 117 | def jd(self): 118 | return self.t.jd 119 | 120 | @property 121 | def jd2000(self): 122 | return self.jd - constants.DELTA_JD2000 123 | 124 | @property 125 | def jd1950(self): 126 | return self.jd - constants.DELTA_JD1950 127 | 128 | @property 129 | def mjd(self): 130 | return self.jd - constants.DELTA_MJD 131 | 132 | @property 133 | def elements(self): 134 | return kepler.elements(self.body.mu, self.r, self.v) 135 | 136 | @property 137 | def semi_major_axis(self): 138 | return self.elements[0] 139 | 140 | @property 141 | def eccentricity(self): 142 | return self.elements[1] 143 | 144 | @property 145 | def inclination(self): 146 | return self.elements[2] 147 | 148 | @property 149 | def ascending_node(self): 150 | return self.elements[3] 151 | 152 | @property 153 | def argument_of_periapsis(self): 154 | return self.elements[4] 155 | 156 | @property 157 | def true_anomaly(self): 158 | return self.elements[5] 159 | 160 | @property 161 | def period(self): 162 | return kepler.period(self.semi_major_axis, self.body.mu) 163 | 164 | @property 165 | def orbital_energy(self): 166 | return kepler.orbital_energy(self.semi_major_axis, self.body.mu) 167 | 168 | @property 169 | def mean_motion(self): 170 | return 2*np.pi*units.rad/self.period 171 | 172 | def kepler_orbit(self, n=100): 173 | dt = np.linspace(0, self.period, n) 174 | sma, ecc, inc, node, peri, ano1 = self.elements 175 | mean_ano = kepler.true_to_mean(ano1, ecc) 176 | mean_ano1 = dt*self.mean_motion + mean_ano 177 | ano = units.Quantity([kepler.mean_to_true(m, ecc) for m in mean_ano1]) 178 | sma = np.repeat(sma, n) 179 | ecc = np.repeat(ecc, n) 180 | inc = np.repeat(inc, n) 181 | node = np.repeat(node, n) 182 | peri = np.repeat(peri, n) 183 | epochs = self.t + TimeDelta(dt, format='sec') 184 | states = kepler.cartesian(self.body.mu, sma, ecc, inc, node, peri, ano) 185 | return Orbit( 186 | deepcopy(self), dt, epochs, states, 187 | elements=[sma, ecc, inc, node, peri, ano] 188 | ) 189 | 190 | def kepler_state(self, dt): 191 | sma, ecc, inc, node, peri, true_ano = self.elements 192 | mean_ano = kepler.true_to_mean(true_ano, ecc) 193 | mean_ano1 = dt*self.mean_motion + mean_ano 194 | true_ano1 = kepler.mean_to_true(mean_ano1, ecc) 195 | rv = kepler.cartesian(self.body.mu, sma, ecc, inc, node, peri, true_ano1) 196 | return State(rv[:3]*self.r.unit, rv[3:]*self.v.unit, 197 | self.t+TimeDelta(dt, format='sec'), 198 | self.frame, self.body) 199 | 200 | def propagate(self, dt=1*units.year, time_unit=units.s, interpolate=100, **kwargs): 201 | tout = [0.0] 202 | yout = [np.copy(self)] 203 | p = Propagator(self, dt.to(time_unit).value, **kwargs) 204 | p.forces = list(self._forces) 205 | p.forces.append(self._gravity) 206 | for t, y in p: 207 | tout.append(t) 208 | yout.append(y) 209 | tout = np.array(tout)*time_unit 210 | epochs = self.t + TimeDelta(tout.to(units.s), format='sec') 211 | yout = np.array(yout).T 212 | return Orbit(deepcopy(self), tout, epochs, yout, interpolate=interpolate, **kwargs) 213 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Plyades documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Dec 10 14:54:27 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | # import plyades 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'numpydoc'] 30 | 31 | intersphinx_mapping = { 32 | 'python': ('http://docs.python.org/', None), 33 | 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 34 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), 35 | 'matplotlib': ('http://matplotlib.sourceforge.net/', None), 36 | 'astropy': ('http://www.astropy.org/', None), 37 | 'h5py': ('http://h5py.alfven.org/docs-2.1/', None) 38 | } 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | #source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'Plyades' 54 | copyright = u'2015, Helge Eichhorn' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = '0.0' 62 | # The full version, including alpha/beta/rc tags. 63 | release = '0.0.1' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | #language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | #today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | #today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ['_build'] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | 100 | # -- Options for HTML output --------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'default' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | #html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | #html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | #html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | #html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | html_logo = '_static/plyades_logo.jpg' 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | #html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ['_static'] 134 | 135 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 136 | # using the given strftime format. 137 | #html_last_updated_fmt = '%b %d, %Y' 138 | 139 | # If true, SmartyPants will be used to convert quotes and dashes to 140 | # typographically correct entities. 141 | #html_use_smartypants = True 142 | 143 | # Custom sidebar templates, maps document names to template names. 144 | #html_sidebars = {} 145 | 146 | # Additional templates that should be rendered to pages, maps page names to 147 | # template names. 148 | #html_additional_pages = {} 149 | 150 | # If false, no module index is generated. 151 | #html_domain_indices = True 152 | 153 | # If false, no index is generated. 154 | #html_use_index = True 155 | 156 | # If true, the index is split into individual pages for each letter. 157 | #html_split_index = False 158 | 159 | # If true, links to the reST sources are added to the pages. 160 | #html_show_sourcelink = True 161 | 162 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 163 | #html_show_sphinx = True 164 | 165 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 166 | #html_show_copyright = True 167 | 168 | # If true, an OpenSearch description file will be output, and all pages will 169 | # contain a tag referring to it. The value of this option must be the 170 | # base URL from which the finished HTML is served. 171 | #html_use_opensearch = '' 172 | 173 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 174 | #html_file_suffix = None 175 | 176 | # Output file base name for HTML help builder. 177 | htmlhelp_basename = 'Plyadesdoc' 178 | 179 | 180 | # -- Options for LaTeX output -------------------------------------------------- 181 | 182 | latex_elements = { 183 | # The paper size ('letterpaper' or 'a4paper'). 184 | #'papersize': 'letterpaper', 185 | 186 | # The font size ('10pt', '11pt' or '12pt'). 187 | #'pointsize': '10pt', 188 | 189 | # Additional stuff for the LaTeX preamble. 190 | #'preamble': '', 191 | } 192 | 193 | # Grouping the document tree into LaTeX files. List of tuples 194 | # (source start file, target name, title, author, documentclass [howto/manual]). 195 | latex_documents = [ 196 | ('index', 'Plyades.tex', u'Plyades Documentation', 197 | u'Helge Eichhorn', 'manual'), 198 | ] 199 | 200 | # The name of an image file (relative to this directory) to place at the top of 201 | # the title page. 202 | #latex_logo = None 203 | 204 | # For "manual" documents, if this is true, then toplevel headings are parts, 205 | # not chapters. 206 | #latex_use_parts = False 207 | 208 | # If true, show page references after internal links. 209 | #latex_show_pagerefs = False 210 | 211 | # If true, show URL addresses after external links. 212 | #latex_show_urls = False 213 | 214 | # Documents to append as an appendix to all manuals. 215 | #latex_appendices = [] 216 | 217 | # If false, no module index is generated. 218 | #latex_domain_indices = True 219 | 220 | 221 | # -- Options for manual page output -------------------------------------------- 222 | 223 | # One entry per manual page. List of tuples 224 | # (source start file, name, description, authors, manual section). 225 | man_pages = [ 226 | ('index', 'plyades', u'Plyades Documentation', 227 | [u'Helge Eichhorn'], 1) 228 | ] 229 | 230 | # If true, show URL addresses after external links. 231 | #man_show_urls = False 232 | 233 | 234 | # -- Options for Texinfo output ------------------------------------------------ 235 | 236 | # Grouping the document tree into Texinfo files. List of tuples 237 | # (source start file, target name, title, author, 238 | # dir menu entry, description, category) 239 | texinfo_documents = [ 240 | ('index', 'Plyades', u'Plyades Documentation', 241 | u'Helge Eichhorn', 'Plyades', 'One line description of project.', 242 | 'Miscellaneous'), 243 | ] 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #texinfo_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #texinfo_domain_indices = True 250 | 251 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 252 | #texinfo_show_urls = 'footnote' 253 | --------------------------------------------------------------------------------