Repository: poliastro/poliastro

Issue:
"New Horizons launch" example won't run on master
<!--- Provide a general summary of the issue in the Title above -->

🐞 **Problem**
<!--- Provide a detailed description of the change or addition you are proposing -->
<!--- If it is a feature or a bug, what problem is it solving-->
[Exploring the new Horizons launch](https://github.com/poliastro/poliastro/blob/d32f3ab802f782a03582e6c183a28c12c6abb2d7/docs/source/examples/Exploring%20the%20New%20Horizons%20launch.ipynb) example fails on `In [5]` when poliastro is installed from master (d32f3ab802f782a03582e6c183a28c12c6abb2d7).

<details>

```python
/usr/local/lib/python3.6/site-packages/astropy/units/quantity.py:639: RuntimeWarning:

invalid value encountered in log

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/usr/local/lib/python3.6/site-packages/astropy/time/core.py in __add__(self, other)
   1369             try:
-> 1370                 other = TimeDelta(other)
   1371             except Exception:

/usr/local/lib/python3.6/site-packages/astropy/time/core.py in __init__(self, val, val2, format, scale, copy)
   1539 
-> 1540             self._init_from_vals(val, val2, format, scale, copy)
   1541 

/usr/local/lib/python3.6/site-packages/astropy/time/core.py in _init_from_vals(self, val, val2, format, scale, copy, precision, in_subfmt, out_subfmt)
    329         self._time = self._get_time_fmt(val, val2, format, scale,
--> 330                                         precision, in_subfmt, out_subfmt)
    331         self._format = self._time.name

/usr/local/lib/python3.6/site-packages/astropy/time/core.py in _get_time_fmt(self, val, val2, format, scale, precision, in_subfmt, out_subfmt)
    373         else:
--> 374             raise ValueError('Input values did not match {0}'.format(err_msg))
    375 

ValueError: Input values did not match the format class jd

During handling of the above exception, another exception occurred:

OperandTypeError                          Traceback (most recent call last)
<ipython-input-5-f1d654cf6abc> in <module>()
      2 
      3 op.plot(parking)
----> 4 op.plot(exit)
      5 
      6 plt.xlim(-8000, 8000)

/usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/plotting.py in plot(self, orbit, label, color)
    152         lines = []
    153 
--> 154         _, positions = orbit.sample(self.num_points)
    155         rr = positions.get_xyz().transpose()
    156 

/usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/twobody/orbit.py in sample(self, values, function)
    308                 nu_values = np.insert(nu_values, 0, self.ecc)
    309 
--> 310             return self.sample(nu_values, function)
    311 
    312         elif hasattr(values, "unit") and values.unit in ('rad', 'deg'):

/usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/twobody/orbit.py in sample(self, values, function)
    311 
    312         elif hasattr(values, "unit") and values.unit in ('rad', 'deg'):
--> 313             values = self._generate_time_values(values)
    314         return (values, self._sample(values, function))
    315 

/usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/twobody/orbit.py in _generate_time_values(self, nu_vals)
    324     def _generate_time_values(self, nu_vals):
    325         M_vals = nu_to_M(nu_vals, self.ecc)
--> 326         time_values = self.epoch + (M_vals / self.n).decompose()
    327         return time_values
    328 

/usr/local/lib/python3.6/site-packages/astropy/time/core.py in __add__(self, other)
   1370                 other = TimeDelta(other)
   1371             except Exception:
-> 1372                 raise OperandTypeError(self, other, '+')
   1373 
   1374         # Tdelta + something is dealt with in TimeDelta, so we have

OperandTypeError: Unsupported operand type(s) for +: 'Time' and 'Quantity'
```

</details><br>

🖥 **Please paste the output of following commands**

(Installed poliastro without conda)
```
$ python3 -V
Python 3.6.4
$ uname -a
Linux linux 4.13.0-32-generic #35~16.04.1-Ubuntu SMP Thu Jan 25 10:13:43 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
```

<details>
<summary>Running tests</summary>

```
$ python3 -c "import poliastro.testing; poliastro.testing.test()"
======================================= test session starts ========================================
platform linux -- Python 3.6.4, pytest-3.4.1, py-1.5.2, pluggy-0.6.0
rootdir: /usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro, inifile:
plugins: rerunfailures-4.0, remotedata-0.2.0, openfiles-0.2.0, mock-1.6.3, doctestplus-0.1.2, cov-2.5.1, arraydiff-0.2, hypothesis-3.45.0
collected 140 items                                                                                

../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_bodies.py . [  0%]
.....                                                                                        [  4%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_coordinates.py . [  5%]
..                                                                                           [  6%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_hyper.py . [  7%]
..........                                                                                   [ 14%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_iod.py . [ 15%]
........                                                                                     [ 20%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_jit.py . [ 21%]
..                                                                                           [ 22%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_maneuver.py . [ 23%]
.....                                                                                        [ 27%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_patched_conics.py . [ 27%]
.                                                                                            [ 28%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_plotting.py . [ 29%]
.......                                                                                      [ 34%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_plotting3d.py . [ 35%]
.....                                                                                        [ 38%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_stumpff.py . [ 39%]
..                                                                                           [ 40%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_twobody.py . [ 41%]
.                                                                                            [ 42%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/test_util.py . [ 42%]
......                                                                                       [ 47%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_neos/test_dastcom5.py . [ 47%]
........                                                                                     [ 53%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_neos/test_neos_neows.py . [ 54%]
.....                                                                                        [ 57%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_twobody/test_angles.py . [ 58%]
......                                                                                       [ 62%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_twobody/test_decorators.py . [ 63%]
.                                                                                            [ 64%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_twobody/test_orbit.py . [ 65%]
..................                                                                           [ 77%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_twobody/test_propagation.py . [ 78%]
........                                                                                     [ 84%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_twobody/test_sample.py . [ 85%]
...........                                                                                  [ 92%]
../../usr/local/lib/python3.6/site-packages/poliastro-0.9.dev0-py3.6.egg/poliastro/tests/tests_twobody/test_states.py . [ 93%]
.........                                                                                    [100%]

=================================== 140 passed in 23.95 seconds ====================================
```

</details><br>

Looks like 27318862c266caf51a96857864eaf3045f613dae (was later on modified by 92b6c9dbcfa2d5fcf707094642821725465b131e) caused this regression.

🎯 **Goal**
<!--- Why is this change important to you? How would you use it? -->
<!--- How can it benefit other users? -->

💡 **Possible solutions**
<!--- Not obligatory, but suggest an idea for implementing addition or change -->

📋  **Steps to solve the problem**

 * Comment below about what you've started working on.
 * Add, commit, push your changes
 * Submit a pull request and add this in comments - `Addresses #<put issue number here>`
 * Ask for a review in comments section of pull request
 * Celebrate your contribution to this project 🎉 


Discussion:

Patch:
diff --git a/src/poliastro/plotting.py b/src/poliastro/plotting.py
index 48bc1e9f..ad3af768 100644
--- a/src/poliastro/plotting.py
+++ b/src/poliastro/plotting.py
@@ -131,6 +131,12 @@ class OrbitPlotter(object):
 
         self.ax.add_patch(mpl.patches.Circle((0, 0), radius, lw=0, color=color))
 
+    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(self, orbit, label=None, color=None):
         """Plots state and osculating orbit in their plane.
 
@@ -156,15 +162,15 @@ class OrbitPlotter(object):
 
         # Project on OrbitPlotter frame
         # x_vec, y_vec, z_vec = self._frame
-        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])
+        x, y = self._project(rr)
+        x0, y0 = self._project(orbit.r[None])
 
         # Plot current position
-        l, = self.ax.plot(x[0].to(u.km).value, y[0].to(u.km).value,
+        l, = self.ax.plot(x0.to(u.km).value, y0.to(u.km).value,
                           'o', mew=0, color=color)
         lines.append(l)
 
+        # Plot trajectory
         l, = self.ax.plot(x.to(u.km).value, y.to(u.km).value,
                           '--', color=l.get_color())
         lines.append(l)
diff --git a/src/poliastro/twobody/orbit.py b/src/poliastro/twobody/orbit.py
index d2ab7542..82c241c0 100644
--- a/src/poliastro/twobody/orbit.py
+++ b/src/poliastro/twobody/orbit.py
@@ -298,20 +298,25 @@ class Orbit(object):
                 # first sample eccentric anomaly, then transform into true anomaly
                 # why sampling eccentric anomaly uniformly to minimize error in the apocenter, see
                 # http://www.dtic.mil/dtic/tr/fulltext/u2/a605040.pdf
+                # Start from pericenter
                 E_values = np.linspace(0, 2 * np.pi, values) * u.rad
                 nu_values = E_to_nu(E_values, self.ecc)
             else:
                 # Select a sensible limiting value for non-closed orbits
-                # This corresponds to r = 3p
-                nu_limit = np.arccos(-(1 - 1 / 3.) / self.ecc)
+                # 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 = self.nu if self.nu < 180 * u.deg else self.nu - 360 * u.deg
+                nu_limit = max(np.arccos(-(1 - 1 / 3.) / self.ecc), wrapped_nu)
                 nu_values = np.linspace(-nu_limit, nu_limit, values)
-                nu_values = np.insert(nu_values, 0, self.ecc)
 
             return self.sample(nu_values, method)
 
         elif hasattr(values, "unit") and values.unit in ('rad', 'deg'):
             values = self._generate_time_values(values)
-        return (values, self._sample(values, method))
+
+        return values, self._sample(values, method)
 
     def _sample(self, time_values, method=mean_motion):
         values = np.zeros((len(time_values), 3)) * self.r.unit
@@ -322,7 +327,8 @@ class Orbit(object):
         return CartesianRepresentation(values, xyz_axis=1)
 
     def _generate_time_values(self, nu_vals):
-        M_vals = nu_to_M(nu_vals, self.ecc)
+        # Subtract current anomaly to start from the desired point
+        M_vals = nu_to_M(nu_vals, self.ecc) - nu_to_M(self.nu, self.ecc)
         time_values = self.epoch + (M_vals / self.n).decompose()
         return time_values
 


Test patch:
diff --git a/src/poliastro/tests/tests_twobody/test_orbit.py b/src/poliastro/tests/tests_twobody/test_orbit.py
index 14b0835d..1649570c 100644
--- a/src/poliastro/tests/tests_twobody/test_orbit.py
+++ b/src/poliastro/tests/tests_twobody/test_orbit.py
@@ -181,7 +181,7 @@ def test_sample_with_time_value():
     ss = Orbit.from_classical(_body, _d, _, _a, _a, _a, _a)
 
     expected_r = [ss.r]
-    _, positions = ss.sample(values=[360] * u.deg)
+    _, positions = ss.sample(values=ss.nu + [360] * u.deg)
     r = positions.get_xyz().transpose()
 
     assert_quantity_allclose(r, expected_r, rtol=1.e-7)
@@ -195,18 +195,21 @@ def test_sample_with_nu_value():
     ss = Orbit.from_classical(_body, _d, _, _a, _a, _a, _a)
 
     expected_r = [ss.r]
-    _, positions = ss.sample(values=[360] * u.deg)
+    _, positions = ss.sample(values=ss.nu + [360] * u.deg)
     r = positions.get_xyz().transpose()
 
     assert_quantity_allclose(r, expected_r, rtol=1.e-7)
 
 
-def test_nu_value_check():
-    _d = [1.197659243752796E+09, -4.443716685978071E+09, -1.747610548576734E+09] * u.km
-    _v = [5.540549267188614E+00, -1.251544669134140E+01, -4.848892572767733E+00] * u.km / u.s
-    ss = Orbit.from_vectors(Sun, _d, _v, Time('2015-07-14 07:59', scale='tdb'))
+def test_hyperbolic_nu_value_check():
+    # A custom hyperbolic orbit
+    r = [1.197659243752796E+09, -4.443716685978071E+09, -1.747610548576734E+09] * u.km
+    v = [5.540549267188614E+00, -1.251544669134140E+01, -4.848892572767733E+00] * u.km / u.s
+
+    ss = Orbit.from_vectors(Sun, r, v, Time('2015-07-14 07:59', scale='tdb'))
+
     values, positions = ss.sample(100)
 
     assert isinstance(positions, CartesianRepresentation)
     assert isinstance(values, Time)
-    assert len(positions) == len(values) == 101
+    assert len(positions) == len(values) == 100
diff --git a/src/poliastro/tests/tests_twobody/test_sample.py b/src/poliastro/tests/tests_twobody/test_sample.py
index 051f86c9..ae1b978c 100644
--- a/src/poliastro/tests/tests_twobody/test_sample.py
+++ b/src/poliastro/tests/tests_twobody/test_sample.py
@@ -9,6 +9,8 @@ from poliastro.twobody import Orbit
 from poliastro.twobody.propagation import kepler, mean_motion, cowell
 import numpy as np
 
+from poliastro.util import norm
+
 
 def test_sample_angle_zero_returns_same():
     # Data from Vallado, example 2.4
@@ -17,7 +19,7 @@ def test_sample_angle_zero_returns_same():
     ss0 = Orbit.from_vectors(Earth, r0, v0)
 
     nu_values = [0] * u.deg
-    _, rr = ss0.sample(nu_values)
+    _, rr = ss0.sample(ss0.nu + nu_values)
 
     assert_quantity_allclose(rr[0].get_xyz(), ss0.r)
 
@@ -71,7 +73,8 @@ def test_sample_nu_values():
     _, rr = ss0.sample(nu_values)
 
     assert len(rr) == len(nu_values)
-    assert_quantity_allclose(rr[-1].get_xyz(), expected_ss.r)
+    assert_quantity_allclose(norm(rr[0].get_xyz()), expected_ss.r_p)
+    assert_quantity_allclose(norm(rr[-1].get_xyz()), expected_ss.r_a)
 
 
 @pytest.mark.parametrize("num_points", [3, 5, 7, 9, 11, 101])
@@ -81,9 +84,10 @@ def test_sample_num_points(num_points):
     v0 = [-5.64305, 4.30333, 2.42879] * u.km / u.s
     ss0 = Orbit.from_vectors(Earth, r0, v0)
 
-    expected_ss = ss0.propagate(ss0.period / 2)
+    # TODO: Test against the perigee and apogee
+    # expected_ss = ss0.propagate(ss0.period / 2)
 
     _, rr = ss0.sample(num_points)
 
     assert len(rr) == num_points
-    assert_quantity_allclose(rr[num_points // 2].get_xyz(), expected_ss.r)
+    # assert_quantity_allclose(rr[num_points // 2].get_xyz(), expected_ss.r)