-
Determine if there is a zero angle before searching. Is there a heuristic that can determine whether zero is reachable? E.g., max range is typically achieved about 40 degrees.
-
MaxIterations for the proportional iterative search should probably be no more than 10. Especially if, instead of starting from elevation of zero, we take a better initial guess. For example, we could use the vacuum angle to zero as the starting point:
def calculate_drag_free_launch_angles_in_degrees(
distance: float, height: float, velocity: float, g: float = EARTH_GRAVITY_CONSTANT
) -> Optional[Tuple[float, float]]:
"""
Calculate both possible launch angles for hitting a target.
Args:
distance (float): Horizontal distance to target in meters
height (float): Vertical height difference to target in meters
velocity (float): Initial launch velocity in m/s
Returns:
Optional[Tuple[float, float]]: Tuple of (low angle, high angle) in degrees,
or None if target is unreachable
"""
# Check if target is reachable with given velocity
velocity_squared = velocity**2
discriminant = velocity_squared**2 - g * (
g * distance**2 + 2 * height * velocity_squared
)
if discriminant < 0:
return None
# Calculate the two possible angles using quadratic formula
term1 = velocity_squared
term2 = math.sqrt(discriminant)
term3 = g * distance
angle1 = math.atan((term1 - term2) / term3)
angle2 = math.atan((term1 + term2) / term3)
# Return angles in ascending order (low, high)
return (math.degrees(min(angle1, angle2)), math.degrees(max(angle1, angle2)))
- When first iterative search fails, an "integrative" correction can work. From Serhiy:
summary_error = 0
summary_error_corrections = 0
history = []
while True:
while zero_finding_error > _cZeroFindingAccuracy and iterations_count < _cMaxIterations:
# Check height of trajectory at the zero distance (using current self.barrel_elevation)
try:
t = self._integrate(shot_info, zero_distance, zero_distance, TrajFlag.NONE)[0]
except RangeError as e:
t = e.incomplete_trajectory[0]
height = t.height >> Distance.Foot
height_diff = height - height_at_zero
zero_finding_error = math.fabs(height_diff)
summary_error+=height_diff
if zero_finding_error > _cZeroFindingAccuracy:
# Adjust barrel elevation to close height at zero distance
self.barrel_elevation -= (height_diff) / zero_distance
history.append((self.barrel_elevation, height_diff))
else: # last barrel_elevation hit zero!
return Angular.Radian(self.barrel_elevation)
iterations_count += 1
if math.fabs(summary_error)>0 and summary_error_corrections<3:
print(f'Got to integral correction {summary_error=}')
print(f'Iterations: {len(history)=} minimum deviation by height {min(history, key=lambda x: abs(x[1]))=} {history=}')
self.barrel_elevation-=(summary_error/iterations_count)/(zero_distance)
summary_error = 0
iterations_count=0
summary_error_corrections += 1
else:
if zero_finding_error > _cZeroFindingAccuracy:
print(f'The search has not converged Iteration Count {len(history)} '
f'minimum deviation by height {min(history, key=lambda x: abs(x[1]))=} {history=}')
# ZeroFindingError contains an instance of last barrel elevation; so caller can check how close zero is
raise ZeroFindingError(zero_finding_error, iterations_count, Angular.Radian(self.barrel_elevation))
- When that fails, a bracket search (e.g., https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brenth.html).
Determine if there is a zero angle before searching. Is there a heuristic that can determine whether zero is reachable? E.g., max range is typically achieved about 40 degrees.
MaxIterationsfor the proportional iterative search should probably be no more than 10. Especially if, instead of starting from elevation of zero, we take a better initial guess. For example, we could use the vacuum angle to zero as the starting point: