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
示例#3
0
    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,
        )
示例#4
0
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
示例#5
0
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
示例#6
0
    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)
示例#7
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
示例#8
0
 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
示例#9
0
 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
示例#10
0
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
示例#11
0
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
示例#12
0
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
示例#13
0
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
示例#14
0
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
示例#15
0
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)
示例#16
0
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)