Improve speed estimations.

Reasonable ground speed depends a lot on altitude, so plumb that
information through to the speed estimator.

Also adds calculations for ground speed based on desired mach. I don't
know if DCS is using the same formulas, but we should at least be
pretty close.
This commit is contained in:
Dan Albert 2020-10-15 23:48:42 -07:00
parent 5a027c552e
commit 2fa3b26119
5 changed files with 111 additions and 39 deletions

View File

@ -788,6 +788,8 @@ class AircraftConflictGenerator:
self.clear_parking_slots() self.clear_parking_slots()
for package in ato.packages: for package in ato.packages:
if not package.flights:
continue
timing = PackageWaypointTiming.for_package(package) timing = PackageWaypointTiming.for_package(package)
for flight in package.flights: for flight in package.flights:
culled = self.game.position_culled(flight.from_cp.position) culled = self.game.position_culled(flight.from_cp.position)
@ -1130,7 +1132,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
pattern=OrbitAction.OrbitPattern.Circle pattern=OrbitAction.OrbitPattern.Circle
)) ))
loiter.stop_after_time( loiter.stop_after_time(
self.timing.push_time(self.flight, waypoint.position)) self.timing.push_time(self.flight, self.waypoint))
waypoint.add_task(loiter) waypoint.add_task(loiter)
return waypoint return waypoint

View File

@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Dict, Optional from typing import Dict, Iterable, Optional
from game import db from game import db
from dcs.unittype import UnitType from dcs.unittype import UnitType
@ -151,6 +151,14 @@ class Flight:
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
+ " (" + str(len(self.points)) + " wpt)" + " (" + str(len(self.points)) + " wpt)"
def waypoint_with_type(
self,
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
for waypoint in self.points:
if waypoint.waypoint_type in types:
return waypoint
return None
# Test # Test
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterable, Optional from typing import Iterable, Optional
@ -19,23 +20,73 @@ from gen.flights.flight import (
CAP_DURATION = 30 # Minutes CAP_DURATION = 30 # Minutes
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP) CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
}
IP_TYPES = {
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK,
}
class GroundSpeed: class GroundSpeed:
@classmethod
def for_package(cls, package: Package) -> int:
speeds = []
for flight in package.flights:
speeds.append(cls.for_flight(flight))
return min(speeds) # knots
@staticmethod @staticmethod
def for_flight(_flight: Flight) -> int: def mission_speed(package: Package) -> int:
speeds = set()
for flight in package.flights:
waypoint = flight.waypoint_with_type(IP_TYPES)
if waypoint is None:
logging.error(f"Could not find ingress point for {flight}")
continue
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
return min(speeds)
@classmethod
def for_flight(cls, _flight: Flight, altitude: int) -> int:
# TODO: Gather data so this is useful. # TODO: Gather data so this is useful.
# TODO: Expose both a cruise speed and target speed. # TODO: Expose both a cruise speed and target speed.
# The cruise speed can be used for ascent, hold, join, and RTB to save # The cruise speed can be used for ascent, hold, join, and RTB to save
# on fuel, but mission speed will be fast enough to keep the flight # on fuel, but mission speed will be fast enough to keep the flight
# safer. # safer.
return 400 # knots return int(cls.from_mach(0.8, altitude)) # knots
@staticmethod
def from_mach(mach: float, altitude: int) -> float:
"""Returns the ground speed in knots for the given mach and altitude.
Args:
mach: The mach number to convert to ground speed.
altitude: The altitude in feet.
Returns:
The ground speed corresponding to the given altitude and mach number
in knots.
"""
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
if altitude <= 36152:
temperature_f = 59 - 0.00356 * altitude
else:
# There's another formula for altitudes over 82k feet, but we better
# not be planning waypoints that high...
temperature_f = -70
temperature_k = (temperature_f + 459.67) * (5 / 9)
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
# Dependent on temperature, but varies very little (+/-0.001)
# between -40F and 180F.
heat_capacity_ratio = 1.4
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
gas_constant = 286 # m^2/s^2/K
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
# c_sound is in m/s, convert to knots.
return (c_sound * 1.944) * mach
class TravelTime: class TravelTime:
@ -97,13 +148,7 @@ class TotEstimator:
The earliest possible TOT for the given flight in seconds. Returns 0 The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found. if an ingress point cannot be found.
""" """
stop_types = { time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES)
FlightWaypointType.PATROL_TRACK,
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
}
time_to_ingress = self.estimate_waypoints_to_target(flight, stop_types)
if time_to_ingress is None: if time_to_ingress is None:
logging.warning( logging.warning(
f"Found no ingress types. Cannot estimate TOT for {flight}") f"Found no ingress types. Cannot estimate TOT for {flight}")
@ -119,7 +164,7 @@ class TotEstimator:
assert self.package.waypoints is not None assert self.package.waypoints is not None
time_to_target = TravelTime.between_points( time_to_target = TravelTime.between_points(
self.package.waypoints.ingress, self.package.target.position, self.package.waypoints.ingress, self.package.target.position,
GroundSpeed.for_package(self.package)) GroundSpeed.mission_speed(self.package))
return sum([ return sum([
self.estimate_startup(flight), self.estimate_startup(flight),
self.estimate_ground_ops(flight), self.estimate_ground_ops(flight),
@ -146,30 +191,38 @@ class TotEstimator:
self, flight: Flight, self, flight: Flight,
stop_types: Iterable[FlightWaypointType]) -> Optional[int]: stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
total = 0 total = 0
# TODO: This is AGL. We want MSL.
previous_altitude = 0
previous_position = flight.from_cp.position previous_position = flight.from_cp.position
for waypoint in flight.points: for waypoint in flight.points:
position = Point(waypoint.x, waypoint.y) position = Point(waypoint.x, waypoint.y)
total += TravelTime.between_points( total += TravelTime.between_points(
previous_position, position, previous_position, position,
self.speed_to_waypoint(flight, waypoint) self.speed_to_waypoint(flight, waypoint, previous_altitude)
) )
previous_position = position previous_position = position
previous_altitude = waypoint.alt
if waypoint.waypoint_type in stop_types: if waypoint.waypoint_type in stop_types:
return total return total
return None return None
def speed_to_waypoint(self, flight: Flight, def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
waypoint: FlightWaypoint) -> int: from_altitude: int) -> int:
# TODO: Adjust if AGL.
# We don't have an exact heightmap, but we should probably be performing
# *some* adjustment for NTTR since the minimum altitude of the map is
# near 2000 ft MSL.
alt_for_speed = min(from_altitude, waypoint.alt)
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN) pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT: if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
# Flights that start airborne already have some altitude and a good # Flights that start airborne already have some altitude and a good
# amount of speed. # amount of speed.
factor = 1.0 if flight.start_type == "In Flight" else 0.5 factor = 1.0 if flight.start_type == "In Flight" else 0.5
return int(GroundSpeed.for_flight(flight) * factor) return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
elif waypoint.waypoint_type in pre_join: elif waypoint.waypoint_type in pre_join:
return GroundSpeed.for_flight(flight) return GroundSpeed.for_flight(flight, alt_for_speed)
return GroundSpeed.for_package(self.package) return GroundSpeed.mission_speed(self.package)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -209,12 +262,12 @@ class PackageWaypointTiming:
else: else:
return self.egress return self.egress
def push_time(self, flight: Flight, hold_point: Point) -> int: def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
assert self.package.waypoints is not None assert self.package.waypoints is not None
return self.join - TravelTime.between_points( return self.join - TravelTime.between_points(
hold_point, Point(hold_point.x, hold_point.y),
self.package.waypoints.join, self.package.waypoints.join,
GroundSpeed.for_flight(flight) GroundSpeed.for_flight(flight, hold_point.alt)
) )
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
@ -224,15 +277,9 @@ class PackageWaypointTiming:
FlightWaypointType.TARGET_SHIP, FlightWaypointType.TARGET_SHIP,
) )
ingress_types = (
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
)
if waypoint.waypoint_type == FlightWaypointType.JOIN: if waypoint.waypoint_type == FlightWaypointType.JOIN:
return self.join return self.join
elif waypoint.waypoint_type in ingress_types: elif waypoint.waypoint_type in INGRESS_TYPES:
return self.ingress return self.ingress
elif waypoint.waypoint_type in target_types: elif waypoint.waypoint_type in target_types:
return self.target return self.target
@ -247,7 +294,7 @@ class PackageWaypointTiming:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint, def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
flight: Flight) -> Optional[int]: flight: Flight) -> Optional[int]:
if waypoint.waypoint_type == FlightWaypointType.LOITER: if waypoint.waypoint_type == FlightWaypointType.LOITER:
return self.push_time(flight, Point(waypoint.x, waypoint.y)) return self.push_time(flight, waypoint)
elif waypoint.waypoint_type == FlightWaypointType.PATROL: elif waypoint.waypoint_type == FlightWaypointType.PATROL:
return self.race_track_end return self.race_track_end
return None return None
@ -256,7 +303,17 @@ class PackageWaypointTiming:
def for_package(cls, package: Package) -> PackageWaypointTiming: def for_package(cls, package: Package) -> PackageWaypointTiming:
assert package.waypoints is not None assert package.waypoints is not None
group_ground_speed = GroundSpeed.for_package(package) # TODO: Plan similar altitudes for the in-country leg of the mission.
# Waypoint altitudes for a given flight *shouldn't* differ too much
# between the join and split points, so we don't need speeds for each
# leg individually since they should all be fairly similar. This doesn't
# hold too well right now since nothing is stopping each waypoint from
# jumping 20k feet each time, but that's a huge waste of energy we
# should be avoiding anyway.
if not package.flights:
raise ValueError("Cannot plan TOT for package with no flights")
group_ground_speed = GroundSpeed.mission_speed(package)
ingress = package.time_over_target - TravelTime.between_points( ingress = package.time_over_target - TravelTime.between_points(
package.waypoints.ingress, package.waypoints.ingress,

View File

@ -122,6 +122,8 @@ class QTopPanel(QFrame):
def negative_start_packages(self) -> List[Package]: def negative_start_packages(self) -> List[Package]:
packages = [] packages = []
for package in self.game_model.ato_model.ato.packages: for package in self.game_model.ato_model.ato.packages:
if not package.flights:
continue
estimator = TotEstimator(package) estimator = TotEstimator(package)
for flight in package.flights: for flight in package.flights:
if estimator.mission_start_time(flight) < 0: if estimator.mission_start_time(flight) < 0:

View File

@ -136,6 +136,9 @@ class QPackageDialog(QDialog):
self.package_model.update_tot(seconds) self.package_model.update_tot(seconds)
def reset_tot(self) -> None: def reset_tot(self) -> None:
if not list(self.package_model.flights):
self.package_model.update_tot(0)
else:
self.package_model.update_tot( self.package_model.update_tot(
TotEstimator(self.package_model.package).earliest_tot()) TotEstimator(self.package_model.package).earliest_tot())
self.tot_spinner.setTime(self.tot_qtime()) self.tot_spinner.setTime(self.tot_qtime())