Skip to content

Commit

Permalink
Refactored angle terms and added "look angle" calculation (#17)
Browse files Browse the repository at this point in the history
* Renamed sight_angle -> zero_angle, shot_angle -> relative_angle
Added comments on Shot attributes
Fixed unit tests

* Added look_angle parameter for uphill/downhill shots.

Demonstration code:
    look_angle = Angular.Degree(30)
    weapon = Weapon(Distance.Inch(2), 100, look_angle, 12)
    calc = Calculator(weapon, ammo, zero_atmo)
    calc.update_elevation()
    print(f'Zero distance: {weapon.zero_distance >> Distance.Yard:.0f} yards; look-angle to zero: {look_angle >> Angular.Degree:.0f}°'
        f'\n\t==> horizontal distance to zero = {math.cos(look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Yard):.2f} yards; vertical distance to zero = {math.sin(look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Centimeter):.0f}cm'
        f'\nZero elevation: {calc.elevation >> Angular.Degree:.2f}°')
  • Loading branch information
dbookstaber authored Oct 12, 2023
1 parent 6b63a1f commit a398afc
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 41 deletions.
23 changes: 16 additions & 7 deletions py_ballisticcalc/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,26 @@ def __post_init__(self):

@dataclass
class Shot(TypedUnits):
"""Stores shot parameters for the trajectory calculation"""
"""
Stores shot parameters for the trajectory calculation
:param max_range: Downrange distance to stop computing trajectory
:param step: Distance between each TrajectoryData row to record
:param zero_angle: The angle between the barrel and horizontal when zeroed
:param relative_angle: Elevation adjustment added to zero_angle for a particular shot
:param cant_angle: Rotation of gun around barrel axis, relative to position when zeroed.
(Only relevant when Weapon.sight_height != 0)
"""
max_range: [float, Distance] = field(default_factory=lambda: Set.Units.distance)
step: [float, Distance] = field(default_factory=lambda: Set.Units.distance)
shot_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular)
zero_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular)
relative_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular)
cant_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular)
sight_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular)

def __post_init__(self):
if not self.shot_angle:
self.shot_angle = 0
if not self.relative_angle:
self.relative_angle = 0
if not self.cant_angle:
self.cant_angle = 0
if not self.sight_angle:
self.sight_angle = 0
if not self.zero_angle:
self.zero_angle = 0
8 changes: 4 additions & 4 deletions py_ballisticcalc/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def cdm(self):
def update_elevation(self):
"""Recalculates barrel elevation for weapon and zero atmo"""
self._calc = TrajectoryCalc(self.ammo)
self._elevation = self._calc.sight_angle(self.weapon, self.zero_atmo)
self._elevation = self._calc.zero_angle(self.weapon, self.zero_atmo)

def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list:
"""Calculates trajectory with current conditions
Expand All @@ -48,9 +48,9 @@ def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list:
:return: trajectory table
"""
self._calc = TrajectoryCalc(self.ammo)
if not self._elevation and not shot.sight_angle:
if not self._elevation and not shot.zero_angle:
self.update_elevation()
shot.sight_angle = self._elevation
shot.zero_angle = self._elevation
data = self._calc.trajectory(self.weapon, current_atmo, shot, winds)
return data

Expand Down Expand Up @@ -120,7 +120,7 @@ def show_plot(self, shot, winds):
[df['drop'].min(), p.drop >> Set.Units.drop], linestyle='--', label='mach')
ax.text(p.distance >> Set.Units.distance, df['drop'].min(), " Mach")

sh = self.weapon.sight_height >> Set.Units.drop
#sh = self.weapon.sight_height >> Set.Units.drop

# # scope line
x_values = [0, df.distance.max()] # Adjust these as needed
Expand Down
18 changes: 16 additions & 2 deletions py_ballisticcalc/munition.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,35 @@
# pylint: disable=import-error,no-name-in-module
from .drag_model import DragModel
from .settings import Settings as Set
from .unit import TypedUnits, Velocity, Temperature, is_unit, Distance
from .unit import TypedUnits, Velocity, Temperature, is_unit, Distance, Angular

__all__ = ('Weapon', 'Ammo')


@dataclass
class Weapon(TypedUnits):
"""Creates Weapon properties"""
"""
:param zero_distance: Sight-line distance to "zero," which is point we want to hit.
This is the distance that a rangefinder would return with no ballistic adjustment.
NB: Some rangefinders offer an adjusted distance based on inclinometer measurement.
However, without a complete ballistic model these can only approximate the effects
on ballistic trajectory of shooting uphill or downhill. Therefore:
For maximum accuracy, use the raw sight distance and look angle as inputs here.
:param zero_look_angle: If = 0 then the target zero is horizontal with the gun at zero_distance.
If zero_look_angle != 0 then the target zero is at a different height than the gun, and
the horizontal distance to target = cos(look_angle) * zero_distance, and
the vertical distance to target from horizontal = sin(look_angle) * zero_distance.
"""
sight_height: [float, Distance] = field(default_factory=lambda: Set.Units.sight_height)
zero_distance: [float, Distance] = field(default_factory=lambda: Set.Units.distance)
zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular)
twist: [float, Distance] = field(default_factory=lambda: Set.Units.twist)

def __post_init__(self):
if not self.twist:
self.twist = 0
if not self.zero_look_angle:
self.zero_look_angle = 0


@dataclass
Expand Down
26 changes: 13 additions & 13 deletions py_ballisticcalc/trajectory_calc.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -119,25 +119,26 @@ cdef class TrajectoryCalc:

return step

def sight_angle(self, weapon: Weapon, atmo: Atmo):
return self._sight_angle(self.ammo, weapon, atmo)
def zero_angle(self, weapon: Weapon, atmo: Atmo):
return self._zero_angle(self.ammo, weapon, atmo)

def trajectory(self, weapon: Weapon, atmo: Atmo,
shot_info: Shot, winds: list[Wind],
filter_flags: int = CTrajFlag.RANGE):
return self._trajectory(self.ammo, weapon, atmo, shot_info, winds, filter_flags)

cdef _sight_angle(TrajectoryCalc self, object ammo, object weapon, object atmo):
cdef _zero_angle(TrajectoryCalc self, object ammo, object weapon, object atmo):
cdef:
double calc_step = self.get_calc_step(weapon.zero_distance.units(10) >> Distance.Foot)
double zero_distance = weapon.zero_distance >> Distance.Foot
double zero_distance = cos(weapon.zero_look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Foot)
double height_at_zero = sin(weapon.zero_look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Foot)
double maximum_range = zero_distance + calc_step
double sight_height = weapon.sight_height >> Distance.Foot
double mach = atmo.mach >> Velocity.FPS
double density_factor = atmo.density_factor()
double muzzle_velocity = ammo.mv >> Velocity.FPS
double barrel_azimuth = 0.0
double barrel_elevation = 0.0
double barrel_elevation = atan(height_at_zero / zero_distance)
int iterations_count = 0
double zero_finding_error = cZeroFindingAccuracy * 2
Vector gravity_vector = Vector(.0, cGravityConstant, .0)
Expand Down Expand Up @@ -171,8 +172,8 @@ cdef class TrajectoryCalc:
time += delta_range_vector.magnitude() / velocity

if fabs(range_vector.x - zero_distance) < 0.5 * calc_step:
zero_finding_error = fabs(range_vector.y)
barrel_elevation -= range_vector.y / range_vector.x
zero_finding_error = fabs(range_vector.y - height_at_zero)
barrel_elevation -= (range_vector.y - height_at_zero) / range_vector.x
break

iterations_count += 1
Expand All @@ -184,7 +185,6 @@ cdef class TrajectoryCalc:
cdef:
double density_factor, mach
double time, velocity, windage, delta_time, drag
double windage_adjustment, drop_adjustment, trajectory_angle

double twist = weapon.twist >> Distance.Inch
double length = ammo.length >> Distance.Inch
Expand All @@ -194,7 +194,7 @@ cdef class TrajectoryCalc:
double step = shot_info.step >> Distance.Foot
double calc_step = self.get_calc_step(step)

double maximum_range = (shot_info.max_range >> Distance.Foot) + step
double maximum_range = (shot_info.max_range >> Distance.Foot) + 1

int ranges_length = int(maximum_range / step) + 1
int len_winds = len(winds)
Expand All @@ -203,8 +203,8 @@ cdef class TrajectoryCalc:
double stability_coefficient = 1.0
double next_wind_range = 1e7

double barrel_elevation = (shot_info.sight_angle >> Angular.Radian) + (
shot_info.shot_angle >> Angular.Radian)
double barrel_elevation = (shot_info.zero_angle >> Angular.Radian) + (
shot_info.relative_angle >> Angular.Radian)
double alt0 = atmo.altitude >> Distance.Foot
double sight_height = weapon.sight_height >> Distance.Foot

Expand Down Expand Up @@ -354,8 +354,8 @@ cdef double calculate_stability_coefficient(object ammo, object rifle, object at

cdef Vector wind_to_vector(object shot, object wind):
cdef:
double sight_cosine = cos(shot.sight_angle >> Angular.Radian)
double sight_sine = sin(shot.sight_angle >> Angular.Radian)
double sight_cosine = cos(shot.zero_angle >> Angular.Radian)
double sight_sine = sin(shot.zero_angle >> Angular.Radian)
double cant_cosine = cos(shot.cant_angle >> Angular.Radian)
double cant_sine = sin(shot.cant_angle >> Angular.Radian)
double range_velocity = (wind.velocity >> Velocity.FPS) * cos(wind.direction_from >> Angular.Radian)
Expand Down
30 changes: 15 additions & 15 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def print_output(data, at_elevation):
with self.subTest(zero=reference_distance, sh=sh):
try:
calc.update_elevation()
shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation)
shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation)
zero_crossing_points = calc.zero_given_elevation(shot)
print_output(zero_crossing_points, calc.elevation)
except ArithmeticError as err:
Expand All @@ -103,7 +103,7 @@ def print_output(data, at_elevation):

with self.subTest(zero=reference_distance, sh=sh, elev=0):
try:
shot = Shot(1000, Distance.Foot(0.2), sight_angle=0)
shot = Shot(1000, Distance.Foot(0.2), zero_angle=0)
zero_crossing_points = calc.zero_given_elevation(shot)
print_output(zero_crossing_points, 0)
except ArithmeticError as err:
Expand All @@ -116,7 +116,7 @@ def test_danger_space(self):
weapon = Weapon(Distance.Inch(4), zero_distance, 11.24)
calc = Calculator(weapon, self.ammo, self.atmosphere)
calc.update_elevation()
shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation)
shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation)
zero_given_elevation = calc.zero_given_elevation(shot)
if len(zero_given_elevation) > 0:
zero = [p for p in zero_given_elevation if abs(
Expand All @@ -138,10 +138,10 @@ def test_zero1(self):
atmosphere = Atmo.icao()
calc = TrajectoryCalc(ammo)

sight_angle = calc.sight_angle(weapon, atmosphere)
zero_angle = calc.zero_angle(weapon, atmosphere)

self.assertAlmostEqual(sight_angle >> Angular.Radian, 0.001651, 6,
f'TestZero1 failed {sight_angle >> Angular.Radian:.10f}')
self.assertAlmostEqual(zero_angle >> Angular.Radian, 0.001651, 6,
f'TestZero1 failed {zero_angle >> Angular.Radian:.10f}')

def test_zero2(self):
dm = DragModel(0.223, TableG7, 69, 0.223)
Expand All @@ -150,10 +150,10 @@ def test_zero2(self):
atmosphere = Atmo.icao()
calc = TrajectoryCalc(ammo)

sight_angle = calc.sight_angle(weapon, atmosphere)
zero_angle = calc.zero_angle(weapon, atmosphere)

self.assertAlmostEqual(sight_angle >> Angular.Radian, 0.001228, 6,
f'TestZero2 failed {sight_angle >> Angular.Radian:.10f}')
self.assertAlmostEqual(zero_angle >> Angular.Radian, 0.001228, 6,
f'TestZero2 failed {zero_angle >> Angular.Radian:.10f}')

def custom_assert_equal(self, a, b, accuracy, name):
with self.subTest(name=name):
Expand Down Expand Up @@ -197,10 +197,10 @@ def test_path_g1(self):
ammo = Ammo(dm, 1.282, Velocity(2750, Velocity.FPS))
weapon = Weapon(Distance(2, Distance.Inch), Distance(100, Distance.Yard))
atmosphere = Atmo.icao()
shot_info = Shot(1000, 100, sight_angle=Angular(0.001228, Angular.Radian))
shot_info = Shot(1000, 100, zero_angle=Angular(0.001228, Angular.Radian))
wind = [Wind(Velocity(5, Velocity.MPH), Angular(10.5, Angular.OClock))]
calc = TrajectoryCalc(ammo)
data = calc.trajectory(weapon, atmosphere, shot_info, wind)
data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value)

self.custom_assert_equal(len(data), 11, 0.1, "Length")

Expand All @@ -222,12 +222,12 @@ def test_path_g7(self):
atmosphere = Atmo.icao()
shot_info = Shot(Distance.Yard(1000),
Distance.Yard(100),
sight_angle=Angular.MOA(4.221)
zero_angle=Angular.MOA(4.221)
)
wind = [Wind(Velocity(5, Velocity.MPH), -45)]

calc = TrajectoryCalc(ammo)
data = calc.trajectory(weapon, atmosphere, shot_info, wind)
data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value)

self.custom_assert_equal(len(data), 11, 0.1, "Length")

Expand All @@ -251,7 +251,7 @@ def setUp(self) -> None:
self.atmo = Atmo.icao()
self.shot = Shot(Distance.Yard(1000),
Distance.Yard(100),
sight_angle=Angular.MOA(4.221)
zero_angle=Angular.MOA(4.221)
)
self.wind = [Wind(Velocity(5, Velocity.MPH), -45)]

Expand All @@ -261,7 +261,7 @@ def test__init__(self):
self.assertTrue(self.calc)

def test_elevation_performance(self):
self.calc.sight_angle(self.weapon, self.atmo)
self.calc.zero_angle(self.weapon, self.atmo)

def test_path_performance(self):
d = self.calc.trajectory(self.weapon, self.atmo, self.shot, self.wind)
Expand Down

0 comments on commit a398afc

Please sign in to comment.