def sample_closed(min_nu, ecc, max_nu=None, num_values=100): """Sample a closed orbit If ``max_nu`` is given, the sampling interval will go from the minimum to the maximum true anomaly in the direction of the orbit. If not given, it will do a full revolution starting in the minimum true anomaly. Notes ----- First sample the eccentric anomaly uniformly, then transform into true anomaly to minimize error in the apocenter, see https://apps.dtic.mil/dtic/tr/fulltext/u2/a605040.pdf """ # Because how nu_to_E works, we don't need to wrap the angle here! # It will do the right thing min_E = nu_to_E(min_nu, ecc) # This linspace will always increase positively, # even though it might contain out of range values E_values = alinspace(min_E, nu_to_E(max_nu, ecc) if max_nu is not None else None, num=num_values) # Because how E_to_nu works, we don't need to wrap the angles here! # It will do the right thing nu_values = E_to_nu(E_values, ecc) # We wrap the angles on return return (nu_values + np.pi * u.rad) % (2 * np.pi * u.rad) - np.pi * u.rad
def sample(self, values=None, function=propagate): """Samples an orbit to some specified time values. .. versionadded:: 0.8.0 Parameters ---------- values : Multiple options Number of interval points (default to 100), True anomaly values, Time values. Returns ------- (Time, CartesianRepresentation) A tuple containing Time and Position vector in each given value. Notes ----- When specifying a number of points, the initial and final position is present twice inside the result (first and last row). This is more useful for plotting. Examples -------- >>> from astropy import units as u >>> from poliastro.examples import iss >>> iss.sample() >>> iss.sample(10) >>> iss.sample([0, 180] * u.deg) >>> iss.sample([0, 10, 20] * u.minute) >>> iss.sample([iss.epoch + iss.period / 2]) """ if values is None: return self.sample(100, function) elif isinstance(values, int): if self.ecc < 1: # 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 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) nu_values = np.linspace(-nu_limit, nu_limit, values) nu_values = np.insert(nu_values, 0, self.ecc) return self.sample(nu_values, function) elif hasattr(values, "unit") and values.unit in ('rad', 'deg'): values = self._generate_time_values(values) return (values, self._sample(values, function))
def test_mean_to_true(): for row in ELLIPTIC_ANGLES_DATA: ecc, M, expected_nu = row ecc = ecc * u.one M = M * u.deg expected_nu = expected_nu * u.deg nu = E_to_nu(M_to_E(M, ecc), ecc) assert_quantity_allclose(nu, expected_nu, rtol=1e-4)
def sample(self, values=100, method=mean_motion): """Samples an orbit to some specified time values. .. versionadded:: 0.8.0 Parameters ---------- values : int Number of interval points (default to 100). method : function, optional Method used for propagation Returns ------- positions: ~astropy.coordinates.BaseCoordinateFrame Array of x, y, z positions, with proper times as the frame attributes if supported. Notes ----- When specifying a number of points, the initial and final position is present twice inside the result (first and last row). This is more useful for plotting. Examples -------- >>> from astropy import units as u >>> from poliastro.examples import iss >>> iss.sample() # doctest: +ELLIPSIS <GCRS Coordinate ...> >>> iss.sample(10) # doctest: +ELLIPSIS <GCRS Coordinate ...> """ if self.ecc < 1: # 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 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.0) / self.ecc), abs(wrapped_nu)) nu_values = np.linspace(-nu_limit, nu_limit, values) time_values = self._generate_time_values(nu_values) return propagate(self, time_values, method=method)
def _sample_closed(self, values, min_anomaly, max_anomaly): limits = [ min_anomaly.to(u.rad).value if min_anomaly is not None else 0, max_anomaly.to(u.rad).value if max_anomaly is not None else 2 * np.pi, ] * u.rad # First sample eccentric anomaly, then transform into true anomaly # to minimize error in the apocenter, see # http://www.dtic.mil/dtic/tr/fulltext/u2/a605040.pdf # Start from pericenter E_values = np.linspace(*limits, values) nu_values = E_to_nu(E_values, self.ecc) return nu_values
def orbit_from_sbdb(name, **kwargs): obj = SBDB.query(name, full_precision=True, **kwargs) if "count" in obj: # No error till now ---> more than one object has been found # Contains all the name of the objects objects_name = obj["list"]["name"] objects_name_in_str = "" # Used to store them in string form each in new line for i in objects_name: objects_name_in_str += i + "\n" raise ValueError( str(obj["count"]) + " different objects found: \n" + objects_name_in_str) if "object" not in obj.keys(): raise ValueError(f"Object {name} not found") a = obj["orbit"]["elements"]["a"] ecc = float(obj["orbit"]["elements"]["e"]) * u.one inc = obj["orbit"]["elements"]["i"] raan = obj["orbit"]["elements"]["om"] argp = obj["orbit"]["elements"]["w"] # Since JPL provides Mean Anomaly (M) we need to make # the conversion to the true anomaly (nu) M = obj["orbit"]["elements"]["ma"].to(u.rad) # NOTE: It is unclear how this conversion should happen, # see https://ssd-api.jpl.nasa.gov/doc/sbdb.html if ecc < 1: M = (M + np.pi * u.rad) % (2 * np.pi * u.rad) - np.pi * u.rad nu = E_to_nu(M_to_E(M, ecc), ecc) elif ecc == 1: nu = D_to_nu(M_to_D(M)) else: nu = F_to_nu(M_to_F(M, ecc), ecc) epoch = Time(obj["orbit"]["epoch"].to(u.d), format="jd") return Orbit.from_classical( attractor=Sun, a=a, ecc=ecc, inc=inc, raan=raan, argp=argp, nu=nu, epoch=epoch.tdb, plane=Planes.EARTH_ECLIPTIC, )
def orbit_from_record(record): """Return :py:class:`~poliastro.twobody.orbit.Orbit` given a record. Retrieve info from JPL DASTCOM5 database. Parameters ---------- record : int Object record. Returns ------- orbit : ~poliastro.twobody.orbit.Orbit NEO orbit. """ body_data = read_record(record) a = body_data["A"].item() * u.au ecc = body_data["EC"].item() * u.one inc = body_data["IN"].item() * u.deg raan = body_data["OM"].item() * u.deg argp = body_data["W"].item() * u.deg M = body_data["MA"].item() * u.deg epoch = Time(body_data["EPOCH"].item(), format="jd", scale="tdb") # NOTE: It is unclear how this conversion should happen, # see https://ssd-api.jpl.nasa.gov/doc/sbdb.html if ecc < 1: M = (M + np.pi * u.rad) % (2 * np.pi * u.rad) - np.pi * u.rad nu = E_to_nu(M_to_E(M, ecc), ecc) elif ecc == 1: nu = D_to_nu(M_to_D(M)) else: nu = F_to_nu(M_to_F(M, ecc), ecc) orbit = Orbit.from_classical(Sun, a, ecc, inc, raan, argp, nu, epoch) orbit._frame = HeliocentricEclipticJ2000(obstime=epoch) return orbit
def newton(func, x0, ecc, M, fprime=None, maxiter=50): EFD = 1.0 * x0 delta = 1e-2 nu_prev = 1e+10 converged = False # Newton-Rapheson method with u.set_enabled_equivalencies(u.dimensionless_angles()): for iter in range(maxiter): if ecc < 1.0 - delta: nu = E_to_nu(EFD, ecc) E_plus = nu_to_E(nu + variation, ecc) E_minus = nu_to_E(nu - variation, ecc) M_actual = _kepler_equation(EFD, M, ecc, count=False) M_plus = _kepler_equation(E_plus, M, ecc, count=False) M_minus = _kepler_equation(E_minus, M, ecc, count=False) elif ecc > 1.0 + delta: nu = F_to_nu(EFD, ecc) F_plus = nu_to_F(nu + variation, ecc) F_minus = nu_to_F(nu - variation, ecc) M_actual = _kepler_equation_hyper(EFD, M, ecc, count=False) M_plus = _kepler_equation_hyper(F_plus, M, ecc, count=False) M_minus = _kepler_equation_hyper(F_minus, M, ecc, count=False) else: nu = D_to_nu(EFD, ecc) D_plus = nu_to_D(nu + variation, ecc) D_minus = nu_to_D(nu - variation, ecc) M_actual = _kepler_equation_parabolic(EFD, M, ecc, count=False) M_plus = _kepler_equation_parabolic(D_plus, M, ecc, count=False) M_minus = _kepler_equation_parabolic(D_minus, M, ecc, count=False) converged = (np.abs(nu_prev - nu) < variation) and (M_actual * M_plus <= 0 or M_minus * M_actual <= 0) nu_prev = nu EFD_new = EFD - func(EFD, M, ecc) / fprime(EFD, M, ecc) if converged: return EFD_new EFD = EFD_new print(ecc, "fail") return -1
def test_eccentric_to_true_range(E, ecc): nu = E_to_nu(E, ecc) E_result = nu_to_E(nu, ecc) assert_quantity_allclose(E_result, E, rtol=1e-8)
def from_sbdb(cls, name, **kwargs): """Return osculating `Orbit` by using `SBDB` from Astroquery. Parameters ---------- name: str Name of the body to make the request. Returns ------- ss: poliastro.twobody.orbit.Orbit Orbit corresponding to body_name Examples -------- >>> from poliastro.twobody.orbit import Orbit >>> apophis_orbit = Orbit.from_sbdb('apophis') # doctest: +REMOTE_DATA """ from poliastro.bodies import Sun obj = SBDB.query(name, full_precision=True, **kwargs) if "count" in obj: # No error till now ---> more than one object has been found # Contains all the name of the objects objects_name = obj["list"]["name"] objects_name_in_str = ( "" # Used to store them in string form each in new line ) for i in objects_name: objects_name_in_str += i + "\n" raise ValueError( str(obj["count"]) + " different objects found: \n" + objects_name_in_str ) if "object" not in obj.keys(): raise ValueError(f"Object {name} not found") a = obj["orbit"]["elements"]["a"] ecc = float(obj["orbit"]["elements"]["e"]) * u.one inc = obj["orbit"]["elements"]["i"] raan = obj["orbit"]["elements"]["om"] argp = obj["orbit"]["elements"]["w"] # Since JPL provides Mean Anomaly (M) we need to make # the conversion to the true anomaly (nu) M = obj["orbit"]["elements"]["ma"].to(u.rad) # NOTE: It is unclear how this conversion should happen, # see https://ssd-api.jpl.nasa.gov/doc/sbdb.html if ecc < 1: M = (M + np.pi * u.rad) % (2 * np.pi * u.rad) - np.pi * u.rad nu = E_to_nu(M_to_E(M, ecc), ecc) elif ecc == 1: nu = D_to_nu(M_to_D(M)) else: nu = F_to_nu(M_to_F(M, ecc), ecc) epoch = time.Time(obj["orbit"]["epoch"].to(u.d), format="jd") ss = cls.from_classical( Sun, a, ecc, inc, raan, argp, nu, epoch=epoch.tdb, plane=Planes.EARTH_ECLIPTIC, ) return ss