def _targetting(departure_body, target_body, t_launch, t_arrival): """This function returns the increment in departure and arrival velocities. """ rr_dpt_body, vv_dpt_body = _get_state(departure_body, t_launch) rr_arr_body, vv_arr_body = _get_state(target_body, t_arrival) # Compute time of flight tof = t_arrival - t_launch if tof <= 0: return None, None, None, None, None try: (v_dpt, v_arr), = lambert(Sun.k, rr_dpt_body.xyz, rr_arr_body.xyz, tof) # Compute all the output variables dv_dpt = norm(v_dpt - vv_dpt_body.xyz) dv_arr = norm(v_arr - vv_arr_body.xyz) c3_launch = dv_dpt ** 2 c3_arrival = dv_arr ** 2 return ( dv_dpt.to(u.km / u.s).value, dv_arr.to(u.km / u.s).value, c3_launch.to(u.km ** 2 / u.s ** 2).value, c3_arrival.to(u.km ** 2 / u.s ** 2).value, tof.jd, ) except AssertionError: return None, None, None, None, None
def test_propagating_to_certain_nu_is_correct(): # take an elliptic orbit a = 1.0 * u.AU ecc = 1.0 / 3.0 * u.one _a = 0.0 * u.rad nu = 10 * u.deg elliptic = Orbit.from_classical(Sun, a, ecc, _a, _a, _a, nu) elliptic_at_perihelion = elliptic.propagate_to_anomaly(0.0 * u.rad) r_per, _ = elliptic_at_perihelion.rv() elliptic_at_aphelion = elliptic.propagate_to_anomaly(np.pi * u.rad) r_ap, _ = elliptic_at_aphelion.rv() assert_quantity_allclose(norm(r_per), a * (1.0 - ecc)) assert_quantity_allclose(norm(r_ap), a * (1.0 + ecc)) # TODO: Test specific values assert elliptic_at_perihelion.epoch > elliptic.epoch assert elliptic_at_aphelion.epoch > elliptic.epoch # test 10 random true anomaly values for _ in range(10): nu = np.random.uniform(low=0.0, high=2 * np.pi) elliptic = elliptic.propagate_to_anomaly(nu * u.rad) r, _ = elliptic.rv() assert_quantity_allclose(norm(r), a * (1.0 - ecc ** 2) / (1 + ecc * np.cos(nu)))
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_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_propagation_hyperbolic(): # Data from Curtis, example 3.5 r0 = [Earth.R.to(u.km).value + 300, 0, 0] * u.km v0 = [0, 15, 0] * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0) tof = 14941 * u.s ss1 = ss0.propagate(tof) r, v = ss1.rv() assert_almost_equal(norm(r).to(u.km).value, 163180, decimal=-1) assert_almost_equal(norm(v).to(u.km/u.s).value, 10.51, decimal=2)
def pqw(self): """Perifocal frame (PQW) vectors. """ if self.ecc < 1e-8: if abs(self.inc.to(u.rad).value) > 1e-8: node = np.cross([0, 0, 1], self.h_vec) / norm(self.h_vec) p_vec = node / norm(node) # Circular inclined else: p_vec = [1, 0, 0] * u.one # Circular equatorial else: p_vec = self.e_vec / self.ecc w_vec = self.h_vec / norm(self.h_vec) q_vec = np.cross(w_vec, p_vec) * u.one return p_vec, q_vec, w_vec
def test_propagation_hyperbolic(): # Data from Curtis, example 3.5 r0 = [Earth.R.to(u.km).value + 300, 0, 0] * u.km v0 = [0, 15, 0] * u.km / u.s expected_r_norm = 163180 * u.km expected_v_norm = 10.51 * u.km / u.s ss0 = Orbit.from_vectors(Earth, r0, v0) tof = 14941 * u.s ss1 = ss0.propagate(tof) r, v = ss1.rv() assert_quantity_allclose(norm(r), expected_r_norm, rtol=1e-4) assert_quantity_allclose(norm(v), expected_v_norm, rtol=1e-3)
def guidance_law(ss_0, ecc_f, inc_f, f): """Guidance law from the model. Thrust is aligned with an inertially fixed direction perpendicular to the semimajor axis of the orbit. Parameters ---------- ss_0 : Orbit Initial orbit, containing all the information. ecc_f : float Final eccentricity. inc_f : float Final inclination. f : float Magnitude of constant acceleration. """ # We fix the inertial direction at the beginning ecc_0 = ss_0.ecc.value if ecc_0 > 0.001: # Arbitrary tolerance ref_vec = ss_0.e_vec / ecc_0 else: ref_vec = ss_0.r / norm(ss_0.r) h_unit = ss_0.h_vec / norm(ss_0.h_vec) thrust_unit = np.cross(h_unit, ref_vec) * np.sign(ecc_f - ecc_0) inc_0 = ss_0.inc.to(u.rad).value argp = ss_0.argp.to(u.rad).value beta_0_ = beta(ecc_0, ecc_f, inc_0, inc_f, argp) @state_from_vector def a_d(t0, ss): r = ss.r.value v = ss.v.value nu = ss.nu.value beta_ = beta_0_ * np.sign(np.cos(nu)) # The sign of ß reverses at minor axis crossings w_ = np.cross(r, v) / norm(np.cross(r, v)) accel_v = f * ( np.cos(beta_) * thrust_unit + np.sin(beta_) * w_ ) return accel_v return a_d
def a_d(t0, ss): r = ss.r.value v = ss.v.value # Change sign of beta with the out-of-plane velocity beta_ = beta(t0, V_0=V_0, f=f, beta_0=beta_0_) * np.sign(r[0] * (inc_f - inc_0)) t_ = v / norm(v) w_ = np.cross(r, v) / norm(np.cross(r, v)) # n_ = np.cross(t_, w_) accel_v = f * ( np.cos(beta_) * t_ + np.sin(beta_) * w_ ) return accel_v
def a_d(t0, ss): r = ss.r.value v = ss.v.value nu = ss.nu.value alpha_ = nu - np.pi / 2 r_ = r / norm(r) w_ = np.cross(r, v) / norm(np.cross(r, v)) s_ = np.cross(w_, r_) accel_v = f * ( np.cos(alpha_) * s_ + np.sin(alpha_) * r_ ) return accel_v
def _lambert(k, r1, r2, tof, M, numiter, rtol): # Check preconditions assert tof > 0 assert k > 0 # Chord c = r2 - r1 c_norm, r1_norm, r2_norm = norm(c), norm(r1), norm(r2) # Semiperimeter s = (r1_norm + r2_norm + c_norm) * .5 # Versors i_r1, i_r2 = r1 / r1_norm, r2 / r2_norm i_h = np.cross(i_r1, i_r2) i_h = i_h / norm(i_h) # Fixed from paper # Geometry of the problem ll = np.sqrt(1 - c_norm / s) if i_h[2] < 0: ll = -ll i_h = -i_h i_t1, i_t2 = np.cross(i_h, i_r1), np.cross(i_h, i_r2) # Fixed from paper # Non dimensional time of flight T = np.sqrt(2 * k / s ** 3) * tof # Find solutions xy = _find_xy(ll, T, M, numiter, rtol) # Reconstruct gamma = np.sqrt(k * s / 2) rho = (r1_norm - r2_norm) / c_norm sigma = np.sqrt(1 - rho ** 2) for x, y in xy: V_r1, V_r2, V_t1, V_t2 = _reconstruct(x, y, r1_norm, r2_norm, ll, gamma, rho, sigma) v1 = V_r1 * i_r1 + V_t1 * i_t1 v2 = V_r2 * i_r2 + V_t2 * i_t2 yield v1, v2
def a_d(t0, ss): r = ss.r.value v = ss.v.value nu = ss.nu.value beta_ = beta_0_ * np.sign(np.cos(nu)) # The sign of ß reverses at minor axis crossings w_ = np.cross(r, v) / norm(np.cross(r, v)) accel_v = f * ( np.cos(beta_) * thrust_unit + np.sin(beta_) * w_ ) return accel_v
def rv2coe(k, r, v, tol=1e-8): """Converts from vectors to classical orbital elements. Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2). r : array Position vector (km). v : array Velocity vector (km / s). tol : float, optional Tolerance for eccentricity and inclination checks, default to 1e-8. """ h = np.cross(r, v) n = np.cross([0, 0, 1], h) / norm(h) e = ((v.dot(v) - k / (norm(r))) * r - r.dot(v) * v) / k ecc = norm(e) p = h.dot(h) / k inc = np.arccos(h[2] / norm(h)) circular = ecc < tol equatorial = abs(inc) < tol if equatorial and not circular: raan = 0 argp = np.arctan2(e[1], e[0]) % (2 * np.pi) # Longitude of periapsis nu = (np.arctan2(h.dot(np.cross(e, r)) / norm(h), r.dot(e)) % (2 * np.pi)) elif not equatorial and circular: raan = np.arctan2(n[1], n[0]) % (2 * np.pi) argp = 0 # Argument of latitude nu = (np.arctan2(r.dot(np.cross(h, n)) / norm(h), r.dot(n)) % (2 * np.pi)) elif equatorial and circular: raan = 0 argp = 0 nu = np.arctan2(r[1], r[0]) % (2 * np.pi) # True longitude else: raan = np.arctan2(n[1], n[0]) % (2 * np.pi) argp = (np.arctan2(e.dot(np.cross(h, n)) / norm(h), e.dot(n)) % (2 * np.pi)) nu = (np.arctan2(r.dot(np.cross(h, e)) / norm(h), r.dot(e)) % (2 * np.pi)) return p, ecc, inc, raan, argp, nu
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), 304700.0 * u.km, rtol=1e-4)
def set_frame(self, p_vec, q_vec, w_vec): """Sets perifocal frame. Raises ------ ValueError If the vectors are not a set of mutually orthogonal unit vectors. """ if not np.allclose([norm(v) for v in (p_vec, q_vec, w_vec)], 1): raise ValueError("Vectors must be unit.") elif not np.allclose([p_vec.dot(q_vec), q_vec.dot(w_vec), w_vec.dot(p_vec)], 0): raise ValueError("Vectors must be mutually orthogonal.") else: self._frame = p_vec, q_vec, w_vec if self._trajectories: self._redraw()
def set_frame(self, p_vec, q_vec, w_vec): """Sets perifocal frame. Raises ------ ValueError If the vectors are not a set of mutually orthogonal unit vectors. """ if self._frame and self.trajectories: raise NotImplementedError( "OrbitPlotter2D does not support reprojecting yet" ) if not np.allclose([norm(v) for v in (p_vec, q_vec, w_vec)], 1): raise ValueError("Vectors must be unit.") elif not np.allclose([p_vec.dot(q_vec), q_vec.dot(w_vec), w_vec.dot(p_vec)], 0): raise ValueError("Vectors must be mutually orthogonal.") else: self._frame = p_vec, q_vec, w_vec
def set_frame(self, p_vec, q_vec, w_vec): """Sets perifocal frame if not existing. Raises ------ ValueError If the vectors are not a set of mutually orthogonal unit vectors. NotImplementedError If the frame was already set up. """ if not self._frame: if not np.allclose([norm(v) for v in (p_vec, q_vec, w_vec)], 1): raise ValueError("Vectors must be unit.") if not np.allclose([p_vec.dot(q_vec), q_vec.dot(w_vec), w_vec.dot(p_vec)], 0): raise ValueError("Vectors must be mutually orthogonal.") else: self._frame = p_vec, q_vec, w_vec else: raise NotImplementedError
def plot(self, orbit, *, label=None, color=None): """Plots state and osculating orbit in their plane. Parameters ---------- orbit : ~poliastro.twobody.orbit.Orbit Orbit to plot. label : string, optional Label of the orbit. color : string, optional Color of the line and the position. """ if color is None: color = next(self._color_cycle) self._set_attractor(orbit.attractor) label = generate_label(orbit, label) trajectory = orbit.sample() trace = self._plot_trajectory(trajectory, label, color, True) self._trajectories.append( Trajectory(trajectory, orbit.r, label, trace.line.color) ) # Redraw the attractor now to compute the attractor radius self._redraw_attractor() # Plot required 2D/3D shape in the position of the body radius = min( self._attractor_radius * 0.5, (norm(orbit.r) - orbit.attractor.R) * 0.5 ) # Arbitrary thresholds self._plot_point(radius, color, label, center=orbit.r) if not self._figure._in_batch_mode: return self.show()
def plot(self, orbit, *, label=None, color=None): """Plots state and osculating orbit in their plane. Parameters ---------- orbit : ~poliastro.twobody.orbit.Orbit label : string, optional color : string, optional """ if color is None: color = next(self._color_cycle) self.set_attractor(orbit.attractor) self._redraw_attractor(orbit.r_p * 0.15) # Arbitrary threshold label = _generate_label(orbit, label) trajectory = orbit.sample() self._plot_trajectory(trajectory, label, color, True) # Plot required 2D/3D shape in the position of the body radius = min(self._attractor_radius * 0.5, (norm(orbit.r) - orbit.attractor.R) * 0.3) # Arbitrary thresholds shape = self._plot_sphere(radius, color, label, center=orbit.r) self._data.append(shape)
def e_vec(self): """Eccentricity vector. """ r, v = self.rv() k = self.attractor.k e_vec = ((v.dot(v) - k / (norm(r))) * r - r.dot(v) * v) / k return e_vec.decompose()
def e_vec(self): """Eccentricity vector. """ r, v = self.rv() k = self.attractor.k e_vec = ((v.dot(v) - k / (norm(r))) * r - r.dot(v) * v) / k return e_vec
def hohmann(cls, orbit_i, r_f): r"""Compute a Hohmann transfer between two circular orbits. By defining the relationship between orbit radius: .. math:: a_{trans} = \frac{r_{i} + r_{f}}{2} The Hohmann maneuver velocities can be expressed as: .. math:: \begin{align} \Delta v_{a} &= \sqrt{\frac{2\mu}{r_{i}} - \frac{\mu}{a_{trans}}} - v_{i}\\ \Delta v_{b} &= \sqrt{\frac{\mu}{r_{f}}} - \sqrt{\frac{2\mu}{r_{f}} - \frac{\mu}{a_{trans}}} \end{align} The time that takes to complete the maneuver can be computed as: .. math:: \tau_{trans} = \pi \sqrt{\frac{(a_{trans})^{3}}{\mu}} Parameters ---------- orbit_i: poliastro.twobody.orbit.Orbit Initial orbit r_f: astropy.unit.Quantity Final altitude of the orbit """ if orbit_i.nu is not 0 * u.deg: orbit_i = orbit_i.propagate_to_anomaly(0 * u.deg) # Initial orbit data k = orbit_i.attractor.k r_i = orbit_i.r v_i = orbit_i.v h_i = norm(cross(r_i.to(u.m).value, v_i.to(u.m / u.s).value) * u.m ** 2 / u.s) p_i = h_i ** 2 / k.to(u.m ** 3 / u.s ** 2) # Hohmann is defined always from the PQW frame, since it is the # natural plane of the orbit r_i, v_i = rv_pqw( k.to(u.m ** 3 / u.s ** 2).value, p_i.to(u.m).value, orbit_i.ecc.value, orbit_i.nu.to(u.rad).value, ) # Now, we apply Hohmman maneuver r_i = norm(r_i * u.m) v_i = norm(v_i * u.m / u.s) a_trans = (r_i + r_f) / 2 # This is the modulus of the velocities dv_a = np.sqrt(2 * k / r_i - k / a_trans) - v_i dv_b = np.sqrt(k / r_f) - np.sqrt(2 * k / r_f - k / a_trans) # Write them in PQW frame dv_a = np.array([0, dv_a.to(u.m / u.s).value, 0]) dv_b = np.array([0, -dv_b.to(u.m / u.s).value, 0]) # Transform to IJK frame rot_matrix = coe_rotation_matrix( orbit_i.inc.to(u.rad).value, orbit_i.raan.to(u.rad).value, orbit_i.argp.to(u.rad).value, ) dv_a = (rot_matrix @ dv_a) * u.m / u.s dv_b = (rot_matrix @ dv_b) * u.m / u.s t_trans = np.pi * np.sqrt(a_trans ** 3 / k) return cls((0 * u.s, dv_a), (t_trans, dv_b))
def get_total_cost(self): """Returns total cost of the maneuver. """ dvs = [norm(dv) for dv in self._dvs] return sum(dvs, 0 * u.km / u.s)
def bielliptic(cls, orbit_i, r_b, r_f): r"""Compute a bielliptic transfer between two circular orbits. The bielliptic maneuver employs two Hohmann transfers, therefore two intermediate orbits are established. We define the different radius relationships as follows: .. math:: \begin{align} a_{trans1} &= \frac{r_{i} + r_{b}}{2}\\ a_{trans2} &= \frac{r_{b} + r_{f}}{2}\\ \end{align} The increments in the velocity are: .. math:: \begin{align} \Delta v_{a} &= \sqrt{\frac{2\mu}{r_{i}} - \frac{\mu}{a_{trans1}}} - v_{i}\\ \Delta v_{b} &= \sqrt{\frac{2\mu}{r_{b}} - \frac{\mu}{a_{trans2}}} - \sqrt{\frac{2\mu}{r_{b}} - \frac{\mu}{a_trans{1}}}\\ \Delta v_{c} &= \sqrt{\frac{\mu}{r_{f}}} - \sqrt{\frac{2\mu}{r_{f}} - \frac{\mu}{a_{trans2}}}\\ \end{align} The time of flight for this maneuver is the addition of the time needed for both transition orbits, following the same formula as Hohmann: .. math:: \begin{align} \tau_{trans1} &= \pi \sqrt{\frac{a_{trans1}^{3}}{\mu}}\\ \tau_{trans2} &= \pi \sqrt{\frac{a_{trans2}^{3}}{\mu}}\\ \end{align} Parameters ---------- orbit_i: poliastro.twobody.orbit.Orbit Initial orbit r_b: astropy.unit.Quantity Altitude of the intermediate orbit r_f: astropy.unit.Quantity Final altitude of the orbit """ if orbit_i.nu is not 0 * u.deg: orbit_i = orbit_i.propagate_to_anomaly(0 * u.deg) # Initial orbit data k = orbit_i.attractor.k r_i = orbit_i.r v_i = orbit_i.v h_i = norm(cross(r_i.to(u.m).value, v_i.to(u.m / u.s).value) * u.m ** 2 / u.s) p_i = h_i ** 2 / k.to(u.m ** 3 / u.s ** 2) # Bielliptic is defined always from the PQW frame, since it is the # natural plane of the orbit r_i, v_i = rv_pqw( k.to(u.m ** 3 / u.s ** 2).value, p_i.to(u.m).value, orbit_i.ecc.value, orbit_i.nu.to(u.rad).value, ) # Define the transfer radius r_i = norm(r_i * u.m) v_i = norm(v_i * u.m / u.s) a_trans1 = (r_i + r_b) / 2 a_trans2 = (r_b + r_f) / 2 # Compute impulses dv_a = np.sqrt(2 * k / r_i - k / a_trans1) - v_i dv_b = np.sqrt(2 * k / r_b - k / a_trans2) - np.sqrt(2 * k / r_b - k / a_trans1) dv_c = np.sqrt(k / r_f) - np.sqrt(2 * k / r_f - k / a_trans2) # Write impulses in PQW frame dv_a = np.array([0, dv_a.to(u.m / u.s).value, 0]) dv_b = np.array([0, -dv_b.to(u.m / u.s).value, 0]) dv_c = np.array([0, dv_c.to(u.m / u.s).value, 0]) rot_matrix = coe_rotation_matrix( orbit_i.inc.to(u.rad).value, orbit_i.raan.to(u.rad).value, orbit_i.argp.to(u.rad).value, ) # Transform to IJK frame dv_a = (rot_matrix @ dv_a) * u.m / u.s dv_b = (rot_matrix @ dv_b) * u.m / u.s dv_c = (rot_matrix @ dv_c) * u.m / u.s # Compute time for maneuver t_trans1 = np.pi * np.sqrt(a_trans1 ** 3 / k) t_trans2 = np.pi * np.sqrt(a_trans2 ** 3 / k) return cls((0 * u.s, dv_a), (t_trans1, dv_b), (t_trans2, dv_c))
def lagrange_points_vec(m1, r1, m2, r2, n): """Computes the five Lagrange points in the CR3BP. Returns the positions in the same frame of reference as `r1` and `r2` for the five Lagrangian points. Parameters ---------- m1 : ~astropy.units.Quantity Mass of the main body. This body is the one with the biggest mass. r1 : ~astropy.units.Quantity Position of the main body. m2 : ~astropy.units.Quantity Mass of the secondary body. r2 : ~astropy.units.Quantity Position of the secondary body. n : ~astropy.units.Quantity Normal vector to the orbital plane. Returns ------- list: Position of the Lagrange points: [L1, L2, L3, L4, L5] The positions are of type ~astropy.units.Quantity """ # Check Body 1 is the main body assert ( m1 > m2 ), "Body 1 is not the main body: it has less mass than the 'secondary' body" # Define local frame of reference: # Center: main body, NOT the barycenter # x-axis: points to the secondary body ux = r2 - r1 r12 = norm(ux) ux = ux / r12 # y-axis: contained in the orbital plane, perpendicular to x-axis def cross(x, y): return np.cross(x, y) * x.unit * y.unit uy = cross(n, ux) uy = uy / norm(uy) # position in x-axis x1, x2, x3, x4, x5 = lagrange_points(r12, m1, m2) # position in y-axis # L1, L2, L3 are located in the x-axis, so y123 = 0 # L4 and L5 are points in the plane of rotation which form an equilateral # triangle with the two masses (Battin) # sqrt(3)/2 = sin(60 deg) y4 = np.sqrt(3) / 2 * r12 y5 = -y4 # Convert L points coordinates (x,y) to original vectorial base [r1 r2] L1 = r1 + ux * x1 L2 = r1 + ux * x2 L3 = r1 + ux * x3 L4 = r1 + ux * x4 + uy * y4 L5 = r1 + ux * x5 + uy * y5 return [L1, L2, L3, L4, L5]
def _plot_position(self, position, label, colors): radius = min( self._attractor_radius * 0.5, (norm(position) - self._attractor.R) * 0.5 ) # Arbitrary thresholds self._draw_point(radius, colors[0], label, center=position)
def compute_flyby(v_spacecraft, v_body, k, r_p, theta=0 * u.deg): """Computes outbound velocity after a flyby. Parameters ---------- v_spacecraft : ~astropy.units.Quantity Velocity of the spacecraft, relative to the attractor of the body. v_body : ~astropy.units.Quantity Velocity of the body, relative to its attractor. k : ~astropy.units.Quantity Standard gravitational parameter of the body. r_p : ~astropy.units.Quantity Radius of periapsis, measured from the center of the body. theta : ~astropy.units.Quantity, optional Aim angle of the B vector, default to 0. Returns ------- v_spacecraft_out : ~astropy.units.Quantity Outbound velocity of the spacecraft. delta : ~astropy.units.Quantity Turn angle. """ v_inf_1 = v_spacecraft - v_body # Hyperbolic excess velocity v_inf = norm(v_inf_1) ecc = 1 + r_p * v_inf ** 2 / k # Eccentricity of the entry hyperbola delta = 2 * np.arcsin(1 / ecc) # Turn angle b = k / v_inf ** 2 * np.sqrt(ecc ** 2 - 1) # Magnitude of the B vector # Now we compute the unit vectors in which to return the outbound hyperbolic excess velocity: # * S goes along the hyperbolic excess velocity and is perpendicular to the B-Plane, # * T goes along the B-Plane and is parallel to _some_ fundamental plane - in this case, the plane in which # the velocities are computed # * R completes the orthonormal set S_vec = v_inf_1 / v_inf c_vec = np.array([0, 0, 1]) * u.one T_vec = np.cross(S_vec, c_vec) * u.one T_vec = T_vec / norm(T_vec) R_vec = np.cross(S_vec, T_vec) * u.one # This vector defines the B-Plane B_vec = b * (np.cos(theta) * T_vec + np.sin(theta) * R_vec) # We have to rotate the inbound hyperbolic excess velocity # an angle delta (turn angle) around a vector that is orthogonal to # the B-Plane and trajectory plane rot_v = np.cross(B_vec / b, S_vec) * u.one # And now we rotate the outbound hyperbolic excess velocity # u_vec = v_inf_1 / norm(v_inf) = S_vec v_vec = np.cross(rot_v, v_inf_1) * u.one v_vec = v_vec / norm(v_vec) v_inf_2 = v_inf * (np.cos(delta) * S_vec + np.sin(delta) * v_vec) v_spacecraft_out = v_inf_2 + v_body return v_spacecraft_out, delta