Skip to content

Commit

Permalink
add user input to specify prediction start time + location (#68)
Browse files Browse the repository at this point in the history
* add user input to specify prediction start time + location

* handle empty call

* use last packet
  • Loading branch information
zacharyburnett authored Jul 10, 2021
1 parent b8da0ad commit 79c0287
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 36 deletions.
16 changes: 16 additions & 0 deletions packetraven/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def main():
'--prediction-output',
help='path to output file to save most up-to-date predicted trajectory',
)
args_parser.add_argument(
'--prediction-start-location', help='start location to use for prediction (x,y,z)'
)
args_parser.add_argument(
'--prediction-start-time', help='start time to use for prediction'
)
args_parser.add_argument(
'--prediction-ascent-rate', help='ascent rate to use for prediction (m/s)'
)
Expand Down Expand Up @@ -190,6 +196,16 @@ def main():
if not prediction_filename.parent.exists():
prediction_filename.parent.mkdir(parents=True, exist_ok=True)

if args.prediction_start_location is not None:
kwargs['prediction_start_location'] = (
float(value) for value in args.prediction_start_location.split(',')
)

if args.prediction_start_time is not None:
kwargs['prediction_start_time'] = convert_value(
args.prediction_start_time, datetime
)

if args.prediction_ascent_rate is not None:
kwargs['prediction_ascent_rate'] = float(args.prediction_ascent_rate)

Expand Down
21 changes: 11 additions & 10 deletions packetraven/gui/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,16 +814,17 @@ async def retrieve_packets(self):
else:
updated_callsigns = {}

await asyncio.wait(
[
self.__update_callsign_window(
callsign,
only_time=callsign not in updated_callsigns,
current_time=current_time,
)
for callsign, packet_track in self.__packet_tracks.items()
]
)
if len(self.__packet_tracks) > 0:
await asyncio.wait(
[
self.__update_callsign_window(
callsign,
only_time=callsign not in updated_callsigns,
current_time=current_time,
)
for callsign, packet_track in self.__packet_tracks.items()
]
)

if self.running:
self.__timeout = teek.after(
Expand Down
30 changes: 30 additions & 0 deletions packetraven/packets/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ def length(self) -> float:
def __getitem__(
self, index: Union[int, Iterable[int], slice]
) -> Union[LocationPacket, 'LocationPacketTrack']:
if isinstance(index, str):
try:
index = parse_date(index)
except:
index = int(index)
if isinstance(index, int):
return self.packets[index]
elif isinstance(index, Iterable) or isinstance(index, slice):
Expand All @@ -162,6 +167,31 @@ def __getitem__(
else:
packets = None
return self.__class__(self.name, packets, self.crs)
elif isinstance(index, datetime):
matching_packets = []
for packet in self.packets:
if packet.time == index:
matching_packets.append(packet)
if len(matching_packets) == 0:
maximum_interval = numpy.timedelta64(int(max(self.intervals)), 's')
if (
index > min(self.times) - maximum_interval
and index < max(self.times) + maximum_interval
):
smallest_difference = None
closest_time = None
for packet in self.packets:
difference = abs(packet.time - index)
if smallest_difference is None or difference < smallest_difference:
smallest_difference = difference
closest_time = packet.time
return self[closest_time]
else:
raise IndexError(f'time index out of range: {index}')
elif len(matching_packets) == 1:
return matching_packets[0]
else:
return self.__class__(self.name, matching_packets, self.crs)
else:
raise ValueError(f'unrecognized index: {index}')

Expand Down
69 changes: 43 additions & 26 deletions packetraven/predicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ def query(self):

def get_predictions(
packet_tracks: {str: LocationPacketTrack},
start_location: (float, float, float) = None,
start_time: datetime = None,
ascent_rate: float = None,
burst_altitude: float = None,
sea_level_descent_rate: float = None,
Expand All @@ -381,6 +383,8 @@ def get_predictions(
Return location tracks detailing predicted trajectory of balloon flight(s) from current location.
:param packet_tracks: location packet tracks
:param start_time: start location
:param start_location: start time
:param ascent_rate: ascent rate (m/s)
:param burst_altitude: altitude at which balloon will burst (m)
:param sea_level_descent_rate: descent rate of payload at sea level (m/s)
Expand All @@ -390,34 +394,46 @@ def get_predictions(
:param api_url: URL of prediction API to use
"""

if api_url is None:
api_url = PredictionAPIURL.cusf
if start_location is not None:
if len(start_location) == 2:
start_location = (*start_location, 0)

if float_altitude is not None and float_duration is None:
raise ValueError('`float_duration` was not provided')

if float_duration is not None and float_altitude is None:
float_altitude = burst_altitude

if api_url is None:
api_url = PredictionAPIURL.cusf

prediction_tracks = {}
for name, packet_track in packet_tracks.items():
prediction_start_location = start_location
prediction_start_time = start_time
prediction_ascent_rate = ascent_rate
prediction_burst_altitude = burst_altitude
prediction_sea_level_descent_rate = sea_level_descent_rate
prediction_float_altitude = float_altitude

ascent_rates = packet_track.ascent_rates
if ascent_rate is None:
if prediction_ascent_rate is None:
average_ascent_rate = ascent_rates[ascent_rates > 0]
if average_ascent_rate > 0:
ascent_rate = average_ascent_rate
else:
ascent_rate = DEFAULT_ASCENT_RATE
if burst_altitude is None:
burst_altitude = DEFAULT_BURST_ALTITUDE
if sea_level_descent_rate is None:
sea_level_descent_rate = DEFAULT_SEA_LEVEL_DESCENT_RATE
prediction_ascent_rate = average_ascent_rate

# if packet track is descending, override given burst altitude
if len(ascent_rates) > 2 and all(ascent_rates[-2:] < 0):
burst_altitude = packet_track.altitudes[-1] + 1
prediction_burst_altitude = packet_track.altitudes[-1] + 1

if prediction_start_time is None:
prediction_start_time = packet_track[-1].time

prediction_start_location = packet_track[-1].coordinates
prediction_start_time = packet_track[-1].time
if prediction_start_location is None:
try:
prediction_start_location = packet_track[prediction_start_time].coordinates
except KeyError:
prediction_start_location = packet_track[-1].coordinates

if float_altitude is not None and not packet_track.falling:
packets_at_float_altitude = packet_track[
Expand All @@ -427,32 +443,33 @@ def get_predictions(
len(packets_at_float_altitude) > 0
and packets_at_float_altitude[-1].time == packet_track.times[-1]
):
float_start_time = packets_at_float_altitude[0].time
prediction_float_start_time = packets_at_float_altitude[0].time
descent_only = False
elif packet_track.ascent_rates[-1] >= 0:
float_start_time = prediction_start_time + timedelta(
seconds=(float_altitude - prediction_start_location[2]) / ascent_rate
prediction_float_start_time = prediction_start_time + timedelta(
seconds=(prediction_float_altitude - prediction_start_location[2])
/ prediction_ascent_rate
)
descent_only = False
else:
float_start_time = None
prediction_float_start_time = None
descent_only = True
if float_start_time is not None:
float_end_time = float_start_time + float_duration
if prediction_float_start_time is not None:
prediction_float_end_time = prediction_float_start_time + float_duration
else:
float_end_time = None
prediction_float_end_time = None
else:
float_end_time = None
prediction_float_end_time = None
descent_only = packet_track.falling or packet_track.ascent_rates[-1] < 0

prediction_query = CUSFBalloonPredictionQuery(
launch_site=prediction_start_location,
launch_time=prediction_start_time,
ascent_rate=ascent_rate,
burst_altitude=burst_altitude,
sea_level_descent_rate=sea_level_descent_rate,
float_altitude=float_altitude,
float_end_time=float_end_time,
ascent_rate=prediction_ascent_rate,
burst_altitude=prediction_burst_altitude,
sea_level_descent_rate=prediction_sea_level_descent_rate,
float_altitude=prediction_float_altitude,
float_end_time=prediction_float_end_time,
api_url=api_url,
name=name,
descent_only=descent_only,
Expand Down
29 changes: 29 additions & 0 deletions tests/test_packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,32 @@ def test_sorting():
track = APRSTrack('W3EAX-13', [packet_2, packet_1, packet_3])

assert sorted(track) == [packet_1, packet_2, packet_3]


def test_index():
packet_1 = APRSPacket.from_frame(
"W3EAX-13>APRS,N3KTX-10*,WIDE1,WIDE2-1,qAR,N3TJJ-11:!/:J..:sh'O /A=053614|!g| /W3EAX,313,0,21'C,"
'nearspace.umd.edu',
packet_time=datetime(2019, 2, 3, 14, 36, 16),
)
packet_2 = APRSPacket.from_frame(
"W3EAX-13>APRS,WIDE1-1,WIDE2-1,qAR,W4TTU:!/:JAe:tn8O /A=046255|!i| /W3EAX,322,0,20'C,nearspace.umd.edu",
packet_time=datetime(2019, 2, 3, 14, 38, 23),
)
packet_3 = APRSPacket.from_frame(
"W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,"
'nearspace.umd.edu',
packet_time=datetime(2019, 2, 3, 14, 39, 28),
)

track = APRSTrack('W3EAX-13', [packet_1, packet_2, packet_3])

assert track[0] == packet_1
assert track[0] is packet_1
assert track[datetime(2019, 2, 3, 14, 36, 16)] == packet_1
assert track['2019-02-03 14:36:16'] == packet_1

assert track['2019-02-03 14:38:00'] == packet_2

with pytest.raises(IndexError):
track['2019-02-03 14:30:00']

0 comments on commit 79c0287

Please sign in to comment.