def filter_expected(function_name: str, data): xsuccess = [] xfail = [] for test_input, test_output in data: input_str = str(test_input) if isinstance(test_output, str): # Expect fail --> Make pytest.param marked with xfai # e.g. pytest.param("6*9", 42, marks=pytest.mark.xfail) xfail.append( pytest.param( f"{function_name}(ephemerides,{input_str})", None, marks=pytest.mark.xfail(raises=ValueError), )) else: # Expect success -> make tuple list ("function(input)", test_output) eph = get_ephemerides_on_day(get_ephemerides(), test_input) sun = list(eph["sun"]) earth = list(eph["earth"]) mars = list(eph["mars"]) del sun[2] del earth[2] del mars[2] xsuccess.append(([sun, earth, mars], test_output)) return xsuccess, xfail
def test_invalid_days(test_input, expected): """ Tests of get_ephemerides_on_day""" # 1. We have to use pytest.approx since there is a difference on the rounding of the # very last decimal in reading in the .csv between Python and Mathematica. # 2. We have to iterate over the input/output with for loop since pytest.approx does # not support nested structures. ephemerides = get_ephemerides() # pylint: disable=W0612 assert eval(test_input) == expected # pylint: disable=W0123
def process_ephemerides_for_coordinate_function(function_name: str, data): coords = [] for test_input, expected in data: # Expect success -> make tuple list ("function(input)", test_output) output = list( get_coordinates_on_day_rad( get_ephemerides_on_day(get_ephemerides(), test_input))) coords.append((output, expected)) return coords
def get_leo_position_and_velocity(day=0, altitude=160, max_year="2020"): """Calculate direction of initial velocity vector. Assumes ephemerides are given with 1 day interval. With a series of cross products, calculate a LEO position perpendicular and velocity parallel to Earth's velocity vector in same direction and plane as Earth. Arguments: ephemerides {[dict of pandas dataframe]} -- ephemerides table. day {[int]} -- day (day=0 at 2019-01-01 00:00:00) Keyword Arguments: orientation {str} -- Wanted LEO orbit orientation (default: {'ecliptic'}) Returns: List[List[int]] -- Initial LEO position vector, spherical [AU/y, rad, rad] initial LEO velocity vector, spherical [AU/y, rad/y, rad/y] """ # region --- 1 Calculate Earth velocity and speed (cartesian & spherical) # Earth velocity at day = dr/dt ≈ Δr/Δt, where Δt = 1 day # i.e. Earth velocity estimated by difference of position vector 1 day apart / 1 day ephemerides = get_ephemerides(max_year=max_year) eph_day = get_ephemerides_on_day(ephemerides, day) eph_daym1 = get_ephemerides_on_day(ephemerides, day - 1) earth_day = eph_day["earth"] earth_daym1 = eph_daym1["earth"] earth_diff = earth_day - earth_daym1 # Get positions of Earth at {day, day-1} in spherical and cartesian coordinates earth_q0_spherical_AU_deg = [earth_day["r"], earth_day["theta"], earth_day["phi"]] earth_q0_cartesian_AU = [earth_day["x"], earth_day["y"], earth_day["z"]] earth_qm1_spherical_AU = [ earth_daym1["r"], earth_daym1["theta"], earth_daym1["phi"], ] earth_qm1_cartesian_AU = [earth_daym1["x"], earth_daym1["y"], earth_daym1["z"]] # Average r and theta needed for speed computation in spherical coordinates r_average_AU = np.mean([earth_day["r"], earth_daym1["r"]]) r_average_km = r_average_AU * UNIT_LENGTH logging.debug(f"r_average: {r_average_AU} AU = {r_average_km} km") theta_average_deg = np.mean([earth_day["theta"], earth_daym1["theta"]]) theta_average_rad = radians(theta_average_deg) logging.debug(f"theta_average: {theta_average_deg} deg = {theta_average_rad} rad") # Earth velocity vector in spherical and cartesian coordinates and convert units earth_qdot0_spherical_au_day_deg = [ earth_diff["r"], earth_diff["theta"], earth_diff["phi"], ] earth_qdot0_spherical_km_s_rad = [ earth_qdot0_spherical_au_day_deg[0] / (24 * 3600) * UNIT_LENGTH, radians(earth_qdot0_spherical_au_day_deg[1]) / (24 * 3600), radians(earth_qdot0_spherical_au_day_deg[2]) / (24 * 3600), ] earth_qdot0_cartesian_au_day = [earth_diff["x"], earth_diff["y"], earth_diff["z"]] earth_qdot0_cartesian_km_s = [ earth_qdot0_cartesian_au_day[0] / (24 * 3600) * UNIT_LENGTH, earth_qdot0_cartesian_au_day[1] / (24 * 3600) * UNIT_LENGTH, earth_qdot0_cartesian_au_day[2] / (24 * 3600) * UNIT_LENGTH, ] # Speeds earth_qdot0_spherical_km_s_rad_speed = get_speed_spherical( r_average_km, theta_average_rad, earth_qdot0_spherical_km_s_rad[0], # rdot earth_qdot0_spherical_km_s_rad[1], # thetadot earth_qdot0_spherical_km_s_rad[2], # phidot ) earth_qdot0_cartesian_au_day_speed = np.linalg.norm(earth_qdot0_cartesian_au_day) earth_qdot0_cartesian_km_s_speed = np.linalg.norm(earth_qdot0_cartesian_km_s) # Logs logging.debug( f"Earth initial position at day {day} (cartesian, AU): " f"{earth_q0_cartesian_AU}" ) logging.debug( f"Earth initial position at day {day-1} (cartesian, AU): " f"{earth_qm1_cartesian_AU}" ) logging.debug( f"Earth initial position at day {day} (spherical, AU & deg): " f"{earth_q0_spherical_AU_deg}" ) logging.debug( f"Earth initial position at day {day-1} (spherical, AU & deg): " f"{earth_qm1_spherical_AU}" ) logging.debug( f"Earth initial velocity (spherical, AU/d & deg/d): " f"{earth_qdot0_spherical_au_day_deg}" ) logging.debug( f"Earth initial velocity (spherical, km/s & rad/s): " f"{earth_qdot0_spherical_km_s_rad} " f"(speed: {earth_qdot0_spherical_km_s_rad_speed})" ) logging.debug( f"Earth initial velocity (cartesian, AU/d): {earth_qdot0_cartesian_au_day}" f" (speed: {earth_qdot0_cartesian_au_day_speed})" ) logging.debug( f"Earth initial velocity (cartesian, km/s): {earth_qdot0_cartesian_km_s}" f" (speed: {earth_qdot0_cartesian_km_s_speed})" ) # endregion # region --- 2 Earth plane vector earth_orbital_plane = np.cross(earth_q0_cartesian_AU, earth_qdot0_cartesian_au_day) earth_orbital_plane /= np.linalg.norm(earth_orbital_plane) logging.debug(f"Ecliptic plane vector: {earth_orbital_plane}") # endregion # region --- 3 Spacecraft initial position # Spacecraft geocentric position: perpendicular to earth velocity pointing outwards # (i.e. chosen such that it's the one pointing outwards from elliptical orbit) # (note this means spacecraft speed != earth speed (helio) + spacecraft speed (geo)) q0_geocentric_cartesian_unit = np.cross( earth_qdot0_cartesian_km_s, earth_orbital_plane ) q0_geocentric_cartesian_unit /= np.linalg.norm(q0_geocentric_cartesian_unit) q0_geocentric_cartesian_km = q0_geocentric_cartesian_unit * ( EARTH_RADIUS + altitude ) q0_geocentric_cartesian_AU = q0_geocentric_cartesian_km / UNIT_LENGTH q0_cartesian_AU = earth_q0_cartesian_AU + q0_geocentric_cartesian_AU q0_cartesian_km = q0_cartesian_AU * UNIT_LENGTH q0_spherical_AU_rad = list(get_position_spherical_from_cartesian(*q0_cartesian_AU)) q0_spherical_AU_deg = list(q0_spherical_AU_rad) # copy, not reference q0_spherical_AU_deg[1] = degrees(q0_spherical_AU_deg[1]) q0_spherical_AU_deg[2] = degrees(q0_spherical_AU_deg[2]) logging.debug( f"Spacecraft initial position unit vector (geocentric, cartesian): " f"{q0_geocentric_cartesian_unit}" ) logging.debug( f"Spacecraft initial position (geocentric, cartesian, km): " f"{q0_geocentric_cartesian_km}" f" (distance from Earth center: {np.linalg.norm(q0_geocentric_cartesian_km)})" ) logging.debug( f"Spacecraft initial position (geocentric, cartesian, AU): " f"{q0_geocentric_cartesian_AU}" f" (distance from Earth center: {np.linalg.norm(q0_geocentric_cartesian_AU)})" ) logging.debug( f"Spacecraft initial position (heliocentric, cartesian, AU): " f"{q0_cartesian_AU}" ) logging.debug( f"Spacecraft initial position (heliocentric, cartesian, km): " f"{q0_cartesian_km}" ) logging.debug( f"Spacecraft initial position (heliocentric, spherical, AU & rad): " f"{q0_spherical_AU_rad}" ) logging.debug( f"Spacecraft initial position (heliocentric, spherical, AU & deg): " f"{q0_spherical_AU_deg}" ) # endregion # region --- 4 Spacecraft initial velocity # Spacecraft velocity = Earth velocity + leo speed (same direction as Earth) leo_speed = get_circular_orbit_speed("Earth", altitude) qdot0_cartesian_unit = list(earth_qdot0_cartesian_km_s) qdot0_cartesian_unit /= np.linalg.norm(qdot0_cartesian_unit) qdot0_cartesian_km_s = earth_qdot0_cartesian_km_s + qdot0_cartesian_unit * leo_speed qdot0_cartesian_km_s_speed = np.linalg.norm(qdot0_cartesian_km_s) # Get spherical velocity vector from cartesian velocity vector qdot0_spherical_km_s_rad = get_velocity_spherical_from_cartesian( q0_cartesian_km, qdot0_cartesian_km_s ) qdot0_spherical_km_s_rad_speed = get_speed_spherical( r_average_km, theta_average_rad, qdot0_spherical_km_s_rad[0], # rdot qdot0_spherical_km_s_rad[1], # thetadot qdot0_spherical_km_s_rad[2], # phidot ) qdot0_spherical_AU_rad_y = [ qdot0_spherical_km_s_rad[0] / UNIT_VELOCITY, # AU/y qdot0_spherical_km_s_rad[1] * UNIT_TIME, # rad/y qdot0_spherical_km_s_rad[2] * UNIT_TIME, # rad/y ] qdot0_spherical_AU_rad_year_speed = get_speed_spherical( r_average_AU, theta_average_rad, qdot0_spherical_AU_rad_y[0], # rdot qdot0_spherical_AU_rad_y[1], # thetadot qdot0_spherical_AU_rad_y[2], # phidot ) logging.debug( f"Spacecraft initial velocity unit vector (cartesian, km/s): " f"{qdot0_cartesian_unit}" ) logging.debug( f"Spacecraft initial velocity vector (cartesian, km/s): {qdot0_cartesian_km_s}" f" (speed: {qdot0_cartesian_km_s_speed})" ) logging.debug( f"Spacecraft initial velocity vector (spherical, km/s & rad/s): " f"{qdot0_spherical_km_s_rad}" f" (speed: {qdot0_spherical_km_s_rad_speed})" ) logging.debug( f"Spacecraft initial velocity vector (spherical, AU/y & rad/y): " f"{qdot0_spherical_AU_rad_y}" f" (speed: {qdot0_spherical_AU_rad_year_speed})" ) # endregion # region --- 5 Calculate B0 from Q0, Qdot0 # FINAL OUTPUT: Initial coordinates (Q) Q0 = q0_spherical_AU_rad # FINAL OUTPUT: Initial momenta per mass (B) R, theta, _ = Q0 Rdot, thetadot, phidot = qdot0_spherical_AU_rad_y B_R = get_B_R(Rdot) B_theta = get_B_theta(R, thetadot) B_phi = get_B_phi(R, theta, phidot) B0 = [B_R, B_theta, B_phi] # Info log output logging.info( f"Spacecraft initial position vector, Q0 (spherical, AU & rad): " f"{Q0}" ) logging.info( f"Spacecraft initial momentum per mass vector, B0 (AU/y & rad/y): {B0}" ) # endregion return Q0, B0
def test_invalid_end_year(): """Tests of get_ephemerides (INVALID YEARS)""" get_ephemerides(end_year="2042")
def test_invalid_planets(): """Tests of get_ephemerides (INVALID PLANETS)""" get_ephemerides(planets=("earth", "mercury"))
def simulate( psi, max_year="2039", h=1 / UNIT_TIME, max_duration=1 * 3600 * 24 / UNIT_TIME, max_iter=int(1e6), ): """Simple simulator that will run a LEO until duration or max_iter is reached. Keyword Arguments: psi {tuple} -- Initial conditions: (day, Q0, B0, burn) max_year {string} -- Max year for ephemerides table (default: "2020") h {float} -- Initial time step size (default: 1/UNIT_LENGTH = 1 second in years) max_duration {int} -- Max duration of simulation (in years) (default: {1 day}) max_iter {int} -- Max number of iterations of simulation (default: {1e6}) (1e6 iterations corresponds to ~11 days with h = 1 s) Returns: [type] -- [description] """ logging.info("STARTING: Simple simulation.") t_start = time.time() max_iter = int(max_iter) # Unpack psi day = psi[0] Q = psi[1] B = psi[2] delta_v0 = psi[3] t = day * 3600 * 24 / UNIT_TIME t0 = t # Read ephemerides logging.debug("Getting ephemerides tables") ephemerides = get_ephemerides(max_year=max_year) # Apply initial burn if delta_v input is provided if delta_v0: B = apply_delta_v(Q, B, delta_v0) # Unpack initial position (Q) and momenta (B) R, theta, phi = Q B_R, B_theta, B_phi = B logging.info(f"Initial coordinates: Q = {B} (R, theta, phi)") logging.info(f"Initial momenta: B = {B} (B_R, B_theta, B_phi") logging.info( f"Starting simulation at time {t} ({day} days) with step size h = {h} " f"({h*UNIT_TIME} s)" f", max {max_iter} iterations and max {max_duration*UNIT_TIME/3600/24} days" ) # List initialization i = 0 ts = [] days = [] Qs = [] Bs = [] q_p_list = [] eph_body_coords = [] body_distances = [] # Run iteration 0 manually ts.append(t) days.append(day) Qs.append([R, theta, phi]) Bs.append([B_R, B_theta, B_phi]) q_p_list.append((Qs[0], Bs[0])) t1 = time.time() sim_time = t1 - t_start logging.info(f"Iteration {str(i).rjust(len(str(max_iter)))} / {max_iter}" f", in-sim time {format_time(t, time_unit='years')} / " f"{format_time(max_duration, time_unit='years')}" f" (out-of-sim elapsed time: {format_time(sim_time)})") # Iteration loop while True: i += 1 t += h day = t * UNIT_TIME / (3600 * 24) eph_on_day = get_ephemerides_on_day(ephemerides, day) eph_coords = get_coordinates_on_day_rad(eph_on_day) # Save ephemerides coords on day into array R_ks, theta_ks, phi_ks = eph_coords sun = [R_ks[0], theta_ks[0], phi_ks[0]] earth = [R_ks[1], theta_ks[1], phi_ks[1]] mars = [R_ks[2], theta_ks[2], phi_ks[2]] eph_body_coords.append([sun, earth, mars]) dist_sun = get_distance_spherical(Q, sun) * UNIT_LENGTH dist_earth = get_distance_spherical(Q, earth) * UNIT_LENGTH dist_mars = get_distance_spherical(Q, mars) * UNIT_LENGTH body_distances.append([dist_sun, dist_earth, dist_mars]) if dist_earth < EARTH_RADIUS: logging.info( f"STOP: Collision with earth {dist_earth:.6f} " f"({format_time(max_duration, time_unit='years')}) " f"reached at t = {t:.6f} ({format_time(t, time_unit='years')})" f" at iteration: {i}/{max_iter} ~ {i/max_iter*100:.3f} %") break Q, B = euler_step_symplectic(h, Q, B, eph_coords) # Q, B = verlet_step_symplectic(h, Q, B, eph_coords) ts.append(t) days.append(day) Qs.append(Q) Bs.append(B) q_p_list.append((Q, B)) # Log status every 1000 iterations. if i % 1000 == 0: t1 = time.time() sim_time = t1 - t_start # if i > 10 ^ 4: # h = 60 / UNIT_TIME # logging.info(f"At iteration {i}, h now set to {h}") # if i > 10 ^ 5: # h = 3600 / UNIT_TIME # logging.info(f"At iteration {i}, h now set to {h}") # if i > 10 ^ 6: # h = 3600 * 12 / UNIT_TIME # logging.info(f"At iteration {i}, h now set to {h}") logging.info( f"Iteration {str(i).rjust(len(str(max_iter)))} / {max_iter}" f", in-sim time {format_time(t, time_unit='years')} / " f"{format_time(max_duration, time_unit='years')}" f" (out-of-sim elapsed time: {format_time(sim_time)})") # Stop simulation of max duration reached if (t - t0) >= max_duration: logging.info( f"STOP: Max time of {max_duration:.6f} " f"({format_time(max_duration, time_unit='years')}) " f"reached at t = {t:.6f} ({format_time(t, time_unit='years')})" f" at iteration: {i}/{max_iter} ~ {i/max_iter*100:.3f} %") break # Stop simulation of max iterations reached if i >= max_iter: logging.info(f"STOP: Max iter of {max_iter} reached (i={i}) " f"at t = {format_time(t, time_unit='years')}/" f"{format_time(max_duration, time_unit='years')} ~ " f"{t/max_duration:.3f} %)") break # Check for body collision t_s = (t - t0) * UNIT_TIME # final in-sim time in seconds t_end = time.time() T = t_end - t_start # final out-of-sim time in seconds # Post simulator run logging logging.info(f"SIMULATOR PERFORMANCE: Sim/Real time ratio: " f"{Decimal(t_s / T):.2E} ({(t_s / T):.2f})") logging.info(f"SIMULATOR PERFORMANCE: 1 second can simulate: " f"{format_time(t_s / T)} (DDD:HH:MM:SS)") logging.info(f"SIMULATOR PERFORMANCE: Time to simulate 1 day: " f"{format_time(T / t_s * 3600 * 24)} (DDD:HH:MM:SS)") logging.info( f"TIME ELAPSED: In-sim time duration: {format_time(t,time_unit='years')} " f"(DDD:HH:MM:SS)") logging.info( f"TIME ELAPSED: Out-of-sim time duration: {format_time(T)} (DDD:HH:MM:SS)" ) return (ts, Qs, Bs, (t, i), ephemerides, eph_body_coords, body_distances)
def __init__(self, qs, ts, t_final, max_year, ax): eph = get_ephemerides(max_year=max_year) earth = eph["earth"] mars = eph["mars"] # ts is in years, and is as such very small. we scale it to days, to fit with eph days_ts = [(t * UNIT_TIME) / 3600 / 24 for t in ts] qs = [get_position_cartesian_from_spherical(x, y, z) for x, y, z in qs] self.xs, self.ys, self.zs = np.array( qs ).T # get individual coordinate sets for plotting self.t_final = t_final ax.set_title("animation") self.ani_ax = ax xs_earth = [] ys_earth = [] zs_earth = [] xs_mars = [] ys_mars = [] zs_mars = [] for t in days_ts: t_eph = get_ephemerides_on_day(eph, day_index=t) xs_earth.append(t_eph["earth"]["x"]) ys_earth.append(t_eph["earth"]["y"]) zs_earth.append(t_eph["earth"]["z"]) xs_mars.append(t_eph["mars"]["x"]) ys_mars.append(t_eph["mars"]["y"]) zs_mars.append(t_eph["mars"]["z"]) self.xs_earth = xs_earth self.ys_earth = ys_earth self.zs_earth = zs_earth self.xs_mars = xs_mars self.ys_mars = ys_mars self.zs_mars = zs_mars self.earth_xdata = [self.xs_earth[0]] self.earth_ydata = [self.ys_earth[0]] self.earth_zdata = [self.zs_earth[0]] self.earth_line = Line3D( self.earth_xdata, self.earth_ydata, self.earth_zdata, color="deepskyblue" ) self.ani_ax.add_line(self.earth_line) self.mars_xdata = [self.xs_mars[0]] self.mars_ydata = [self.ys_mars[0]] self.mars_zdata = [self.zs_mars[0]] self.mars_line = Line3D( self.mars_xdata, self.mars_ydata, self.mars_zdata, color="orange" ) self.ani_ax.add_line(self.mars_line) self.xdata = [self.xs[0]] self.ydata = [self.ys[0]] self.zdata = [self.zs[0]] self.traj_line = Line3D(self.xdata, self.ydata, self.zdata, color="black") self.ani_ax.add_line(self.traj_line) # -- SUN -- ax.scatter(0, 0, 0, c="gold", marker="o") self.ani_ax.set_xlim(-1.5, 1.5) self.ani_ax.set_ylim(-1.5, 1.5) self.ani_ax.set_zlim(-1, 1) self.ani_terminate = False