def test_J2_propagation_Earth(): # from Curtis example 12.2: r0 = np.array([-2384.46, 5729.01, 3050.46]) # km v0 = np.array([-7.36138, -2.98997, 1.64354]) # km/s k = Earth.k.to(u.km**3 / u.s**2).value orbit = Orbit.from_vectors(Earth, r0 * u.km, v0 * u.km / u.s) tof = (48.0 * u.h).to(u.s).value r, v = cowell(orbit, tof, ad=J2_perturbation, J2=Earth.J2.value, R=Earth.R.to(u.km).value) _, _, _, raan0, argp0, _ = rv2coe(k, r0, v0) _, _, _, raan, argp, _ = rv2coe(k, r, v) raan_variation_rate = (raan - raan0) / tof argp_variation_rate = (argp - argp0) / tof raan_variation_rate = (raan_variation_rate * u.rad / u.s).to(u.deg / u.h) argp_variation_rate = (argp_variation_rate * u.rad / u.s).to(u.deg / u.h) assert_quantity_allclose(raan_variation_rate, -0.172 * u.deg / u.h, rtol=1e-2) assert_quantity_allclose(argp_variation_rate, 0.282 * u.deg / u.h, rtol=1e-2)
def test_long_propagation_preserves_orbit_elements(method): tof = 100 * u.year r_halleys = np.array([-9018878.63569932, -94116054.79839276, 22619058.69943215]) # km v_halleys = np.array([-49.95092305, -12.94843055, -4.29251577]) # km/s halleys = Orbit.from_vectors(Sun, r_halleys * u.km, v_halleys * u.km / u.s) params_ini = rv2coe(Sun.k.to(u.km**3 / u.s**2).value, r_halleys, v_halleys)[:-1] r_new, v_new = halleys.propagate(tof, method=method).rv() params_final = rv2coe(Sun.k.to(u.km**3 / u.s**2).value, r_new.to(u.km).value, v_new.to(u.km / u.s).value)[:-1] print(params_ini, params_final) assert_quantity_allclose(params_ini, params_final)
def propagate(self, value, method=mean_motion, rtol=1e-10): """ if value is true anomaly, propagate orbit to this anomaly and return the result if time is provided, propagate this `Orbit` some `time` and return the result. Parameters ---------- value : Multiple options True anomaly values, Time values. rtol : float, optional Relative tolerance for the propagation algorithm, default to 1e-10. """ if hasattr(value, "unit") and value.unit in ('rad', 'deg'): p, ecc, inc, raan, argp, _ = rv.rv2coe(self.attractor.k.to(u.km ** 3 / u.s ** 2).value, self.r.to(u.km).value, self.v.to(u.km / u.s).value) return self.from_classical(self.attractor, p / (1.0 - ecc ** 2) * u.km, ecc * u.one, inc * u.rad, raan * u.rad, argp * u.rad, value) else: if isinstance(value, time.Time) and not isinstance(value, time.TimeDelta): time_of_flight = value - self.epoch else: time_of_flight = time.TimeDelta(value) return propagate(self, time_of_flight, method=method, rtol=rtol)
def inertial_body_centered_to_pqw(r, v, source_body): """Converts position and velocity from inertial body-centered frame to perifocal frame. Parameters ---------- r : ~astropy.units.Quantity Position vector in a inertial body-centered reference frame. v : ~astropy.units.Quantity Velocity vector in a inertial body-centered reference frame. source_body : Body Source body. Returns ------- r_pqw, v_pqw : tuple (~astropy.units.Quantity) Position and velocity vectors in ICRS. """ r = r.to('km').value v = v.to('km/s').value k = source_body.k.to('km^3 / s^2').value p, ecc, inc, _, _, nu = rv2coe(k, r, v) r_pqw = (np.array([cos(nu), sin(nu), 0 * nu]) * p / (1 + ecc * cos(nu))).T * u.km v_pqw = (np.array([-sin(nu), (ecc + cos(nu)), 0]) * sqrt(k / p)).T * u.km / u.s return r_pqw, v_pqw
def test_solar_pressure(): # based on example 12.9 from Howard Curtis solar_system_ephemeris.set('de432s') j_date = 2438400.5 tof = 600 sun_r = build_ephem_interpolant(Sun, 365, (j_date, j_date + tof), rtol=1e-2) epoch = Time(j_date, format='jd', scale='tdb') drag_force_orbit = [10085.44 * u.km, 0.025422 * u.one, 88.3924 * u.deg, 45.38124 * u.deg, 227.493 * u.deg, 343.4268 * u.deg] initial = Orbit.from_classical(Earth, *drag_force_orbit, epoch=epoch) # in Curtis, the mean distance to Sun is used. In order to validate against it, we have to do the same thing sun_normalized = functools.partial(normalize_to_Curtis, sun_r=sun_r) r, v = cowell(initial, np.linspace(0, (tof * u.day).to(u.s).value, 4000), rtol=1e-8, ad=radiation_pressure, R=Earth.R.to(u.km).value, C_R=2.0, A=2e-4, m=100, Wdivc_s=Sun.Wdivc.value, star=sun_normalized) delta_as, delta_eccs, delta_incs, delta_raans, delta_argps, delta_hs = [], [], [], [], [], [] for ri, vi in zip(r, v): orbit_params = rv2coe(Earth.k.to(u.km**3 / u.s**2).value, ri, vi) delta_eccs.append(orbit_params[1] - drag_force_orbit[1].value) delta_incs.append((orbit_params[2] * u.rad).to(u.deg).value - drag_force_orbit[2].value) delta_raans.append((orbit_params[3] * u.rad).to(u.deg).value - drag_force_orbit[3].value) delta_argps.append((orbit_params[4] * u.rad).to(u.deg).value - drag_force_orbit[4].value) # averaging over 5 last values in the way Curtis does for check in solar_pressure_checks: index = int(check['t_days'] / tof * 4000) delta_ecc, delta_inc, delta_raan, delta_argp = np.mean(delta_eccs[index - 5:index]), \ np.mean(delta_incs[index - 5:index]), np.mean(delta_raans[index - 5:index]), \ np.mean(delta_argps[index - 5:index]) assert_quantity_allclose([delta_ecc, delta_inc, delta_raan, delta_argp], check['deltas_expected'], rtol=1e-1, atol=1e-4)
def test_3rd_body_Curtis(test_params): # based on example 12.11 from Howard Curtis body = test_params['body'] solar_system_ephemeris.set('de432s') j_date = 2454283.0 tof = (test_params['tof'] * u.day).to(u.s).value body_r = build_ephem_interpolant(body, test_params['period'], (j_date, j_date + test_params['tof']), rtol=1e-2) epoch = Time(j_date, format='jd', scale='tdb') initial = Orbit.from_classical(Earth, *test_params['orbit'], epoch=epoch) r, v = cowell(initial, np.linspace(0, tof, 400), rtol=1e-10, ad=third_body, k_third=body.k.to(u.km**3 / u.s**2).value, third_body=body_r) incs, raans, argps = [], [], [] for ri, vi in zip(r, v): angles = Angle(rv2coe(Earth.k.to(u.km**3 / u.s**2).value, ri, vi)[2:5] * u.rad) # inc, raan, argp angles = angles.wrap_at(180 * u.deg) incs.append(angles[0].value) raans.append(angles[1].value) argps.append(angles[2].value) # averaging over 5 last values in the way Curtis does inc_f, raan_f, argp_f = np.mean(incs[-5:]), np.mean(raans[-5:]), np.mean(argps[-5:]) assert_quantity_allclose([(raan_f * u.rad).to(u.deg) - test_params['orbit'][3], (inc_f * u.rad).to(u.deg) - test_params['orbit'][2], (argp_f * u.rad).to(u.deg) - test_params['orbit'][4]], [test_params['raan'], test_params['inc'], test_params['argp']], rtol=1e-1)
def mean_motion(k, r0, v0, tof, **kwargs): r"""Propagates orbit using mean motion Parameters ---------- k : float Gravitational constant of main attractor (km^3 / s^2). r0 : array Initial position (km). v0 : array Initial velocity (km). ad : function(t0, u, k), optional Non Keplerian acceleration (km/s2), default to None. tof : float Time of flight (s). Notes ----- This method takes initial :math:`\vec{r}, \vec{v}`, calculates classical orbit parameters, increases mean anomaly and performs inverse transformation to get final :math:`\vec{r}, \vec{v}` The logic is based on formulae (4), (6) and (7) from http://dx.doi.org/10.1007/s10569-013-9476-9 """ # get the initial true anomaly and orbit parameters that are constant over time p, ecc, inc, raan, argp, nu0 = rv2coe(k, r0, v0) # get the initial mean anomaly M0 = nu_to_M(nu0, ecc) # elliptic or hyperbolic orbits if not np.isclose(ecc, 1.0, rtol=1e-06): a = p / (1.0 - ecc**2) # given the initial mean anomaly, calculate mean anomaly # at the end, mean motion (n) equals sqrt(mu / |a^3|) with u.set_enabled_equivalencies(u.dimensionless_angles()): M = M0 + tof * np.sqrt(k / np.abs(a**3)) * u.rad nu = M_to_nu(M, ecc) # parabolic orbit else: q = p / 2.0 # mean motion n = sqrt(mu / 2 q^3) for parabolic orbit with u.set_enabled_equivalencies(u.dimensionless_angles()): M = M0 + tof * np.sqrt(k / (2.0 * q**3)) # using Barker's equation, which is solved analytically # for parabolic orbit, get true anomaly B = 3.0 * M / 2.0 A = (B + np.sqrt(1.0 + B**2))**(2.0 / 3.0) D = 2.0 * A * B / (1.0 + A + A**2) nu = 2.0 * np.arctan(D) with u.set_enabled_equivalencies(u.dimensionless_angles()): return coe2rv(k, p, ecc, inc, raan, argp, nu)
def mean_motion(orbit, tof, **kwargs): r"""Propagates orbit using mean motion .. versionadded:: 0.9.0 Parameters ---------- orbit : ~poliastro.twobody.orbit.Orbit the Orbit object to propagate. tof : float Time of flight (s). Notes ----- This method takes initial :math:`\vec{r}, \vec{v}`, calculates classical orbit parameters, increases mean anomaly and performs inverse transformation to get final :math:`\vec{r}, \vec{v}` The logic is based on formulae (4), (6) and (7) from http://dx.doi.org/10.1007/s10569-013-9476-9 """ k = orbit.attractor.k.to(u.km**3 / u.s**2).value r0 = orbit.r.to(u.km).value v0 = orbit.v.to(u.km / u.s).value # get the initial true anomaly and orbit parameters that are constant over time p, ecc, inc, raan, argp, nu0 = rv2coe(k, r0, v0) # get the initial mean anomaly M0 = nu_to_M(nu0, ecc) # elliptic or hyperbolic orbits if not np.isclose(ecc, 1.0, rtol=1e-06): a = p / (1.0 - ecc**2) # given the initial mean anomaly, calculate mean anomaly # at the end, mean motion (n) equals sqrt(mu / |a^3|) with u.set_enabled_equivalencies(u.dimensionless_angles()): M = M0 + tof * np.sqrt(k / np.abs(a**3)) * u.rad nu = M_to_nu(M, ecc) # parabolic orbit else: q = p / 2.0 # mean motion n = sqrt(mu / 2 q^3) for parabolic orbit with u.set_enabled_equivalencies(u.dimensionless_angles()): M = M0 + tof * np.sqrt(k / (2.0 * q**3)) # using Barker's equation, which is solved analytically # for parabolic orbit, get true anomaly B = 3.0 * M / 2.0 A = (B + np.sqrt(1.0 + B**2))**(2.0 / 3.0) D = 2.0 * A * B / (1.0 + A + A**2) nu = 2.0 * np.arctan(D) with u.set_enabled_equivalencies(u.dimensionless_angles()): return coe2rv(k, p, ecc, inc, raan, argp, nu)
def test_convert_between_coe_and_rv_is_transitive(): k = Earth.k.to(u.km**3 / u.s**2).value # u.km**3 / u.s**2 p = 11067.790 # u.km ecc = 0.83285 # u.one inc = np.deg2rad(87.87) # u.rad raan = np.deg2rad(227.89) # u.rad argp = np.deg2rad(53.38) # u.rad nu = np.deg2rad(92.335) # u.rad expected_res = (p, ecc, inc, raan, argp, nu) res = rv2coe(k, *coe2rv(k, *expected_res)) assert_allclose(res, expected_res)
def test_convert_between_coe_and_rv_is_transitive(): k = Earth.k.to(u.km**3 / u.s**2).value # u.km**3 / u.s**2 p = 11067.790 # u.km ecc = 0.83285 # u.one inc = np.deg2rad(87.87) # u.rad raan = np.deg2rad(227.89) # u.rad argp = np.deg2rad(53.38) # u.rad nu = np.deg2rad(92.335) # u.rad expected_res = (p, ecc, inc, raan, argp, nu) res = rv2coe(k, *coe2rv(k, *expected_res)) assert_array_almost_equal(res, expected_res)
def test_propagation_mean_motion_parabolic(): # example from Howard Curtis (3rd edition), section 3.5, problem 3.15 p = 2.0 * 6600 * u.km _a = 0.0 * u.deg orbit = Orbit.parabolic(Earth, p, _a, _a, _a, _a) orbit = orbit.propagate(0.8897 / 2.0 * u.h, method=mean_motion) _, _, _, _, _, nu0 = rv2coe(Earth.k.to(u.km**3 / u.s**2).value, orbit.r.to(u.km).value, orbit.v.to(u.km / u.s).value) assert_quantity_allclose(nu0, np.deg2rad(90.0), rtol=1e-4) orbit = Orbit.parabolic(Earth, p, _a, _a, _a, _a) orbit = orbit.propagate(36.0 * u.h, method=mean_motion) assert_quantity_allclose(norm(orbit.r.to(u.km).value), 304700.0, rtol=1e-4)
def test_J3_propagation_Earth(test_params): # Nai-ming Qi, Qilong Sun, Yong Yang, (2018) "Effect of J3 perturbation on satellite position in LEO", # Aircraft Engineering and Aerospace Technology, Vol. 90 Issue: 1, # pp.74-86, https://doi.org/10.1108/AEAT-03-2015-0092 a_ini = 8970.667 * u.km ecc_ini = 0.25 * u.one raan_ini = 1.047 * u.rad nu_ini = 0.0 * u.rad argp_ini = 1.0 * u.rad inc_ini = test_params['inc'] k = Earth.k.to(u.km**3 / u.s**2).value orbit = Orbit.from_classical(Earth, a_ini, ecc_ini, inc_ini, raan_ini, argp_ini, nu_ini) tof = (10.0 * u.day).to(u.s).value r_J2, v_J2 = cowell(orbit, np.linspace(0, tof, int(1e+3)), ad=J2_perturbation, J2=Earth.J2.value, R=Earth.R.to(u.km).value, rtol=1e-8) a_J2J3 = lambda t0, u_, k_: J2_perturbation(t0, u_, k_, J2=Earth.J2.value, R=Earth.R.to(u.km).value) + \ J3_perturbation(t0, u_, k_, J3=Earth.J3.value, R=Earth.R.to(u.km).value) r_J3, v_J3 = cowell(orbit, np.linspace(0, tof, int(1e+3)), ad=a_J2J3, rtol=1e-8) a_values_J2 = np.array([ rv2coe(k, ri, vi)[0] / (1.0 - rv2coe(k, ri, vi)[1]**2) for ri, vi in zip(r_J2, v_J2) ]) a_values_J3 = np.array([ rv2coe(k, ri, vi)[0] / (1.0 - rv2coe(k, ri, vi)[1]**2) for ri, vi in zip(r_J3, v_J3) ]) da_max = np.max(np.abs(a_values_J2 - a_values_J3)) ecc_values_J2 = np.array( [rv2coe(k, ri, vi)[1] for ri, vi in zip(r_J2, v_J2)]) ecc_values_J3 = np.array( [rv2coe(k, ri, vi)[1] for ri, vi in zip(r_J3, v_J3)]) decc_max = np.max(np.abs(ecc_values_J2 - ecc_values_J3)) inc_values_J2 = np.array( [rv2coe(k, ri, vi)[2] for ri, vi in zip(r_J2, v_J2)]) inc_values_J3 = np.array( [rv2coe(k, ri, vi)[2] for ri, vi in zip(r_J3, v_J3)]) dinc_max = np.max(np.abs(inc_values_J2 - inc_values_J3)) assert_quantity_allclose(dinc_max, test_params['dinc_max'], rtol=1e-1, atol=1e-7) assert_quantity_allclose(decc_max, test_params['decc_max'], rtol=1e-1, atol=1e-7) try: assert_quantity_allclose(da_max * u.km, test_params['da_max']) except AssertionError as exc: pytest.xfail('this assertion disagrees with the paper')