def test_LOS_event_raises_warning_if_norm_of_r1_less_than_attractor_radius_during_propagation( ): r2 = np.array([-500, 1500, 4012.09]) << u.km v2 = np.array([5021.38, -2900.7, 1000.354]) << u.km / u.s orbit = Orbit.from_vectors(Earth, r2, v2) tofs = [100, 500, 1000, 2000] << u.s # Propagate the secondary body to generate its position coordinates. rr, vv = cowell( Earth.k, orbit.r, orbit.v, tofs, ) pos_coords = rr # Trajectory of the secondary body. r1 = (np.array([0, -5010.696, -5102.509]) << u.km ) # This position vectors' norm gets less than attractor radius. v1 = np.array([736.138, 29899.7, 164.354]) << u.km / u.s orb = Orbit.from_vectors(Earth, r1, v1) los_event = LosEvent(Earth, pos_coords, terminal=True) events = [los_event] tofs = [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.5] << u.s with pytest.warns(UserWarning, match="The norm of the position vector"): r, v = cowell( Earth.k, orb.r, orb.v, tofs, events=events, )
def test_LOS_event(): t_los = 2327.165 * u.s r2 = np.array([-500, 1500, 4012.09]) << u.km v2 = np.array([5021.38, -2900.7, 1000.354]) << u.km / u.s orbit = Orbit.from_vectors(Earth, r2, v2) tofs = [100, 500, 1000, 2000] << u.s # Propagate the secondary body to generate its position coordinates. rr, vv = cowell( Earth.k, orbit.r, orbit.v, tofs, ) pos_coords = rr # Trajectory of the secondary body. orb = Orbit.from_classical(Earth, 16000 * u.km, 0.53 * u.one, 5 * u.deg, 5 * u.deg, 10 * u.deg, 30 * u.deg) los_event = LosEvent(Earth, pos_coords, terminal=True) events = [los_event] tofs = [1, 5, 10, 100, 1000, 2000, 3000, 5000] << u.s r, v = cowell( Earth.k, orb.r, orb.v, tofs, events=events, ) assert_quantity_allclose(los_event.last_t, t_los)
def _compute_results_array(a, ecc_0, ecc_f, inc_0, inc_f, argp, f): k = Earth.k.decompose([u.km, u.s]).value _, _, t_f = extra_quantities(k, a, ecc_0, ecc_f, inc_0, inc_f, argp, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical( Earth, a * u.km, ecc_0 * u.one, inc_0 * u.deg, 0 * u.deg, argp * u.deg, 0 * u.deg ) r0, v0 = s0.rv() combined_ei_accel = guidance_law(s0, ecc_f, inc_f, f) # Propagate orbit with ProgressResultsCallback(t_f) as res: cowell( k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=combined_ei_accel, callback=res, nsteps=1000000 ) return res.get_results() # ~70 k rows, 7 columns, 3 MB in memory
def test_atmospheric_demise(): # Test an orbital decay that hits Earth. No analytic solution. R = Earth.R.to(u.km).value orbit = Orbit.circular(Earth, 230 * u.km) t_decay = 48.2179 * u.d # not an analytic value # parameters of a body C_D = 2.2 # dimentionless (any value would do) A_over_m = ((np.pi / 4.0) * (u.m**2) / (100 * u.kg)).to_value( u.km**2 / u.kg) # km^2/kg # parameters of the atmosphere rho0 = rho0_earth.to(u.kg / u.km**3).value # kg/km^3 H0 = H0_earth.to(u.km).value # km tofs = [365] * u.d # actually hits the ground a bit after day 48 lithobrake_event = LithobrakeEvent(R) events = [lithobrake_event] rr, _ = cowell( Earth.k, orbit.r, orbit.v, tofs, ad=atmospheric_drag_exponential, R=R, C_D=C_D, A_over_m=A_over_m, H0=H0, rho0=rho0, events=events, ) assert_quantity_allclose(norm(rr[0].to(u.km).value), R, atol=1) # below 1km assert_quantity_allclose(lithobrake_event.last_t, t_decay, rtol=1e-2) # make sure having the event not firing is ok tofs = [1] * u.d lithobrake_event = LithobrakeEvent(R) events = [lithobrake_event] rr, _ = cowell( Earth.k, orbit.r, orbit.v, tofs, ad=atmospheric_drag_exponential, R=R, C_D=C_D, A_over_m=A_over_m, H0=H0, rho0=rho0, events=events, ) assert lithobrake_event.last_t == tofs[-1]
def time_J3_propagation(self): cowell( self.orbit.attractor.k, self.orbit.r, self.orbit.v, self.tof, ad=self.a_J2J3, rtol=1e-8, )
def time_J2_propagation(self): cowell( self.orbit.attractor.k, self.orbit.r, self.orbit.v, self.tof, ad=J2_perturbation, J2=Earth.J2.value, R=Earth.R.to(u.km).value, )
def time_atmospheric_drag_exponential(self): cowell( self.orbit.attractor.k, self.orbit.r, self.orbit.v, self.tof, ad=atmospheric_drag_exponential, R=self.R, C_D=self.C_D, A_over_m=self.A_over_m, H0=self.H0, rho0=self.rho0, )
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 orbit = Orbit.from_vectors(Earth, r0 * u.km, v0 * u.km / u.s) tofs = [48.0] * u.h rr, vv = cowell( Earth.k, orbit.r, orbit.v, tofs, ad=J2_perturbation, J2=Earth.J2.value, R=Earth.R.to(u.km).value, ) k = Earth.k.to(u.km ** 3 / u.s ** 2).value _, _, _, raan0, argp0, _ = rv2coe(k, r0, v0) _, _, _, raan, argp, _ = rv2coe(k, rr[0].to(u.km).value, vv[0].to(u.km / u.s).value) raan_variation_rate = (raan - raan0) / tofs[0].to(u.s).value argp_variation_rate = (argp - argp0) / tofs[0].to(u.s).value 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 _propagate_curOrb(self, curOrb): # propagate to get an ECI vector at each timestep # by default, this produces a list of numpy arrays where the if self.prop_method == 'cowell': (r_list, v_list) = propagation.cowell(curOrb, self.timesteps_arr, ad=J23_perturbation, J2=Earth.J2.value, J3=Earth.J3.value) elif self.prop_method == 'kepler': r_list = [curOrb.state.r.value] v_list = [curOrb.state.v.value] for _ in range(1, len(self.timesteps_arr)): # propagate assumes input is angular, unless explicity a time object curOrb = curOrb.propagate( TimeDelta(self.timestep_s, format='sec')) r_list.append(curOrb.state.r.value) v_list.append(curOrb.state.v.value) # reset back to base epoch curOrb = Orbit.from_vectors(Earth, curOrb.state.r.value * u.km, curOrb.state.v.value * u.km / u.s) else: raise NotImplementedError( 'Only cowell and singleStep implemented for poliAstro propagation' ) return (r_list, v_list)
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 orbit = Orbit.from_vectors(Earth, r0 * u.km, v0 * u.km / u.s) tofs = [48.0] * u.h def f(t0, u_, k): du_kep = func_twobody(t0, u_, k) ax, ay, az = J2_perturbation( t0, u_, k, J2=Earth.J2.value, R=Earth.R.to(u.km).value ) du_ad = np.array([0, 0, 0, ax, ay, az]) return du_kep + du_ad rr, vv = cowell(Earth.k, orbit.r, orbit.v, tofs, f=f) k = Earth.k.to(u.km ** 3 / u.s ** 2).value _, _, _, raan0, argp0, _ = rv2coe(k, r0, v0) _, _, _, raan, argp, _ = rv2coe(k, rr[0].to(u.km).value, vv[0].to(u.km / u.s).value) raan_variation_rate = (raan - raan0) / tofs[0].to(u.s).value # type: ignore argp_variation_rate = (argp - argp0) / tofs[0].to(u.s).value # type: ignore 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_cowell_propagation_circle_to_circle(): # From [Edelbaum, 1961] accel = 1e-7 def constant_accel(t0, u, k): v = u[3:] norm_v = (v[0]**2 + v[1]**2 + v[2]**2)**.5 return accel * v / norm_v ss = Orbit.circular(Earth, 500 * u.km) tof = 20 * ss.period r, v = cowell(ss, tof.to(u.s).value, ad=constant_accel) ss_final = Orbit.from_vectors( Earth, r * u.km, v * u.km / u.s) da_a0 = (ss_final.a - ss.a) / ss.a dv_v0 = abs(norm(ss_final.v) - norm(ss.v)) / norm(ss.v) assert_quantity_allclose(da_a0, 2 * dv_v0, rtol=1e-2) dv = abs(norm(ss_final.v) - norm(ss.v)) accel_dt = accel * u.km / u.s**2 * tof assert_quantity_allclose(dv, accel_dt, rtol=1e-2)
def test_atmospheric_drag(): # http://farside.ph.utexas.edu/teaching/celestial/Celestialhtml/node94.html#sair (10.148) # given the expression for \dot{r} / r, aproximate \Delta r \approx F_r * \Delta t R = Earth.R.to(u.km).value k = Earth.k.to(u.km**3 / u.s**2).value # parameters of a circular orbit with h = 250 km (any value would do, but not too small) orbit = Orbit.circular(Earth, 250 * u.km) r0, _ = orbit.rv() r0 = r0.to(u.km).value # parameters of a body C_D = 2.2 # dimentionless (any value would do) A = ((np.pi / 4.0) * (u.m**2)).to(u.km**2).value # km^2 m = 100 # kg B = C_D * A / m # parameters of the atmosphere rho0 = Earth.rho0.to(u.kg / u.km**3).value # kg/km^3 H0 = Earth.H0.to(u.km).value tof = 100000 # s dr_expected = -B * rho0 * np.exp(-(norm(r0) - R) / H0) * np.sqrt(k * norm(r0)) * tof # assuming the atmospheric decay during tof is small, # dr_expected = F_r * tof (Newton's integration formula), where # F_r = -B rho(r) |r|^2 sqrt(k / |r|^3) = -B rho(r) sqrt(k |r|) r, v = cowell(orbit, tof, ad=atmospheric_drag, R=R, C_D=C_D, A=A, m=m, H0=H0, rho0=rho0) assert_quantity_allclose(norm(r) - norm(r0), dr_expected, rtol=1e-2)
def test_solar_pressure(): # based on example 12.9 from Howard Curtis with solar_system_ephemeris.set('builtin'): j_date = 2438400.5 * u.day tof = 600 * u.day sun_r = build_ephem_interpolant(Sun, 365 * u.day, (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).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_eccs, delta_incs, delta_raans, delta_argps = [], [], [], [] 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(1.0 * check['t_days'] / tof.to(u.day).value * 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'] with solar_system_ephemeris.set('builtin'): j_date = 2454283.0 * u.day tof = (test_params['tof']).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 test_cowell_propagation_circle_to_circle(): # From [Edelbaum, 1961] accel = 1e-7 def constant_accel(t0, u, k): v = u[3:] norm_v = (v[0]**2 + v[1]**2 + v[2]**2)**.5 return accel * v / norm_v ss = Orbit.circular(Earth, 500 * u.km) tof = 20 * ss.period r0, v0 = ss.rv() k = ss.attractor.k r, v = cowell(k.to(u.km**3 / u.s**2).value, r0.to(u.km).value, v0.to(u.km / u.s).value, tof.to(u.s).value, ad=constant_accel) ss_final = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s) da_a0 = (ss_final.a - ss.a) / ss.a dv_v0 = abs(norm(ss_final.v) - norm(ss.v)) / norm(ss.v) assert_almost_equal(da_a0.value, 2 * dv_v0.value, decimal=4) dv = abs(norm(ss_final.v) - norm(ss.v)) accel_dt = accel * u.km / u.s**2 * tof assert_almost_equal(dv.value, accel_dt.value, decimal=4)
def test_sso_disposal_numerical(ecc_0, ecc_f): a_0 = Earth.R.to(u.km).value + 900 # km f = 2.4e-7 # km / s2, assumed constant k = Earth.k.decompose([u.km, u.s]).value _, t_f = extra_quantities(k, a_0, ecc_0, ecc_f, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical(Earth, a_0 * u.km, ecc_0 * u.one, 0 * u.deg, 0 * u.deg, 0 * u.deg, 0 * u.deg) r0, v0 = s0.rv() optimal_accel = guidance_law(s0, ecc_f, f) # Propagate orbit r, v = cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=optimal_accel, nsteps=1000000) sf = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s, s0.epoch + t_f * u.s) assert_allclose(sf.ecc.value, ecc_f, rtol=1e-4, 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 * u.day tof = (test_params['tof']).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 test_solar_pressure(): # based on example 12.9 from Howard Curtis solar_system_ephemeris.set('de432s') j_date = 2438400.5 * u.day tof = 600 * u.day sun_r = build_ephem_interpolant(Sun, 365 * u.day, (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).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(1.0 * check['t_days'] / tof.to(u.day).value * 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_cowell_propagation_circle_to_circle(): # From [Edelbaum, 1961] accel = 1e-7 def constant_accel(t0, u, k): v = u[3:] norm_v = (v[0]**2 + v[1]**2 + v[2]**2)**.5 return accel * v / norm_v ss = Orbit.circular(Earth, 500 * u.km) tof = 20 * ss.period r0, v0 = ss.rv() k = ss.attractor.k r, v = cowell(ss, tof.to(u.s).value, ad=constant_accel) ss_final = Orbit.from_vectors( Earth, r * u.km, v * u.km / u.s) da_a0 = (ss_final.a - ss.a) / ss.a dv_v0 = abs(norm(ss_final.v) - norm(ss.v)) / norm(ss.v) assert_quantity_allclose(da_a0, 2 * dv_v0, rtol=1e-2) dv = abs(norm(ss_final.v) - norm(ss.v)) accel_dt = accel * u.km / u.s**2 * tof assert_quantity_allclose(dv, accel_dt, rtol=1e-2)
def test_soyuz_standard_gto_numerical(): # Data from Soyuz Users Manual, issue 2 revision 0 r_a = (Earth.R + 35950 * u.km).to(u.km).value r_p = (Earth.R + 250 * u.km).to(u.km).value a = (r_a + r_p) / 2 # km ecc = r_a / a - 1 argp_0 = (178 * u.deg).to(u.rad).value # rad argp_f = (178 * u.deg + 5 * u.deg).to(u.rad).value # rad f = 2.4e-7 # km / s2 k = Earth.k.decompose([u.km, u.s]).value optimal_accel = guidance_law(f) _, t_f = extra_quantities(k, a, ecc, argp_0, argp_f, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical(Earth, a * u.km, (r_a / a - 1) * u.one, 6 * u.deg, 188.5 * u.deg, 178 * u.deg, 0 * u.deg) r0, v0 = s0.rv() # Propagate orbit r, v = cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=optimal_accel, nsteps=1000000) sf = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s, s0.epoch + t_f * u.s) assert_allclose(sf.argp.to(u.rad).value, argp_f, rtol=1e-4)
def test_solar_pressure(t_days, deltas_expected, sun_r): # based on example 12.9 from Howard Curtis with solar_system_ephemeris.set("builtin"): j_date = 2_438_400.5 * u.day tof = 600 * u.day epoch = Time(j_date, format="jd", scale="tdb") initial = Orbit.from_classical( Earth, 10085.44 * u.km, 0.025422 * u.one, 88.3924 * u.deg, 45.38124 * u.deg, 227.493 * u.deg, 343.4268 * u.deg, 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) rr, vv = cowell( Earth.k, initial.r, initial.v, np.linspace(0, (tof).to(u.s).value, 4000) * u.s, rtol=1e-8, ad=radiation_pressure, R=Earth.R.to(u.km).value, C_R=2.0, A_over_m=2e-4 / 100, Wdivc_s=Wdivc_sun.value, star=sun_normalized, ) delta_eccs, delta_incs, delta_raans, delta_argps = [], [], [], [] for ri, vi in zip(rr.to(u.km).value, vv.to(u.km / u.s).value): orbit_params = rv2coe(Earth.k.to(u.km**3 / u.s**2).value, ri, vi) delta_eccs.append(orbit_params[1] - initial.ecc.value) delta_incs.append((orbit_params[2] * u.rad).to(u.deg).value - initial.inc.value) delta_raans.append((orbit_params[3] * u.rad).to(u.deg).value - initial.raan.value) delta_argps.append((orbit_params[4] * u.rad).to(u.deg).value - initial.argp.value) # averaging over 5 last values in the way Curtis does index = int(1.0 * t_days / tof.to(u.day).value * 4000 # type: ignore ) 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], deltas_expected, rtol=1e0, # TODO: Excessively low, rewrite test? atol=1e-4, )
def test_cowell_propagation_circle_to_circle(): # From [Edelbaum, 1961] accel = 1e-7 def constant_accel(t0, u_, k): v = u_[3:] norm_v = (v[0]**2 + v[1]**2 + v[2]**2)**0.5 return accel * v / norm_v def f(t0, u_, k): du_kep = func_twobody(t0, u_, k) ax, ay, az = constant_accel(t0, u_, k) du_ad = np.array([0, 0, 0, ax, ay, az]) return du_kep + du_ad ss = Orbit.circular(Earth, 500 * u.km) tofs = [20] * ss.period r, v = cowell(Earth.k, ss.r, ss.v, tofs, f=f) ss_final = Orbit.from_vectors(Earth, r[0], v[0]) da_a0 = (ss_final.a - ss.a) / ss.a dv_v0 = abs(norm(ss_final.v) - norm(ss.v)) / norm(ss.v) assert_quantity_allclose(da_a0, 2 * dv_v0, rtol=1e-2) dv = abs(norm(ss_final.v) - norm(ss.v)) accel_dt = accel * u.km / u.s**2 * tofs[0] assert_quantity_allclose(dv, accel_dt, rtol=1e-2)
def test_leo_geo_numerical(inc_0): edelbaum_accel = guidance_law(k, a_0, a_f, inc_0, inc_f, f) _, t_f = extra_quantities(k, a_0, a_f, inc_0, inc_f, f) # Retrieve r and v from initial orbit s0 = Orbit.circular(Earth, a_0 * u.km - Earth.R, inc_0 * u.rad) r0, v0 = s0.rv() # Propagate orbit r, v = cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=edelbaum_accel, nsteps=1000000) sf = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s, s0.epoch + t_f * u.s) assert_allclose(sf.a.to(u.km).value, a_f, rtol=1e-5) assert_allclose(sf.ecc.value, 0.0, atol=1e-2) assert_allclose(sf.inc.to(u.rad).value, inc_f, atol=1e-3)
def test_umbra_event_crossing(): expected_umbra_t = Time("2020-01-01 00:04:51.328", scale="utc") # From Orekit. attractor = Earth tof = 2 * u.d epoch = Time("2020-01-01", scale="utc") coe = ( 6828137.0 * u.m, 0.0073 * u.one, 87.0 * u.deg, 20.0 * u.deg, 10.0 * u.deg, 0 * u.deg, ) orbit = Orbit.from_classical(attractor, *coe, epoch=epoch) umbra_event = UmbraEvent(orbit, terminal=True) events = [umbra_event] rr, _ = cowell( attractor.k, orbit.r, orbit.v, [tof] * u.s, events=events, ) assert expected_umbra_t.isclose(epoch + umbra_event.last_t, atol=1 * u.s)
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_geo_cases_numerical(ecc_0, ecc_f): a = 42164 # km inc_0 = 0.0 # rad, baseline inc_f = (20.0 * u.deg).to(u.rad).value # rad argp = 0.0 # rad, the method is efficient for 0 and 180 f = 2.4e-7 # km / s2 k = Earth.k.decompose([u.km, u.s]).value _, _, t_f = extra_quantities(k, a, ecc_0, ecc_f, inc_0, inc_f, argp, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical(Earth, a * u.km, ecc_0 * u.one, inc_0 * u.deg, 0 * u.deg, argp * u.deg, 0 * u.deg) r0, v0 = s0.rv() optimal_accel = guidance_law(s0, ecc_f, inc_f, f) # Propagate orbit r, v = cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=optimal_accel, nsteps=1000000) sf = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s, s0.epoch + t_f * u.s) assert_allclose(sf.ecc.value, ecc_f, rtol=1e-2, atol=1e-2) assert_allclose(sf.inc.to(u.rad).value, inc_f, rtol=1e-1)
def test_penumbra_event_crossing(): expected_penumbra_t = Time("2020-01-01 00:04:26.060", scale="utc") # From Orekit. attractor = Earth tof = 2 * u.d epoch = Time("2020-01-01", scale="utc") orbit = Orbit.from_classical( attractor=attractor, a=6828137.0 * u.m, ecc=0.0073 * u.one, inc=87.0 * u.deg, raan=20.0 * u.deg, argp=10.0 * u.deg, nu=0 * u.deg, epoch=epoch, ) penumbra_event = PenumbraEvent(orbit, terminal=True) events = [penumbra_event] rr, _ = cowell( attractor.k, orbit.r, orbit.v, [tof] * u.s, events=events, ) assert expected_penumbra_t.isclose(epoch + penumbra_event.last_t, atol=1 * u.s)
def _sample(self, time_values, method=mean_motion): # if use cowell, propagate to max_time and use other values as intermediate (dense output) if method == cowell: values, _ = cowell(self, (time_values - self.epoch).to(u.s).value) values = values * u.km else: values = np.zeros((len(time_values), 3)) * self.r.unit for ii, epoch in enumerate(time_values): rr = self.propagate(epoch, method).r values[ii] = rr return CartesianRepresentation(values, xyz_axis=1)
def test_3rd_body_Curtis(test_params): # based on example 12.11 from Howard Curtis body = test_params["body"] with solar_system_ephemeris.set("builtin"): j_date = 2454283.0 * u.day tof = (test_params["tof"]).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) rr, vv = cowell( Earth.k, initial.r, initial.v, np.linspace(0, tof, 400) * u.s, rtol=1e-10, ad=third_body, k_third=body.k.to(u.km**3 / u.s**2).value, perturbation_body=body_r, ) incs, raans, argps = [], [], [] for ri, vi in zip(rr.to(u.km).value, vv.to(u.km / u.s).value): 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 test_atmospheric_drag_exponential(): # http://farside.ph.utexas.edu/teaching/celestial/Celestialhtml/node94.html#sair (10.148) # Given the expression for \dot{r} / r, aproximate \Delta r \approx F_r * \Delta t R = Earth.R.to(u.km).value k = Earth.k.to(u.km**3 / u.s**2).value # Parameters of a circular orbit with h = 250 km (any value would do, but not too small) orbit = Orbit.circular(Earth, 250 * u.km) r0, _ = orbit.rv() r0 = r0.to(u.km).value # Parameters of a body C_D = 2.2 # dimentionless (any value would do) A_over_m = ((np.pi / 4.0) * (u.m**2) / (100 * u.kg)).to_value( u.km**2 / u.kg) # km^2/kg B = C_D * A_over_m # Parameters of the atmosphere rho0 = rho0_earth.to(u.kg / u.km**3).value # kg/km^3 H0 = H0_earth.to(u.km).value # km tof = 100000 # s dr_expected = -B * rho0 * np.exp(-(norm(r0) - R) / H0) * np.sqrt( k * norm(r0)) * tof # Assuming the atmospheric decay during tof is small, # dr_expected = F_r * tof (Newton's integration formula), where # F_r = -B rho(r) |r|^2 sqrt(k / |r|^3) = -B rho(r) sqrt(k |r|) def f(t0, u_, k): du_kep = func_twobody(t0, u_, k) ax, ay, az = atmospheric_drag_exponential(t0, u_, k, R=R, C_D=C_D, A_over_m=A_over_m, H0=H0, rho0=rho0) du_ad = np.array([0, 0, 0, ax, ay, az]) return du_kep + du_ad rr, _ = cowell( Earth.k, orbit.r, orbit.v, [tof] * u.s, f=f, ) assert_quantity_allclose(norm(rr[0].to(u.km).value) - norm(r0), dr_expected, rtol=1e-2)
def _compute_results_array(a_0, a_f, inc_0, i_f, f): k = Earth.k.decompose([u.km, u.s]).value edelbaum_accel = guidance_law(k, a_0, a_f, inc_0, i_f, f) _, t_f = extra_quantities(k, a_0, a_f, inc_0, i_f, f) # Retrieve r and v from initial orbit s0 = Orbit.circular(Earth, a_0 * u.km - Earth.R, inc_0 * u.rad) r0, v0 = s0.rv() # Propagate orbit with ProgressResultsCallback(t_f) as res: cowell( k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=edelbaum_accel, callback=res, nsteps=1000000 ) return res.get_results() # ~70 k rows, 7 columns, 3 MB in memory
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')
def test_3rd_body_Curtis(test_params): # based on example 12.11 from Howard Curtis body = test_params["body"] with solar_system_ephemeris.set("builtin"): j_date = 2454283.0 * u.day tof = (test_params["tof"]).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) rr, vv = cowell( Earth.k, initial.r, initial.v, np.linspace(0, tof, 400) * u.s, 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(rr.to(u.km).value, vv.to(u.km / u.s).value): 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 test_atmospheric_demise_coesa76(): # Test an orbital decay that hits Earth. No analytic solution. R = Earth.R.to(u.km).value orbit = Orbit.circular(Earth, 250 * u.km) t_decay = 7.17 * u.d # Parameters of a body C_D = 2.2 # Dimensionless (any value would do) A_over_m = ((np.pi / 4.0) * (u.m**2) / (100 * u.kg)).to_value( u.km**2 / u.kg) # km^2/kg tofs = [365] * u.d lithobrake_event = LithobrakeEvent(R) events = [lithobrake_event] coesa76 = COESA76() def f(t0, u_, k): du_kep = func_twobody(t0, u_, k) # Avoid undershooting H below attractor radius R H = max(norm(u_[:3]), R) rho = coesa76.density((H - R) * u.km).to_value(u.kg / u.km**3) ax, ay, az = atmospheric_drag( t0, u_, k, C_D=C_D, A_over_m=A_over_m, rho=rho, ) du_ad = np.array([0, 0, 0, ax, ay, az]) return du_kep + du_ad rr, _ = cowell( Earth.k, orbit.r, orbit.v, tofs, events=events, f=f, ) assert_quantity_allclose(norm(rr[0].to(u.km).value), R, atol=1) # Below 1km assert_quantity_allclose(lithobrake_event.last_t, t_decay, rtol=1e-2)
def _compute_results_array(a, ecc_0, ecc_f, inc_0, inc_f, argp, f): k = Earth.k.decompose([u.km, u.s]).value _, _, t_f = extra_quantities(k, a, ecc_0, ecc_f, inc_0, inc_f, argp, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical(Earth, a * u.km, ecc_0 * u.one, inc_0 * u.deg, 0 * u.deg, argp * u.deg, 0 * u.deg) r0, v0 = s0.rv() combined_ei_accel = guidance_law(s0, ecc_f, inc_f, f) # Propagate orbit with ProgressResultsCallback(t_f) as res: cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=combined_ei_accel, callback=res, nsteps=1000000) return res.get_results() # ~70 k rows, 7 columns, 3 MB in memory
def test_cowell_propagation_with_zero_acceleration_equals_kepler(): # Data from Vallado, example 2.4 k = Earth.k.to(u.km**3 / u.s**2).value r0 = np.array([1131.340, -2282.343, 6672.423]) # km v0 = np.array([-5.64305, 4.30333, 2.42879]) # km/s tof = 40 * 60.0 # s expected_r = np.array([-4219.7527, 4363.0292, -3958.7666]) expected_v = np.array([3.689866, -1.916735, -6.112511]) r, v = cowell(k, r0, v0, tof, ad=None) assert_array_almost_equal(r, expected_r, decimal=1) assert_array_almost_equal(v, expected_v, decimal=4)
def test_LOS_event_with_lithobrake_event_raises_warning_when_satellite_cuts_attractor( ): r2 = np.array([-500, 1500, 4012.09]) << u.km v2 = np.array([5021.38, -2900.7, 1000.354]) << u.km / u.s orbit = Orbit.from_vectors(Earth, r2, v2) tofs = [100, 500, 1000, 2000] << u.s # Propagate the secondary body to generate its position coordinates. rr, vv = cowell( Earth.k, orbit.r, orbit.v, tofs, ) pos_coords = rr # Trajectory of the secondary body. r1 = np.array([0, -5010.696, -5102.509]) << u.km v1 = np.array([736.138, 2989.7, 164.354]) << u.km / u.s orb = Orbit.from_vectors(Earth, r1, v1) los_event = LosEvent(Earth, pos_coords, terminal=True) tofs = [ 0.003, 0.004, 0.01, 0.02, 0.03, 0.04, 0.07, 0.1, 0.2, 0.3, 0.4, 1, 3 ] << u.s lithobrake_event = LithobrakeEvent(Earth.R.to_value(u.km)) events = [lithobrake_event, los_event] r, v = cowell( Earth.k, orb.r, orb.v, tofs, events=events, ) assert lithobrake_event.last_t < los_event.last_t
def test_cowell_propagation_with_zero_acceleration_equals_kepler(): # Data from Vallado, example 2.4 k = Earth.k.to(u.km**3 / u.s**2).value r0 = np.array([1131.340, -2282.343, 6672.423]) # km v0 = np.array([-5.64305, 4.30333, 2.42879]) # km/s tof = 40 * 60.0 # s expected_r = np.array([-4219.7527, 4363.0292, -3958.7666]) expected_v = np.array([3.689866, -1.916735, -6.112511]) r, v = cowell(k, r0, v0, tof, ad=None) assert_allclose(r, expected_r, rtol=1e-5) assert_allclose(v, expected_v, rtol=1e-4)
def test_cowell_propagation_with_zero_acceleration_equals_kepler(): # Data from Vallado, example 2.4 r0 = np.array([1131.340, -2282.343, 6672.423]) * u.km v0 = np.array([-5.64305, 4.30333, 2.42879]) * u.km / u.s tofs = [40 * 60.0] * u.s orbit = Orbit.from_vectors(Earth, r0, v0) expected_r = np.array([-4219.7527, 4363.0292, -3958.7666]) * u.km expected_v = np.array([3.689866, -1.916735, -6.112511]) * u.km / u.s r, v = cowell(Earth.k, orbit.r, orbit.v, tofs, ad=None) assert_quantity_allclose(r[0], expected_r, rtol=1e-5) assert_quantity_allclose(v[0], expected_v, rtol=1e-4)
def test_cowell_propagation_with_zero_acceleration_equals_kepler(): # Data from Vallado, example 2.4 r0 = np.array([1131.340, -2282.343, 6672.423]) # km v0 = np.array([-5.64305, 4.30333, 2.42879]) # km/s tof = 40 * 60.0 # s orbit = Orbit.from_vectors(Earth, r0 * u.km, v0 * u.km / u.s) expected_r = np.array([-4219.7527, 4363.0292, -3958.7666]) expected_v = np.array([3.689866, -1.916735, -6.112511]) r, v = cowell(orbit, tof, ad=None) assert_allclose(r, expected_r, rtol=1e-5) assert_allclose(v, expected_v, rtol=1e-4)
def test_cowell_propagation_callback(): # Data from Vallado, example 2.4 k = Earth.k.to(u.km**3 / u.s**2).value r0 = np.array([1131.340, -2282.343, 6672.423]) # km v0 = np.array([-5.64305, 4.30333, 2.42879]) # km/s tof = 40 * 60.0 # s results = [] def cb(t, u_): row = [t] row.extend(u_) results.append(row) r, v = cowell(k, r0, v0, tof, callback=cb) assert len(results) == 17 assert len(results[0]) == 7 assert results[-1][0] == tof
def test_soyuz_standard_gto_numerical(): # Data from Soyuz Users Manual, issue 2 revision 0 r_a = (Earth.R + 35950 * u.km).to(u.km).value r_p = (Earth.R + 250 * u.km).to(u.km).value a = (r_a + r_p) / 2 # km ecc = r_a / a - 1 argp_0 = (178 * u.deg).to(u.rad).value # rad argp_f = (178 * u.deg + 5 * u.deg).to(u.rad).value # rad f = 2.4e-7 # km / s2 k = Earth.k.decompose([u.km, u.s]).value optimal_accel = guidance_law(f) _, t_f = extra_quantities(k, a, ecc, argp_0, argp_f, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical( Earth, a * u.km, (r_a / a - 1) * u.one, 6 * u.deg, 188.5 * u.deg, 178 * u.deg, 0 * u.deg ) r0, v0 = s0.rv() # Propagate orbit r, v = cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=optimal_accel, nsteps=1000000) sf = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s, s0.epoch + t_f * u.s) assert_allclose(sf.argp.to(u.rad).value, argp_f, rtol=1e-4)
def test_geo_cases_numerical(ecc_0, ecc_f): a = 42164 # km inc_0 = 0.0 # rad, baseline inc_f = (20.0 * u.deg).to(u.rad).value # rad argp = 0.0 # rad, the method is efficient for 0 and 180 f = 2.4e-7 # km / s2 k = Earth.k.decompose([u.km, u.s]).value _, _, t_f = extra_quantities(k, a, ecc_0, ecc_f, inc_0, inc_f, argp, f) # Retrieve r and v from initial orbit s0 = Orbit.from_classical( Earth, a * u.km, ecc_0 * u.one, inc_0 * u.deg, 0 * u.deg, argp * u.deg, 0 * u.deg ) r0, v0 = s0.rv() optimal_accel = guidance_law(s0, ecc_f, inc_f, f) # Propagate orbit r, v = cowell(k, r0.to(u.km).value, v0.to(u.km / u.s).value, t_f, ad=optimal_accel, nsteps=1000000) sf = Orbit.from_vectors(Earth, r * u.km, v * u.km / u.s, s0.epoch + t_f * u.s) assert_allclose(sf.ecc.value, ecc_f, rtol=1e-2, atol=1e-2) assert_allclose(sf.inc.to(u.rad).value, inc_f, rtol=1e-1)