def change_ecc_inc(k, a, ecc_0, ecc_f, inc_0, inc_f, argp, r, v, f): # We fix the inertial direction at the beginning if ecc_0 > 0.001: # Arbitrary tolerance e_vec = eccentricity_vector(k, r, v) ref_vec = e_vec / ecc_0 else: ref_vec = r / norm(r) h_vec = cross(r, v) # Specific angular momentum vector h_unit = h_vec / norm(h_vec) thrust_unit = cross(h_unit, ref_vec) * np.sign(ecc_f - ecc_0) beta_0 = beta(ecc_0, ecc_f, inc_0, inc_f, argp) @jit def a_d(t0, u_, k_): r_ = u_[:3] v_ = u_[3:] nu = rv2coe(k_, r_, v_)[-1] beta_ = beta_0 * np.sign( np.cos(nu)) # The sign of ß reverses at minor axis crossings w_ = cross(r_, v_) / norm(cross(r_, v_)) accel_v = f * (np.cos(beta_) * thrust_unit + np.sin(beta_) * w_) return accel_v delta_v = delta_V(circular_velocity(k, a), ecc_0, ecc_f, beta_0) t_f = delta_t(delta_v, f) return a_d, delta_v, t_f
def third_body(t0, state, k, k_third, perturbation_body): r"""Calculate third body acceleration (km/s2). .. math:: \vec{p} = \mu_{m}\left ( \frac{\vec{r_{m/s}}}{r_{m/s}^3} - \frac{\vec{r_{m}}}{r_{m}^3} \right ) Parameters ---------- t0 : float Current time (s). state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard Gravitational parameter of the attractor (km^3/s^2). k_third : float Standard Gravitational parameter of the third body (km^3/s^2). perturbation_body : callable A callable object returning the position of the body that causes the perturbation in the attractor frame. Notes ----- This formula is taken from Howard Curtis, section 12.10. As an example, a third body could be the gravity from the Moon acting on a small satellite. """ body_r = perturbation_body(t0) delta_r = body_r - state[:3] return k_third * delta_r / norm(delta_r)**3 - k_third * body_r / norm( body_r)**3
def line_of_sight(r1, r2, R): """Calculates the line of sight condition between two position vectors, r1 and r2. Parameters ---------- r1 : numpy.ndarray The position vector of the first object with respect to a central attractor. r2 : numpy.ndarray The position vector of the second object with respect to a central attractor. R : float The radius of the central attractor. Returns ------- delta_theta: float Greater than or equal to zero, if there exists a LOS between two objects located by r1 and r2, else negative. """ r1_norm = norm(r1) r2_norm = norm(r2) theta = np.arccos((r1 @ r2) / r1_norm / r2_norm) theta_1 = np.arccos(R / r1_norm) theta_2 = np.arccos(R / r2_norm) return (theta_1 + theta_2) - theta
def hohmann(k, rv, r_f): r"""Calculate the Hohmann maneuver velocities and the duration of the maneuver. 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 ---------- k : float Standard Gravitational parameter rv : numpy.ndarray, numpy.ndarray Position and velocity vectors r_f : float Final orbital radius """ _, ecc, inc, raan, argp, nu = rv2coe(k, *rv) h_i = norm(cross(*rv)) p_i = h_i**2 / k r_i, v_i = rv_pqw(k, p_i, ecc, nu) r_i = norm(r_i) v_i = norm(v_i) a_trans = (r_i + r_f) / 2 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) dv_a = np.array([0, dv_a, 0]) dv_b = np.array([0, -dv_b, 0]) rot_matrix = coe_rotation_matrix(inc, raan, argp) dv_a = rot_matrix @ dv_a dv_b = rot_matrix @ dv_b t_trans = np.pi * np.sqrt(a_trans**3 / k) return dv_a, dv_b, t_trans
def a_d(t0, u_, k): r = u_[:3] v = u_[3:] # Change sign of beta with the out-of-plane velocity beta_ = beta(t0, V_0, f, beta_0_) * np.sign(r[0] * (inc_f - inc_0)) t_ = v / norm(v) w_ = cross(r, v) / norm(cross(r, v)) accel_v = f * (np.cos(beta_) * t_ + np.sin(beta_) * w_) return accel_v
def a_d(t0, u_, k): r = u_[:3] v = u_[3:] nu = rv2coe(k, r, v)[-1] alpha_ = nu - np.pi / 2 r_ = r / norm(r) w_ = cross(r, v) / norm(cross(r, v)) s_ = cross(w_, r_) accel_v = f * (np.cos(alpha_) * s_ + np.sin(alpha_) * r_) return accel_v
def J3_perturbation(t0, state, k, J3, R): r"""Calculates J3_perturbation acceleration (km/s2) Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard Gravitational parameter. (km^3/s^2) J3 : float Oblateness factor R : float Attractor radius Notes ----- The J3 accounts for the oblateness of the attractor. The formula is given in Howard Curtis, problem 12.8 This perturbation has not been fully validated, see https://github.com/poliastro/poliastro/pull/398 """ r_vec = state[:3] r = norm(r_vec) factor = (1.0 / 2.0) * k * J3 * (R**3) / (r**5) cos_phi = r_vec[2] / r a_x = 5.0 * r_vec[0] / r * (7.0 * cos_phi**3 - 3.0 * cos_phi) a_y = 5.0 * r_vec[1] / r * (7.0 * cos_phi**3 - 3.0 * cos_phi) a_z = 3.0 * (35.0 / 3.0 * cos_phi**4 - 10.0 * cos_phi**2 + 1) return np.array([a_x, a_y, a_z]) * factor
def atmospheric_drag(t0, state, k, C_D, A_over_m, rho): r"""Calculates atmospheric drag acceleration (km/s2) .. math:: \vec{p} = -\frac{1}{2}\rho v_{rel}\left ( \frac{C_{d}A}{m} \right )\vec{v_{rel}} .. versionadded:: 1.14 Parameters ---------- t0 : float Current time (s). state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard Gravitational parameter (km^3/s^2) C_D : float Dimensionless drag coefficient () A_over_m : float Frontal area/mass of the spacecraft (km^2/kg) rho : float Air density at corresponding state (kg/m^3) Notes ----- This function provides the acceleration due to atmospheric drag, as computed by a model from poliastro.earth.atmosphere """ v_vec = state[3:] v = norm(v_vec) B = C_D * A_over_m return -(1.0 / 2.0) * rho * B * v * v_vec
def __call__(self, t, u_, k): self._last_t = t pos_on_body = (u_[:3] / norm(u_[:3])) * self._R lat_, _, _ = cartesian_to_ellipsoidal_fast(self._R, self._R_polar, *pos_on_body) return np.rad2deg(lat_) - self._lat
def project_point_on_ellipsoid(x, y, z, a, b, c): """Return the projection of a point on an ellipsoid. Parameters ---------- x, y, z : float Cartesian coordinates of point a, b, c : float Semi-axes of the ellipsoid """ p1, p2 = intersection_ellipsoid_line(x, y, z, x, y, z, a, b, c) norm_1 = norm(np.array([p1[0] - x, p1[1] - y, p1[2] - z])) norm_2 = norm(np.array([p2[0] - x, p2[1] - y, p2[2] - z])) return p1 if norm_1 <= norm_2 else p2
def atmospheric_drag_exponential(t0, state, k, R, C_D, A_over_m, H0, rho0): r"""Calculates atmospheric drag acceleration (km/s2) .. math:: \vec{p} = -\frac{1}{2}\rho v_{rel}\left ( \frac{C_{d}A}{m} \right )\vec{v_{rel}} .. versionadded:: 0.9.0 Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard Gravitational parameter (km^3/s^2). R : float Radius of the attractor (km) C_D : float Dimensionless drag coefficient () A_over_m : float Frontal area/mass of the spacecraft (km^2/kg) H0 : float Atmospheric scale height, (km) rho0 : float Exponent density pre-factor, (kg / km^3) Notes ----- This function provides the acceleration due to atmospheric drag using an overly-simplistic exponential atmosphere model. We follow Howard Curtis, section 12.4 the atmospheric density model is rho(H) = rho0 x exp(-H / H0) """ H = norm(state[:3]) v_vec = state[3:] v = norm(v_vec) B = C_D * A_over_m rho = rho0 * np.exp(-(H - R) / H0) return -(1.0 / 2.0) * rho * B * v * v_vec
def a_d(t0, u_, k_): r_ = u_[:3] v_ = u_[3:] nu = rv2coe(k_, r_, v_)[-1] beta_ = beta_0 * np.sign( np.cos(nu)) # The sign of ß reverses at minor axis crossings w_ = cross(r_, v_) / norm(cross(r_, v_)) accel_v = f * (np.cos(beta_) * thrust_unit + np.sin(beta_) * w_) return accel_v
def radiation_pressure(t0, state, k, R, C_R, A_over_m, Wdivc_s, star): r"""Calculates radiation pressure acceleration (km/s2) .. math:: \vec{p} = -\nu \frac{S}{c} \left ( \frac{C_{r}A}{m} \right )\frac{\vec{r}}{r} Parameters ---------- t0 : float Current time (s). state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard Gravitational parameter (km^3/s^2). R : float Radius of the attractor. C_R : float Dimensionless radiation pressure coefficient, 1 < C_R < 2 (). A_over_m : float Effective spacecraft area/mass of the spacecraft (km^2/kg). Wdivc_s : float Total star emitted power divided by the speed of light (W * s / km). star : callable A callable object returning the position of radiating star in the attractor frame. Notes ----- This function provides the acceleration due to star light pressure. We follow Howard Curtis, section 12.9 """ r_star = star(t0) r_sat = state[:3] P_s = Wdivc_s / (norm(r_star)**2) nu = float(line_of_sight_fast(r_sat, r_star, R) > 0) return -nu * P_s * (C_R * A_over_m) * r_star / norm(r_star)
def correct_pericenter(k, R, J2, max_delta_r, v, a, inc, ecc): """Calculates the time before burning and the velocity vector in direction of the burn. Parameters ---------- k : float Standard Gravitational parameter R : float Radius of the attractor J2 : float Oblateness factor max_delta_r : float Maximum satellite’s geocentric distance v : numpy.ndarray Velocity vector a : float Semi-major axis inc : float Inclination ecc : float Eccentricity Notes ----- The algorithm was obtained from "Fundamentals of Astrodynamics and Applications, 4th ed (2013)" by David A. Vallado, page 885. Given a max_delta_r, we determine the maximum perigee drift before we do an orbit-adjustment burn to restore the perigee to its nominal value. We estimate the time until this burn using the allowable drift delta_w and the drift rate :math:`|dw|`. For positive delta_v, the change in the eccentricity is positive for perigee burns and negative for apogee burns. The opposite holds for a delta_v applied against the velocity vector, which decreases the satellite’s velocity. Perigee drift are mainly due to the zonal harmonics, which cause variations in the altitude by changing the argument of perigee. Please note that ecc ≈ 0.001, so the error incurred by assuming a small eccentricity is on the order of 0.1%. This is smaller than typical variations in thruster performance between burns. """ p = a * (1 - ecc**2) n = (k / a**3)**0.5 dw = ((3 * n * R**2 * J2) / (4 * p**2)) * (4 - 5 * np.sin(inc)**2) delta_w = 2 * (1 + ecc) * max_delta_r delta_w /= a * ecc * (1 - ecc) delta_w **= 0.5 delta_t = abs(delta_w / dw) delta_v = 0.5 * n * a * ecc * abs(delta_w) vf_ = v / norm(v) * delta_v return delta_t, vf_
def __call__(self, t, u_, k): self._last_t = t if norm(u_[:3]) < self._R: warn( "The norm of the position vector of the primary body is less than the radius of the attractor." ) pos_coord = self._pos_coords.pop( 0) if self._pos_coords else self._last_coord # Need to cast `pos_coord` to array since `norm` inside numba only works for arrays, not lists. delta_angle = line_of_sight_fast(u_[:3], np.array(pos_coord), self._R) return delta_angle
def tangential_vecs(N): """Returns orthonormal vectors tangential to the ellipsoid at the given location. Parameters ---------- N : numpy.ndarray Normal vector of the ellipsoid """ u = np.array([1.0, 0, 0]) u -= (u @ N) * N u /= norm(u) v = np.cross(N, u) return u, v
def eclipse_function(k, u_, r_sec, R_sec, R_primary, umbra=True): """Calculates a continuous shadow function. Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2). u_ : numpy.ndarray Satellite position and velocity vector with respect to the primary body. r_sec : numpy.ndarray Position vector of the secondary body with respect to the primary body. R_sec : float Equatorial radius of the secondary body. R_primary : float Equatorial radius of the primary body. umbra : bool Whether to calculate the shadow function for umbra or penumbra, defaults to True i.e. calculates for umbra. Notes ----- The shadow function is taken from Escobal, P. (1985). Methods of orbit determination. The current implementation assumes circular bodies and doesn't account for flattening. """ # Plus or minus condition pm = 1 if umbra else -1 p, ecc, inc, raan, argp, nu = rv2coe(k, u_[:3], u_[3:]) PQW = coe_rotation_matrix(inc, raan, argp) # Make arrays contiguous for faster dot product with numba. P_, Q_ = np.ascontiguousarray(PQW[:, 0]), np.ascontiguousarray(PQW[:, 1]) r_sec_norm = norm(r_sec) beta = (P_ @ r_sec) / r_sec_norm zeta = (Q_ @ r_sec) / r_sec_norm sin_delta_shadow = np.sin((R_sec - pm * R_primary) / r_sec_norm) cos_psi = beta * np.cos(nu) + zeta * np.sin(nu) shadow_function = (((R_primary**2) * (1 + ecc * np.cos(nu))**2) + (p**2) * (cos_psi**2) - p**2 + pm * (2 * p * R_primary * cos_psi) * (1 + ecc * np.cos(nu)) * sin_delta_shadow) return shadow_function
def eccentricity_vector(k, r, v): r"""Eccentricity vector. .. math:: \vec{e} = \frac{1}{\mu} \left( \left( v^{2} - \frac{\mu}{r}\right ) \vec{r} - (\vec{r} \cdot \vec{v})\vec{v} \right) Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2). r : numpy.ndarray Position vector (km) v : numpy.ndarray Velocity vector (km / s) """ return ((v @ v - k / norm(r)) * r - (r @ v) * v) / k
def N(a, b, c, cartesian_cords): """Normal vector of the ellipsoid at the given location. Parameters ---------- a : float Semi-major axis b : float Equatorial radius c : float Semi-minor axis cartesian_cords : numpy.ndarray Cartesian coordinates """ x, y, z = cartesian_cords N = np.array([2 * x / a**2, 2 * y / b**2, 2 * z / c**2]) N /= norm(N) return N
def distance(cartesian_cords, px, py, pz): """Calculates the distance from an arbitrary point to the given location (Cartesian coordinates). Parameters ---------- cartesian_cords : numpy.ndarray Cartesian coordinates px : float x-coordinate of the point py : float y-coordinate of the point pz : float z-coordinate of the point """ c = cartesian_cords u = np.array([px, py, pz]) d = norm(c - u) return d
def J2_perturbation(t0, state, k, J2, R): r"""Calculates J2_perturbation acceleration (km/s2) .. math:: \vec{p} = \frac{3}{2}\frac{J_{2}\mu R^{2}}{r^{4}}\left [\frac{x}{r}\left ( 5\frac{z^{2}}{r^{2}}-1 \right )\vec{i} + \frac{y}{r}\left ( 5\frac{z^{2}}{r^{2}}-1 \right )\vec{j} + \frac{z}{r}\left ( 5\frac{z^{2}}{r^{2}}-3 \right )\vec{k}\right] .. versionadded:: 0.9.0 Parameters ---------- t0 : float Current time (s) state : numpy.ndarray Six component state vector [x, y, z, vx, vy, vz] (km, km/s). k : float Standard Gravitational parameter. (km^3/s^2) J2 : float Oblateness factor R : float Attractor radius Notes ----- The J2 accounts for the oblateness of the attractor. The formula is given in Howard Curtis, (12.30) """ r_vec = state[:3] r = norm(r_vec) factor = (3.0 / 2.0) * k * J2 * (R**2) / (r**5) a_x = 5.0 * r_vec[2]**2 / r**2 - 1 a_y = 5.0 * r_vec[2]**2 / r**2 - 1 a_z = 5.0 * r_vec[2]**2 / r**2 - 3 return np.array([a_x, a_y, a_z]) * r_vec * factor
def __call__(self, t, u, k): self._last_t = t r_norm = norm(u[:3]) return (r_norm - self._R - self._alt ) # If this goes from +ve to -ve, altitude is decreasing.
def bielliptic(k, r_b, r_f, rv): r"""Calculate the increments in the velocities and the time of flight of the maneuver 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 ---------- k : float Standard Gravitational parameter r_b : float Altitude of the intermediate orbit r_f : float Final orbital radius rv : numpy.ndarray, numpy.ndarray Position and velocity vectors """ _, ecc, inc, raan, argp, nu = rv2coe(k, *rv) h_i = norm(cross(*rv)) p_i = h_i**2 / k r_i, v_i = rv_pqw(k, p_i, ecc, nu) r_i = norm(r_i) v_i = norm(v_i) a_trans1 = (r_i + r_b) / 2 a_trans2 = (r_b + r_f) / 2 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) dv_a = np.array([0, dv_a, 0]) dv_b = np.array([0, -dv_b, 0]) dv_c = np.array([0, dv_c, 0]) rot_matrix = coe_rotation_matrix(inc, raan, argp) dv_a = rot_matrix @ dv_a dv_b = rot_matrix @ dv_b dv_c = rot_matrix @ dv_c t_trans1 = np.pi * np.sqrt(a_trans1**3 / k) t_trans2 = np.pi * np.sqrt(a_trans2**3 / k) return dv_a, dv_b, dv_c, t_trans1, t_trans2
def rv2coe(k, r, v, tol=1e-8): r"""Converts from vectors to classical orbital elements. Parameters ---------- k : float Standard gravitational parameter (km^3 / s^2) r : numpy.ndarray Position vector (km) v : numpy.ndarray Velocity vector (km / s) tol : float, optional Tolerance for eccentricity and inclination checks, default to 1e-8 Returns ------- p : float Semi-latus rectum of parameter (km) ecc: float Eccentricity inc: float Inclination (rad) raan: float Right ascension of the ascending nod (rad) argp: float Argument of Perigee (rad) nu: float True Anomaly (rad) Notes ----- This example is a real exercise from Orbital Mechanics for Engineering students by Howard D.Curtis. This exercise is 4.3 of 3rd. Edition, page 200. 1. First the angular momentum is computed: .. math:: \vec{h} = \vec{r} \times \vec{v} 2. With it the eccentricity can be solved: .. math:: \begin{align} \vec{e} &= \frac{1}{\mu}\left [ \left ( v^{2} - \frac{\mu}{r}\right ) \vec{r} - (\vec{r} \cdot \vec{v})\vec{v} \right ] \\ e &= \sqrt{\vec{e}\cdot\vec{e}} \\ \end{align} 3. The node vector line is solved: .. math:: \begin{align} \vec{N} &= \vec{k} \times \vec{h} \\ N &= \sqrt{\vec{N}\cdot\vec{N}} \end{align} 4. The rigth ascension node is computed: .. math:: \Omega = \left\{ \begin{array}{lcc} cos^{-1}{\left ( \frac{N_{x}}{N} \right )} & if & N_{y} \geq 0 \\ \\ 360^{o} -cos^{-1}{\left ( \frac{N_{x}}{N} \right )} & if & N_{y} < 0 \\ \end{array} \right. 5. The argument of perigee: .. math:: \omega = \left\{ \begin{array}{lcc} cos^{-1}{\left ( \frac{\vec{N}\vec{e}}{Ne} \right )} & if & e_{z} \geq 0 \\ \\ 360^{o} -cos^{-1}{\left ( \frac{\vec{N}\vec{e}}{Ne} \right )} & if & e_{z} < 0 \\ \end{array} \right. 6. And finally the true anomaly: .. math:: \nu = \left\{ \begin{array}{lcc} cos^{-1}{\left ( \frac{\vec{e}\vec{r}}{er} \right )} & if & v_{r} \geq 0 \\ \\ 360^{o} -cos^{-1}{\left ( \frac{\vec{e}\vec{r}}{er} \right )} & if & v_{r} < 0 \\ \end{array} \right. Examples -------- >>> from poliastro.bodies import Earth >>> from astropy import units as u >>> k = Earth.k.to_value(u.km ** 3 / u.s ** 2) >>> r = np.array([-6045., -3490., 2500.]) >>> v = np.array([-3.457, 6.618, 2.533]) >>> p, ecc, inc, raan, argp, nu = rv2coe(k, r, v) >>> print("p:", p, "[km]") # doctest: +FLOAT_CMP p: 8530.47436396927 [km] >>> print("ecc:", ecc) # doctest: +FLOAT_CMP ecc: 0.17121118195416898 >>> print("inc:", np.rad2deg(inc), "[deg]") # doctest: +FLOAT_CMP inc: 153.2492285182475 [deg] >>> print("raan:", np.rad2deg(raan), "[deg]") # doctest: +FLOAT_CMP raan: 255.27928533439618 [deg] >>> print("argp:", np.rad2deg(argp), "[deg]") # doctest: +FLOAT_CMP argp: 20.068139973005362 [deg] >>> print("nu:", np.rad2deg(nu), "[deg]") # doctest: +FLOAT_CMP nu: 28.445804984192122 [deg] """ h = cross(r, v) n = cross([0, 0, 1], h) e = ((v @ v - k / norm(r)) * r - (r @ v) * v) / k ecc = norm(e) p = (h @ 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 @ cross(e, r)) / norm(h), r @ e) elif not equatorial and circular: raan = np.arctan2(n[1], n[0]) % (2 * np.pi) argp = 0 # Argument of latitude nu = np.arctan2((r @ cross(h, n)) / norm(h), r @ n) elif equatorial and circular: raan = 0 argp = 0 nu = np.arctan2(r[1], r[0]) % (2 * np.pi) # True longitude else: a = p / (1 - (ecc**2)) ka = k * a if a > 0: e_se = (r @ v) / sqrt(ka) e_ce = norm(r) * (v @ v) / k - 1 nu = E_to_nu(np.arctan2(e_se, e_ce), ecc) else: e_sh = (r @ v) / sqrt(-ka) e_ch = norm(r) * (norm(v)**2) / k - 1 nu = F_to_nu(np.log((e_ch + e_sh) / (e_ch - e_sh)) / 2, ecc) raan = np.arctan2(n[1], n[0]) % (2 * np.pi) px = r @ n py = (r @ cross(h, n)) / norm(h) argp = (np.arctan2(py, px) - nu) % (2 * np.pi) nu = (nu + np.pi) % (2 * np.pi) - np.pi return p, ecc, inc, raan, argp, nu
def izzo(k, r1, r2, tof, M, prograde, lowpath, numiter, rtol): """Aplies izzo algorithm to solve Lambert's problem. Parameters ---------- k : float Gravitational Constant r1 : numpy.ndarray Initial position vector r2 : numpy.ndarray Final position vector tof : float Time of flight between both positions M : int Number of revolutions prograde: boolean Controls the desired inclination of the transfer orbit. lowpath: boolean If `True` or `False`, gets the transfer orbit whose vacant focus is below or above the chord line, respectively. numiter : int Number of iterations rtol : float Error tolerance Returns ------- v1: numpy.ndarray Initial velocity vector v2: numpy.ndarray Final velocity vector """ # Check preconditions assert tof > 0 assert k > 0 # Check collinearity of r1 and r2 if not cross(r1, r2).any(): raise ValueError( "Lambert solution cannot be computed for collinear vectors") # Chord c = r2 - r1 c_norm, r1_norm, r2_norm = norm(c), norm(r1), norm(r2) # Semiperimeter s = (r1_norm + r2_norm + c_norm) * 0.5 # Versors i_r1, i_r2 = r1 / r1_norm, r2 / r2_norm i_h = cross(i_r1, i_r2) i_h = i_h / norm(i_h) # Fixed from paper # Geometry of the problem ll = np.sqrt(1 - min(1.0, c_norm / s)) # Compute the fundamental tangential directions if i_h[2] < 0: ll = -ll i_t1, i_t2 = cross(i_r1, i_h), cross(i_r2, i_h) else: i_t1, i_t2 = cross(i_h, i_r1), cross(i_h, i_r2) # Correct transfer angle parameter and tangential vectors if required ll, i_t1, i_t2 = (ll, i_t1, i_t2) if prograde else (-ll, -i_t1, -i_t2) # Non dimensional time of flight T = np.sqrt(2 * k / s**3) * tof # Find solutions x, y = _find_xy(ll, T, M, numiter, lowpath, rtol) # Reconstruct gamma = np.sqrt(k * s / 2) rho = (r1_norm - r2_norm) / c_norm sigma = np.sqrt(1 - rho**2) # Compute the radial and tangential components at r0 and r V_r1, V_r2, V_t1, V_t2 = _reconstruct(x, y, r1_norm, r2_norm, ll, gamma, rho, sigma) # Solve for the initial and final velocity v1 = V_r1 * (r1 / r1_norm) + V_t1 * i_t1 v2 = V_r2 * (r2 / r2_norm) + V_t2 * i_t2 return v1, v2
def vallado(k, r0, r, tof, M, prograde, lowpath, numiter, rtol): r"""Solves the Lambert's problem. The algorithm returns the initial velocity vector and the final one, these are computed by the following expresions: .. math:: \vec{v_{o}} &= \frac{1}{g}(\vec{r} - f\vec{r_{0}}) \\ \vec{v} &= \frac{1}{g}(\dot{g}\vec{r} - \vec{r_{0}}) Therefore, the lagrange coefficients need to be computed. For the case of Lamber's problem, they can be expressed by terms of the initial and final vector: .. math:: \begin{align} f = 1 -\frac{y}{r_{o}} \\ g = A\sqrt{\frac{y}{\mu}} \\ \dot{g} = 1 - \frac{y}{r} \\ \end{align} Where y(z) is a function that depends on the :py:mod:`poliastro.core.stumpff` coefficients: .. math:: y = r_{o} + r + A\frac{zS(z)-1}{\sqrt{C(z)}} \\ A = \sin{(\Delta \nu)}\sqrt{\frac{rr_{o}}{1 - \cos{(\Delta \nu)}}} The value of z to evaluate the stump functions is solved by applying a Numerical method to the following equation: .. math:: z_{i+1} = z_{i} - \frac{F(z_{i})}{F{}'(z_{i})} Function F(z) to the expression: .. math:: F(z) = \left [\frac{y(z)}{C(z)} \right ]^{\frac{3}{2}}S(z) + A\sqrt{y(z)} - \sqrt{\mu}\Delta t Parameters ---------- k : float Gravitational Parameter r0 : numpy.ndarray Initial position vector r : numpy.ndarray Final position vector tof : float Time of flight M : int Number of revolutions prograde: boolean Controls the desired inclination of the transfer orbit. lowpath: boolean If `True` or `False`, gets the transfer orbit whose vacant focus is below or above the chord line, respectively. numiter : int Number of iterations to rtol : int Number of revolutions Returns ------- v0: numpy.ndarray Initial velocity vector v: numpy.ndarray Final velocity vector Examples -------- >>> from poliastro.core.iod import vallado >>> from astropy import units as u >>> import numpy as np >>> from poliastro.bodies import Earth >>> k = Earth.k.to(u.km ** 3 / u.s ** 2) >>> r1 = np.array([5000, 10000, 2100]) * u.km # Initial position vector >>> r2 = np.array([-14600, 2500, 7000]) * u.km # Final position vector >>> tof = 3600 * u.s # Time of flight >>> v1, v2 = vallado(k.value, r1.value, r2.value, tof.value, M=0, prograde=True, lowpath=True, numiter=35, rtol=1e-8) >>> v1 = v1 * u.km / u.s >>> v2 = v2 * u.km / u.s >>> print(v1, v2) [-5.99249503 1.92536671 3.24563805] km / s [-3.31245851 -4.19661901 -0.38528906] km / s Notes ----- This procedure can be found in section 5.3 of Curtis, with all the theoretical description of the problem. Analytical example can be found in the same book under name Example 5.2. """ # TODO: expand for the multi-revolution case. # Issue: https://github.com/poliastro/poliastro/issues/858 if M > 0: raise NotImplementedError( "Multi-revolution scenario not supported for Vallado. See issue https://github.com/poliastro/poliastro/issues/858" ) t_m = 1 if prograde else -1 norm_r0 = norm(r0) norm_r = norm(r) norm_r0_times_norm_r = norm_r0 * norm_r norm_r0_plus_norm_r = norm_r0 + norm_r cos_dnu = (r0 @ r) / norm_r0_times_norm_r A = t_m * (norm_r * norm_r0 * (1 + cos_dnu))**0.5 if A == 0.0: raise RuntimeError("Cannot compute orbit, phase angle is 180 degrees") psi = 0.0 psi_low = -4 * np.pi**2 psi_up = 4 * np.pi**2 count = 0 while count < numiter: y = norm_r0_plus_norm_r + A * (psi * c3(psi) - 1) / c2(psi)**0.5 if A > 0.0: # Readjust xi_low until y > 0.0 # Translated directly from Vallado while y < 0.0: psi_low = psi psi = (0.8 * (1.0 / c3(psi)) * (1.0 - norm_r0_times_norm_r * np.sqrt(c2(psi)) / A)) y = norm_r0_plus_norm_r + A * (psi * c3(psi) - 1) / c2(psi)**0.5 xi = np.sqrt(y / c2(psi)) tof_new = (xi**3 * c3(psi) + A * np.sqrt(y)) / np.sqrt(k) # Convergence check if np.abs((tof_new - tof) / tof) < rtol: break count += 1 # Bisection check condition = tof_new <= tof psi_low = psi_low + (psi - psi_low) * condition psi_up = psi_up + (psi - psi_up) * (not condition) psi = (psi_up + psi_low) / 2 else: raise RuntimeError("Maximum number of iterations reached") f = 1 - y / norm_r0 g = A * np.sqrt(y / k) gdot = 1 - y / norm_r v0 = (r - f * r0) / g v = (gdot * r - r0) / g return v0, v
def vallado(k, r0, v0, tof, numiter): r"""Solves Kepler's Equation by applying a Newton-Raphson method. If the position of a body along its orbit wants to be computed for a specific time, it can be solved by terms of the Kepler's Equation: .. math:: E = M + e\sin{E} In this case, the equation is written in terms of the Universal Anomaly: .. math:: \sqrt{\mu}\Delta t = \frac{r_{o}v_{o}}{\sqrt{\mu}}\chi^{2}C(\alpha \chi^{2}) + (1 - \alpha r_{o})\chi^{3}S(\alpha \chi^{2}) + r_{0}\chi This equation is solved for the universal anomaly by applying a Newton-Raphson numerical method. Once it is solved, the Lagrange coefficients are returned: .. math:: \begin{align} f &= 1 \frac{\chi^{2}}{r_{o}}C(\alpha \chi^{2}) \\ g &= \Delta t - \frac{1}{\sqrt{\mu}}\chi^{3}S(\alpha \chi^{2}) \\ \dot{f} &= \frac{\sqrt{\mu}}{rr_{o}}(\alpha \chi^{3}S(\alpha \chi^{2}) - \chi) \\ \dot{g} &= 1 - \frac{\chi^{2}}{r}C(\alpha \chi^{2}) \\ \end{align} Lagrange coefficients can be related then with the position and velocity vectors: .. math:: \begin{align} \vec{r} &= f\vec{r_{o}} + g\vec{v_{o}} \\ \vec{v} &= \dot{f}\vec{r_{o}} + \dot{g}\vec{v_{o}} \\ \end{align} Parameters ---------- k : float Standard gravitational parameter. r0 : numpy.ndarray Initial position vector. v0 : numpy.ndarray Initial velocity vector. tof : float Time of flight. numiter : int Number of iterations. Returns ------- f: float First Lagrange coefficient g: float Second Lagrange coefficient fdot: float Derivative of the first coefficient gdot: float Derivative of the second coefficient Notes ----- The theoretical procedure is explained in section 3.7 of Curtis in really deep detail. For analytical example, check in the same book for example 3.6. """ # Cache some results dot_r0v0 = r0 @ v0 norm_r0 = norm(r0) sqrt_mu = k**0.5 alpha = -(v0 @ v0) / k + 2 / norm_r0 # First guess if alpha > 0: # Elliptic orbit xi_new = sqrt_mu * tof * alpha elif alpha < 0: # Hyperbolic orbit xi_new = ( np.sign(tof) * (-1 / alpha) ** 0.5 * np.log( (-2 * k * alpha * tof) / ( dot_r0v0 + np.sign(tof) * np.sqrt(-k / alpha) * (1 - norm_r0 * alpha) ) ) ) else: # Parabolic orbit # (Conservative initial guess) xi_new = sqrt_mu * tof / norm_r0 # Newton-Raphson iteration on the Kepler equation count = 0 while count < numiter: xi = xi_new psi = xi * xi * alpha c2_psi = c2(psi) c3_psi = c3(psi) norm_r = ( xi * xi * c2_psi + dot_r0v0 / sqrt_mu * xi * (1 - psi * c3_psi) + norm_r0 * (1 - psi * c2_psi) ) xi_new = ( xi + ( sqrt_mu * tof - xi * xi * xi * c3_psi - dot_r0v0 / sqrt_mu * xi * xi * c2_psi - norm_r0 * xi * (1 - psi * c3_psi) ) / norm_r ) if abs(xi_new - xi) < 1e-7: break else: count += 1 else: raise RuntimeError("Maximum number of iterations reached") # Compute Lagrange coefficients f = 1 - xi**2 / norm_r0 * c2_psi g = tof - xi**3 / sqrt_mu * c3_psi gdot = 1 - xi**2 / norm_r * c2_psi fdot = sqrt_mu / (norm_r * norm_r0) * xi * (psi * c3_psi - 1) return f, g, fdot, gdot
def compute_flyby(v_spacecraft, v_body, k, r_p, theta): """Computes outbound velocity after a flyby and the turn angle Parameters ---------- k : float Standard Gravitational parameter. r_p : float Radius of periapsis, measured from the center of the body. v_spacecraft : float Velocity of the spacecraft, relative to the attractor of the body. v_body : float Velocity of the body, relative to its attractor. theta : float Aim angle of the B vector. Returns ------- v_spacecraft_out : float Outbound velocity of the spacecraft. delta : float 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]) T_vec = cross(S_vec, c_vec) T_vec = T_vec / norm(T_vec) R_vec = cross(S_vec, T_vec) # 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 = cross(B_vec / b, S_vec) # And now we rotate the outbound hyperbolic excess velocity # u_vec = v_inf_1 / norm(v_inf) = S_vec v_vec = cross(rot_v, v_inf_1) 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