def test_position_of_radec(): epsilon = _GIGAPARSEC_AU * 1e-16 p = api.position_of_radec(0, 0) assert length_of(p.position.au - [_GIGAPARSEC_AU, 0, 0]) < epsilon p = api.position_of_radec(6, 0) assert length_of(p.position.au - [0, _GIGAPARSEC_AU, 0]) < epsilon epsilon = 2e-16 p = api.position_of_radec(12, 90, 2) assert length_of(p.position.au - [0, 0, 2]) < epsilon p = api.position_of_radec(12, 90, distance_au=2) assert length_of(p.position.au - [0, 0, 2]) < epsilon ts = api.load.timescale() epoch = ts.tt_jd(api.B1950) p = api.position_of_radec(0, 0, 1, epoch=epoch) assert length_of(p.position.au - [1, 0, 0]) > 1e-16 ra, dec, distance = p.radec(epoch=epoch) assert abs(ra.hours) < 1e-12 assert abs(dec.degrees) < 1e-12 assert abs(distance.au - 1) < 1e-16
def test_position_from_radec(): # Only a couple of minimal tests, since the routine is deprecated. p = api.position_from_radec(0, 0) assert length_of(p.position.au - [1, 0, 0]) < 1e-16 p = api.position_from_radec(6, 0) assert length_of(p.position.au - [0, 1, 0]) < 1e-16
def make(cls, position: Astrometric) -> "ReflectionParameters": # This is a ridiculous hack borrowed from skylib's planetary magnitudes library. Shamelessly # treat the Sun as being at the Solar System barycenter. sunToObserver = position.center_barycentric.position.au observerToPlanet = position.position.au sunToPlanet = sunToObserver + observerToPlanet return ReflectionParameters( position=position, r=length_of(sunToPlanet), delta=length_of(observerToPlanet), alpha=angle_between(-sunToPlanet, -observerToPlanet) * RAD2DEG, )
def _correct_for_light_travel_time2(observer, target): # # This version makes subtraction work by replacing normal NumPy # broadcasting subtraction with a sub() function of our own that # broadcasts in the other direction. # t = observer.t ts = t.ts whole = t.whole tdb_fraction = t.tdb_fraction cposition = observer.position.au cvelocity = observer.velocity.au_per_d tposition, tvelocity, gcrs_position, message = target._at(t) distance = length_of(sub(tposition, cposition)) print('distance', distance.shape) # (t, targets) light_time0 = 0.0 for i in range(10): light_time = distance / C_AUDAY # GOOD: scalar delta = light_time - light_time0 # GOOD first time: scalar; 2nd: ? if abs(max(delta)) < 1e-12: break # We assume a light travel time of at most a couple of days. A # longer light travel time would best be split into a whole and # fraction, for adding to the whole and fraction of TDB. print('tdb_fraction', tdb_fraction.shape, '[ORIGINAL]') print('light_time', light_time.shape) diff = sub(tdb_fraction, light_time) print('sub()', diff.shape) # (4,2)? YES!!! print('whole', whole.shape) # (4,)? YES!!! # whole, diff = _reconcile(whole, diff) # Winds up not needed? # print('sub()', diff.shape) # (4,2) # print('whole', whole.shape) # (4,1) t2 = ts.tdb_jd(whole, diff) print('t2', t2.shape) # (4, 1)? Why not (4, 2)? Because it prints top. tposition, tvelocity, gcrs_position, message = target._at(t2) print('tposition', tposition.shape) # Needs to be 3,4,2 distance = length_of(sub(tposition, cposition)) light_time0 = light_time #exit() else: raise ValueError('light-travel time failed to converge') return sub(tposition, cposition), sub(tvelocity, cvelocity), t, light_time
def test_velocity(): # It looks like this is a sweet spot for accuracy: presumably a # short enough fraction of a second that the vector does not time to # change direction much, but long enough that the direction does not # get lost down in the noise. factor = 300.0 ts = load.timescale() t = ts.utc(2019, 11, 2, 3, 53, [0, 1.0 / factor]) jacob = Topos(latitude_degrees=36.7138, longitude_degrees=-112.2169) p = jacob.at(t) velocity1 = p.position.km[:,1] - p.position.km[:,0] velocity2 = p.velocity.km_per_s[:,0] print(length_of(velocity2 - factor * velocity1)) assert length_of(velocity2 - factor * velocity1) < 0.0007
def intersect_line_with_sphere(self, ut, target_pos, test_body, dist_limit=ERAD / 1000. + 200.): """Compute distance to the nearest intersection of a line with a sphere. line_endpoint: NumPy array giving a point on a line starting at the origin. sphere_origin: NumPy array giving the center of the sphere. sphere_radius: Float giving the sphere radius. """ sat_origin_pos = test_body.at(ut) sat_ecliptic = sat_origin_pos.ecliptic_position().au + self.at( ut).ecliptic_position().au test_body_ecliptic = test_body.at(ut).ecliptic_position().au target_ecliptic = target_pos.ecliptic_position().au line_endpoint = target_ecliptic - sat_ecliptic sphere_center = test_body_ecliptic - sat_ecliptic sphere_radius = dist_limit / AU_KM unit = line_endpoint / length_of(line_endpoint) b = -2.0 * (unit * sphere_center).sum() c = (sphere_center * sphere_center).sum() - sphere_radius * sphere_radius discriminant = b * b - 4 * c return np.nan if discriminant < 0 else ( max(-b - np.sqrt(discriminant), -b + np.sqrt(discriminant)) / 2.0)
def find_closest_satellite(self, t, xyz, satlist): a = SatrecArray([sat.model for sat in satlist]) # jd = np.array([t._utc_float()]) # skyfield 1.2x or so.... jd = np.array([t.whole + t.tai_fraction - t._leap_seconds() / DAY_S]) e, r, v = a.sgp4(jd, jd * 0.0) r = r[:,0,:] # eliminate t axis since we only have one time v = v[:,0,:] r = r.T # move x,y,z to top level like Skyfield expects v = v.T ut1 = np.array([t.ut1]) r, v = TEME_to_ITRF(ut1, r, v) r2=np.array(xyz) r2.shape = 3, 1 # add extra dimension to stand in for time # sep_a = angle_between(r, r2) sep_d = length_of(r-r2) i = np.argmin(sep_d) closest_satellite = satlist[i] # closest_angle = sep_a[i] / tau * 360.0 closest_distance = sep_d[i] if False: print("Position:",xyz,"at",t.utc_strftime(),":") for idx,s in enumerate(sorted(satlist, key=lambda sat: sat.name)): print(" %s: %8.2fkm %s"%(s.name,sep_d[idx],["","*"][i==idx])) return closest_satellite, closest_distance
def umbra_radius(self, t: Time) -> float: jd, fr = t.whole, t.tdb_fraction b = self.earth_barycenter.compute(jd, fr) e = self.earth.compute(jd, fr) m = self.moon.compute(jd, fr) s = self.sun.compute(jd, fr) earth_to_sun = s - b - e moon_to_earth = e - m solar_radius_km = 696340.0 pi_m = ERAD / 1e3 / length_of(moon_to_earth) pi_s = ERAD / 1e3 / length_of(earth_to_sun) s_s = solar_radius_km / length_of(earth_to_sun) atmosphere_blur = 1.02 pi_1 = 0.998340 * pi_m umbra_radius = atmosphere_blur * (pi_1 + pi_s - s_s) return umbra_radius
def moon_radius(self, t: Time) -> float: jd, fr = t.whole, t.tdb_fraction e = self.earth.compute(jd, fr) m = self.moon.compute(jd, fr) moon_to_earth = e - m moon_radius_km = 1737.1 moon_radius = arcsin(moon_radius_km / length_of(moon_to_earth)) return moon_radius
def _correct_for_light_travel_time4(observer, target): """Return a light-time corrected astrometric position and velocity. Given an `observer` that is a `Barycentric` position somewhere in the solar system, compute where in the sky they will see the body `target`, by computing the light-time between them and figuring out where `target` was back when the light was leaving it that is now reaching the eyes or instruments of the `observer`. """ t = observer.t ts = t.ts whole = t.whole tdb_fraction = t.tdb_fraction cposition = observer.position.au cvelocity = observer.velocity.au_per_d print('======', cposition.shape) # ? print('tdb_fraction before:', tdb_fraction.shape) tdb_fraction = tdb_fraction[:,None] print('tdb_fraction after:', tdb_fraction.shape) tposition, tvelocity, gcrs_position, message = target._at(t) distance = length_of(tposition - cposition) light_time0 = 0.0 for i in range(10): print('i =', i) light_time = distance / C_AUDAY delta = light_time - light_time0 if abs(max(delta)) < 1e-12: break # We assume a light travel time of at most a couple of days. A # longer light travel time would best be split into a whole and # fraction, for adding to the whole and fraction of TDB. t2 = ts.tdb_jd(whole, tdb_fraction - light_time) tposition, tvelocity, gcrs_position, message = target._at(t2) distance = length_of(tposition - cposition) light_time0 = light_time else: raise ValueError('light-travel time failed to converge') return tposition - cposition, tvelocity - cvelocity, t, light_time
def _correct_for_light_travel_time3(observer, target): # # This version uses normal NumPy subtraction with its default # broadcasting, which it makes work by always turning the position # vectors upside down when it receives them from other parts of # Skyfield, then turning them back over when its own computations # are done. # t = observer.t ts = t.ts whole = t.whole tdb_fraction = t.tdb_fraction cposition = observer.position.au cvelocity = observer.velocity.au_per_d cposition = cposition.T cvelocity = cvelocity.T tposition, tvelocity, gcrs_position, message = target._at(t) tposition = tposition.T tvelocity = tvelocity.T distance = length_of((tposition - cposition).T).T light_time0 = 0.0 for i in range(10): light_time = distance / C_AUDAY delta = light_time - light_time0 if abs(max(delta)) < 1e-12: break # We assume a light travel time of at most a couple of days. A # longer light travel time would best be split into a whole and # fraction, for adding to the whole and fraction of TDB. t2 = ts.tdb_jd(whole, (tdb_fraction - light_time).T) tposition, tvelocity, gcrs_position, message = target._at(t2) tposition = tposition.T tvelocity = tvelocity.T distance = length_of((tposition - cposition).T).T light_time0 = light_time else: raise ValueError('light-travel time failed to converge') tposition = tposition - cposition tvelocity = tvelocity - cvelocity return tposition.T, tvelocity.T, t, light_time
def test_position_from_radec(): p = api.position_from_radec(0, 0) assert length_of(p.position.au - [1, 0, 0]) < 1e-16 p = api.position_from_radec(6, 0) assert length_of(p.position.au - [0, 1, 0]) < 1e-16 p = api.position_from_radec(12, 90, 2) assert length_of(p.position.au - [0, 0, 2]) < 2e-16 ts = api.load.timescale(builtin=True) epoch = ts.tt_jd(api.B1950) p = api.position_from_radec(0, 0, epoch=epoch) assert length_of(p.position.au - [1, 0, 0]) > 1e-16 ra, dec, distance = p.radec(epoch=epoch) assert abs(ra.hours) < 1e-12 assert abs(dec.degrees) < 1e-12 assert abs(distance.au - 1) < 1e-16
def test_velocity_in_ITRF_to_GCRS2(): # TODO: Get test working with these vectors too, showing it works # with a non-zero velocity vector, but in that case the test will # have to be fancier in how it corrects. # r = np.array([(1, 0, 0), (1, 1 / DAY_S, 0)]).T # v = np.array([(0, 1, 0), (0, 1, 0)]).T ts = api.load.timescale() t = ts.utc(2020, 7, 17, 8, 51, [0, 1]) r = np.array([(1, 0, 0), (1, 0, 0)]).T v = np.array([(0, 0, 0), (0, 0, 0)]).T r, v = ITRF_to_GCRS2(t, r, v, True) # Rotate back to equinox-of-date before applying correction. r = mxv(t.M, r) v = mxv(t.M, v) r0, r1 = r.T v0 = v[:,0] # Apply a correction: the instantaneous velocity does not in fact # carry the position in a straight line, but in an arc around the # origin; so use trigonometry to move the destination point to where # linear motion would have carried it. angvel = (t.gast[1] - t.gast[0]) / 24.0 * tau r1 = mxv(rot_z(np.arctan(angvel) - angvel), r1) r1 *= np.sqrt(1 + angvel*angvel) actual_motion = r1 - r0 predicted_motion = v0 / DAY_S relative_error = (length_of(actual_motion - predicted_motion) / length_of(actual_motion)) acceptable_error = 4e-12 assert relative_error < acceptable_error
def propagate(position, velocity, t0, t1, gm): """Propagates a position and velocity vector with an array of times. Based on the function toolkit/src/spicelib/prop2b.f from the SPICE toolkit, which can be downloaded from naif.jpl.nasa.gov/naif/toolkit_FORTRAN.html Parameters ---------- position : ndarray Position vector with shape (3,) velocity : ndarray Velocity vector with shape (3,) t0 : float Time corresponding to `position` and `velocity` t1 : float or ndarray Time or times to propagate to gm : float Gravitational parameter in units that match the other arguments """ if gm <= 0: raise ValueError("'gm' should be positive") if length_of(velocity) == 0: raise ValueError('Velocity vector has zero magnitude') if length_of(position) == 0: raise ValueError('Position vector has zero magnitude') r0 = length_of(position) rv = dots(position, velocity) hvec = cross(position, velocity) h2 = dots(hvec, hvec) if h2 == 0: raise ValueError('Motion is not conical') eqvec = cross(velocity, hvec) / gm + -position / r0 e = length_of(eqvec) q = h2 / (gm * (1 + e)) f = 1 - e b = sqrt(q / gm) br0 = b * r0 b2rv = b**2 * rv bq = b * q qovr0 = q / r0 maxc = max(abs(br0), abs(b2rv), abs(bq), abs(qovr0 / (bq))) if f < 0: fixed = log(dpmax / 2) - log(maxc) rootf = sqrt(-f) logf = log(-f) bound = min(fixed / rootf, (fixed + 1.5 * logf) / rootf) else: logbound = (log(1.5) + log(dpmax) - log(maxc)) / 3 bound = exp(logbound) def kepler(x): c0, c1, c2, c3 = stumpff(f * x * x) return x * (br0 * c1 + x * (b2rv * c2 + x * (bq * c3))) dt = t1 - t0 if not isinstance(dt, ndarray): dt = array([dt]) return_1d_array = True else: return_1d_array = False x = bracket(dt / bq, -bound, bound) kfun = kepler(x) past = dt < 0 future = dt > 0 upper = zeros_like(dt, dtype='float64') lower = zeros_like(dt, dtype='float64') oldx = zeros_like(dt, dtype='float64') lower[past] = x[past] upper[future] = x[future] while (kfun[past] > dt[past]).any(): upper[past] = lower[past] lower[past] *= 2 oldx[past] = x[past] x[past] = bracket(lower[past], -bound, bound) if (x[past] == oldx[past]).any(): raise ValueError('The input delta time (dt) has a value of {0}.' 'This is beyond the range of DT for which we ' 'can reliably propagate states. The limits for ' 'this GM and initial state are from {1}' 'to {2}.'.format(dt, kepler(-bound), kepler(bound))) kfun[past] = kepler(x[past]) while (kfun[future] < dt[future]).any(): lower[future] = upper[future] upper[future] *= 2 oldx[future] = x[future] x[future] = bracket(upper[future], -bound, bound) if (x[future] == oldx[future]).any(): raise ValueError('The input delta time (dt) has a value of {0}.' 'This is beyond the range of DT for which we ' 'can reliably propagate states. The limits for ' 'this GM and initial state are from {1} ' 'to {2}.'.format(dt, kepler(-bound), kepler(bound))) kfun[future] = kepler(x[future]) x = amin(array([upper, amax(array([lower, (lower + upper) / 2]), axis=0)]), axis=0) lcount = zeros_like(dt) mostc = ones_like(dt) * 1000 not_done = (lower < x) * (x < upper) while not_done.any(): kfun[not_done] = kepler(x[not_done]) high = (kfun > dt) * not_done low = (kfun < dt) * not_done same = (~high * ~low) * not_done upper[high] = x[high] lower[low] = x[low] upper[same] = lower[same] = x[same] condition = not_done * (mostc > 64) * (upper != 0) * (lower != 0) mostc[condition] = 64 lcount[condition] = 0 # vectorized version of min(upper, max(lower, (upper + lower)/2)) x[not_done] = amin(array([ upper[not_done], amax(array( [lower[not_done], (lower[not_done] + upper[not_done]) / 2]), axis=0) ]), axis=0) lcount += 1 not_done = (lower < x) * (x < upper) * (lcount < mostc) c0, c1, c2, c3 = stumpff(f * x * x) br = br0 * c0 + x * (b2rv * c1 + x * (bq * c2)) pc = 1 - qovr0 * x**2 * c2 vc = dt - bq * x**3 * c3 pcdot = -qovr0 / br * x * c1 vcdot = 1 - bq / br * x**2 * c2 if return_1d_array: position_prop = pc * position + vc * velocity velocity_prop = pcdot * position + vcdot * velocity else: position_prop = pc * tile(position[newaxis].T, dt.size) + vc * tile( velocity[newaxis].T, dt.size) velocity_prop = pcdot * tile(position[newaxis].T, dt.size) + vcdot * tile( velocity[newaxis].T, dt.size) return position_prop, velocity_prop
def trajectory(context): """Calculate the path loss for a given trajectory. Args (context): tle_file: the file containing the TLE of all the satellites, must contains the following columns: tle1, tle2, norad_id. trajectory_file: the file containing the trajectory, must contains the following columns: altitude, longitude, latitude, time. frequency: the frequency, in hertz, of the carrier timestamp: the UTC Epoch timestamp (number of seconds since 01/01/1970). output_file: the file where the data are saved. confirm: whether or not we have to ask for confirmation. """ satellites_file = context.tle_file trajectory_file = context.trajectory_file frequency = context.frequency timestamp = context.time save_file = context.output_file write_trajectories = context.write_trajectories confirm = context.confirm print("Calculating the trajectory") # Set up skyfield load = Loader(".") data = load('de421.bsp') ts = load.timescale() planets = load('de421.bsp') # Load satellites orbits satellites = {} with open(satellites_file, 'r') as csvfile: reader = csv.reader(csvfile, delimiter=',') header = next(reader, None) tle1_index, tle2_index, id_index = header_indexes(header, ["tle1", "tle2", "norad_id"]) for row in reader: name, L1, L2 = row[id_index], row[tle1_index], row[tle2_index] satellites[name] = EarthSatellite(L1, L2) # Check if the output file already exists if confirm and os.path.isfile(save_file): if not confirmation("\"{}\" already exists. Overwrite it ?".format(save_file)): raise RuntimeError("Aborting trajectory calculation.") # Calculate attenuation at each point of the trajectory data = [] # List of list, [time, dist1, dist2, ..., dist N, minimum dist, minimum name, path_loss] with open(trajectory_file, 'r') as csvfile: reader = csv.reader(csvfile, delimiter=',', quotechar='\"') header = next(reader, None) altitude_index, longitude_index, latitude_index, time_index = header_indexes(header, ["altitude", "longitude", "latitude", "time"]) for row in reader: tofloat = lambda s : float(s.replace(',','.')) try: altitude, longitude, latitude, rela_time = tofloat(row[altitude_index]), tofloat(row[longitude_index]), tofloat(row[latitude_index]), tofloat(row[time_index]) except ValueError as e: raise RuntimeError("Ill-formed trajectory file ({}).".format(e)) epoch_time = timestamp + rela_time new_time = datetime.datetime.utcfromtimestamp(epoch_time) time = ts.utc(new_time.year, new_time.month, new_time.day, new_time.hour, new_time.minute, new_time.second) pos = Topos(longitude_degrees=longitude, latitude_degrees=latitude, elevation_m=altitude).at(time) data.append([epoch_time, longitude, latitude, altitude]) min_dist = math.inf min_name = "None" dists = [] for name, sat in satellites.items(): pos_relay = sat.at(time) dist = length_of((pos_relay-pos).distance().m) dists.append(dist) los = line_of_sight(pos, pos_relay) dists.append(path_loss(frequency, dist) if los else "") dists.append(line_of_sight(pos, pos_relay)) if los and dist < min_dist: min_dist = dist min_name = name if write_trajectories: sub = pos_relay.subpoint() # dists.extend(list(pos_relay.itrf_xyz().m)) dists.extend([sub.longitude.degrees, sub.latitude.degrees, sub.elevation.m]) data[-1].append(min_dist) data[-1].append(min_name) data[-1].append(path_loss(frequency, min_dist)) data[-1].extend(dists) # Save the file with open(save_file, 'w', newline='') as csvfile: spamwriter = csv.writer(csvfile, delimiter=',') sat_headers = [] for name in satellites: sat_headers.extend([name + ":dist (m)", name + ":path_loss (dB)", name + ":los"]) if write_trajectories: sat_headers.extend([name+":longitude (°)", name+":latitude (°)", name+":altitude (m)"]) spamwriter.writerow(["time (s)", "longitude (°)", "latitude (°)", "altitude (m)"] + ["minimum_dist (m)", "minimum_name (norad id)", "path_loss (dB)"] + sat_headers) for line in data: spamwriter.writerow(line)
def propagate(position, velocity, t0, t1, gm): """Propagates a position and velocity vector with an array of times. Based on the function toolkit/src/spicelib/prop2b.f from the SPICE toolkit, which can be downloaded from naif.jpl.nasa.gov/naif/toolkit_FORTRAN.html Parameters ---------- position : ndarray Position vector with shape (3,) velocity : ndarray Velocity vector with shape (3,) t0 : float Time corresponding to `position` and `velocity` t1 : float or ndarray Time or times to propagate to gm : float Gravitational parameter in units that match the other arguments """ gm = atleast_1d(gm) if (gm <= 0).any(): raise ValueError("'gm' should be positive") if (length_of(velocity)).any() == 0: raise ValueError('Velocity vector has zero magnitude') if (length_of(position)).any() == 0: raise ValueError('Position vector has zero magnitude') if position.ndim == 1: position = position[:, newaxis] if velocity.ndim == 1: velocity = velocity[:, newaxis] r0 = length_of(position) rv = dots(position, velocity) hvec = _cross(position, velocity) h2 = dots(hvec, hvec) if (h2 == 0).any(): raise ValueError('Motion is not conical') eqvec = _cross(velocity, hvec) / gm + -position / r0 e = length_of(eqvec) q = h2 / (gm * (1 + e)) f = 1 - e b = sqrt(q / gm) br0 = b * r0 b2rv = b * b * rv bq = b * q qovr0 = q / r0 maxc = amax(array([abs(br0), abs(b2rv), abs(bq), abs(qovr0 / bq)]), axis=0) hyperbolic = (f < 0) bound = zeros_like(f) fixed = log(dpmax / 2) - log(maxc[hyperbolic]) rootf = sqrt(-f[hyperbolic]) logf = log(-f[hyperbolic]) bound[hyperbolic] = amin(array( [fixed / rootf, (fixed + 1.5 * logf) / rootf]), axis=0) logbound = (log(1.5) + log(dpmax) - log(maxc[~hyperbolic])) / 3 bound[~hyperbolic] = exp(logbound) # each of these arrays has 1 entry per orbit, so its shape is (#orbits, 1) f = f[:, newaxis] bq = bq[:, newaxis] b2rv = b2rv[:, newaxis] br0 = br0[:, newaxis] qovr0 = qovr0[:, newaxis] bound = bound[:, newaxis] def kepler(x): _, c1, c2, c3 = stumpff(f * x * x) return x * (br0 * c1 + x * (b2rv * c2 + x * bq * c3)) def kepler_1d(x, orb_inds): _, c1, c2, c3 = stumpff(x * x * repeat(f, orb_inds)) return x * (c1 * repeat(br0, orb_inds) + x * (c2 * repeat(b2rv, orb_inds) + x * (c3 * repeat(bq, orb_inds)))) t1 = atleast_1d(t1) t0 = atleast_1d(t0) if len(t0) == 1: t0 = repeat(t0, position.shape[1]) # shape of 2 dimensional arrays from here on out should be (#orbits, len(t1)) dt = t1 - t0[:, newaxis] x = dt / bq copyto(x, -bound, where=(x < -bound)) copyto(x, bound, where=(x > bound)) kfun = kepler(x) past = dt < 0 future = dt > 0 upper = zeros_like(dt, dtype='float64') lower = zeros_like(dt, dtype='float64') oldx = zeros_like(dt, dtype='float64') copyto(lower, x, where=past) copyto(upper, x, where=future) while (kfun[past] > dt[past]).any(): copyto(upper, lower, where=past) lower[past] *= 2 copyto(oldx, x, where=past) orb_ind = sum(past, axis=1) x[past] = clip(lower[past], repeat(-bound, orb_ind), repeat(bound, orb_ind)) if (x[past] == oldx[past]).any(): raise ValueError('The input delta time (dt) has a value of {0}.' 'This is beyond the range of DT for which we ' 'can reliably propagate states. The limits for ' 'this GM and initial state are from {1}' 'to {2}.'.format(dt, kepler(-bound), kepler(bound))) kfun[past] = kepler_1d(x[past], orb_ind) while (kfun[future] < dt[future]).any(): copyto(lower, upper, where=future) upper[future] *= 2 copyto(oldx, x, where=future) orb_ind = sum(future, axis=1) x[future] = clip(upper[future], repeat(-bound, orb_ind), repeat(bound, orb_ind)) if (x[future] == oldx[future]).any(): raise ValueError('The input delta time (dt) has a value of {0}.' 'This is beyond the range of DT for which we ' 'can reliably propagate states. The limits for ' 'this GM and initial state are from {1} ' 'to {2}.'.format(dt, kepler(-bound), kepler(bound))) kfun[future] = kepler_1d(x[future], orb_ind) x = copy(upper) copyto(x, (upper + lower) / 2, where=(lower <= upper)) lcount = zeros_like(dt) mostc = full_like(dt, 1000) not_done = (lower < x) & (x < upper) while not_done.any(): orb_inds = sum(not_done, axis=1) kfun[not_done] = kepler_1d(x[not_done], orb_inds) high = (kfun > dt) & not_done low = (kfun < dt) & not_done same = (~high & ~low) & not_done copyto(upper, x, where=(high | same)) copyto(lower, x, where=(low | same)) condition = not_done & (mostc > 64) & (upper != 0) & (lower != 0) mostc[condition] = 64 lcount[condition] = 0 copyto(x, upper, where=(not_done & (lower > upper))) copyto(x, (upper + lower) / 2, where=(not_done & (lower <= upper))) lcount += 1 not_done = (lower < x) & (x < upper) & (lcount < mostc) c0, c1, c2, c3 = stumpff(f * x * x) br = br0 * c0 + x * (b2rv * c1 + x * bq * c2) pc = 1 - qovr0 * x * x * c2 vc = dt - bq * x**3 * c3 pcdot = -qovr0 / br * x * c1 vcdot = 1 - bq / br * x * x * c2 position_prop = pc[newaxis, :, :] * position[:, :, newaxis] + vc[ newaxis, :, :] * velocity[:, :, newaxis] velocity_prop = pcdot[newaxis, :, :] * position[:, :, newaxis] + vcdot[ newaxis, :, :] * velocity[:, :, newaxis] return squeeze(position_prop), squeeze(velocity_prop)