Skip to content

Commit

Permalink
Incomplete shot handling (#154)
Browse files Browse the repository at this point in the history
* Addition of recording of the trajectory interruption point for RangeError

Fix of test_find_index_for_distance in test_helpers.py

* Changed TestDangerSpace to pass.

* Added test for checking the addition of last point to HitResult.trajectory in case of RangeError

* Refactored test and added test_vertical_shot

* Added changes as well to cython version

* Changed to cythonic version of call of create_trajectory_row

* Added hash method to AbstractDimension. This allows using TrajectoryData in sets, which was not possible before.

Removed checked for equality of the last point (as it will be different due to step being advanced in computation loop.

Added two tests which are checking for absense of duplicated points in trajectory (One test takes 14 seconds, as it performs 22 shots)

* Removed check for time point being different from last point time, as it cannot happen for now as well in cython code

* Fix for #150

Removed ranges_length field and should_break method from TrajectoryDataFilter to allow integration cycle to complete to end. Added addition of final point at the end (unless point with same time exist).

Corrected tests, which had dependency on length of trajectory.

Performed same changes in the trajectory_calc.pyx

* Change handling of end_point inclusion through changing logic of range_length computation.
Current logic ensures that last point of trajectory lays before the max_range_distance.

Restored old tests test_issues.py and test_trajectory.py
Changed TestDangerSpace to Davids version of fix

Changed cython version of trajectory_calc to match updated version

* Fix of cython errors

* Removed commented out print statement and dependency on numpy call

* _TrajectoryDataFilter patch

* Renamed step to record_step, added comment

* Removed ranges_length and current_item members in _TrajectoryDataFilter, and method should_break().

* bump to v2.1.0b5

---------

Co-authored-by: o-murphy <[email protected]>
Co-authored-by: Dmytro Yaroshenko <[email protected]>
  • Loading branch information
3 people authored Feb 4, 2025
1 parent f254f0d commit 3d4f616
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 88 deletions.
37 changes: 18 additions & 19 deletions py_ballisticcalc.exts/py_ballisticcalc_exts/trajectory_calc.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,16 @@ __all__ = (
cdef class _TrajectoryDataFilter:
cdef:
int filter, current_flag, seen_zero
int current_item, ranges_length
double previous_mach, previous_time, next_range_distance, time_step, look_angle
int current_item
double previous_mach, previous_time, next_range_distance, range_step, time_step, look_angle

def __cinit__(_TrajectoryDataFilter self,
int filter_flags, int ranges_length, double time_step = 0.0) -> None:
int filter_flags, double range_step, double time_step = 0.0) -> None:
self.filter = filter_flags
self.current_flag = CTrajFlag.NONE
self.seen_zero = CTrajFlag.NONE
self.time_step = time_step
self.current_item = 0
self.ranges_length = ranges_length
self.range_step = range_step
self.previous_mach = 0.0
self.previous_time = 0.0
self.next_range_distance = 0.0
Expand All @@ -82,26 +81,21 @@ cdef class _TrajectoryDataFilter:
CVector range_vector,
double velocity,
double mach,
double step,
double time,
):
self.check_zero_crossing(range_vector)
self.check_mach_crossing(velocity, mach)
if self.check_next_range(range_vector.x, step):
if self.check_next_range(range_vector.x, self.range_step):
self.previous_time = time
elif self.time_step > 0:
self.check_next_time(time)
return (self.current_flag & self.filter) != 0

cdef bint should_break(_TrajectoryDataFilter self):
return self.current_item == self.ranges_length

cdef bint check_next_range(_TrajectoryDataFilter self, double next_range, double step):
# Next range check
if next_range >= self.next_range_distance:
self.current_flag |= CTrajFlag.RANGE
self.next_range_distance += step
self.current_item += 1
return True
return False

Expand Down Expand Up @@ -314,7 +308,7 @@ cdef class TrajectoryCalc:


cdef list[object] _integrate(TrajectoryCalc self,
double maximum_range, double step, int filter_flags, double time_step = 0.0):
double maximum_range, double record_step, int filter_flags, double time_step = 0.0):
cdef:
double velocity, delta_time
double density_factor = .0
Expand Down Expand Up @@ -353,16 +347,17 @@ cdef class TrajectoryCalc:
velocity_vector = mul_c(&_dir_vector, velocity)
# endregion

min_step = min(calc_step, record_step)
# With non-zero look_angle, rounding can suggest multiple adjacent zero-crossings
data_filter = _TrajectoryDataFilter(filter_flags=filter_flags,
ranges_length=<int> ((maximum_range / step) + 1),
range_step=record_step,
time_step=time_step)
data_filter.setup_seen_zero(range_vector.y, self.__shot.barrel_elevation, self.__shot.look_angle)

#region Trajectory Loop
warnings.simplefilter("once") # used to avoid multiple warnings in a loop
cdef int it = 0
while range_vector.x <= maximum_range + calc_step:
while range_vector.x <= maximum_range + min_step:
it += 1
data_filter.current_flag = CTrajFlag.NONE

Expand All @@ -377,15 +372,13 @@ cdef class TrajectoryCalc:
if filter_flags:

# Record TrajectoryData row
# if data_filter.should_record(range_vector, velocity, mach, step, self.look_angle, time):
if data_filter.should_record(range_vector, velocity, mach, step, time):
# if data_filter.should_record(range_vector, velocity, mach, record_step, self.look_angle, time):
if data_filter.should_record(range_vector, velocity, mach, time):
ranges.append(create_trajectory_row(
time, range_vector, velocity_vector,
velocity, mach, cy_spin_drift(&self.__shot, time), self.__shot.look_angle,
density_factor, drag, self.__shot.weight, data_filter.current_flag
))
if data_filter.should_break():
break

#region Ballistic calculation step
# use just cdef methods to
Expand All @@ -412,6 +405,12 @@ cdef class TrajectoryCalc:
or range_vector.y < _cMaximumDrop
or self.__shot.alt0 + range_vector.y < _cMinimumAltitude
):
ranges.append(create_trajectory_row(
time, range_vector, velocity_vector,
velocity, mach, cy_spin_drift(&self.__shot, time), self.__shot.look_angle,
density_factor, drag, self.__shot.weight, data_filter.current_flag
))

if velocity < _cMinimumVelocity:
reason = RangeError.MinimumVelocityReached
elif range_vector.y < _cMaximumDrop:
Expand All @@ -423,7 +422,7 @@ cdef class TrajectoryCalc:

#endregion
# If filter_flags == 0 then all we want is the ending value
if not filter_flags:
if len(ranges)==0:
ranges.append(create_trajectory_row(
time, range_vector, velocity_vector,
velocity, mach, cy_spin_drift(&self.__shot, time), self.__shot.look_angle,
Expand Down
2 changes: 1 addition & 1 deletion py_ballisticcalc.exts/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "py_ballisticcalc.exts"
version = "2.1.0b4"
version = "2.1.0b5"

authors = [
{ name="o-murphy", email="[email protected]" },
Expand Down
50 changes: 25 additions & 25 deletions py_ballisticcalc/trajectory_calc/_trajectory_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,19 @@ class _TrajectoryDataFilter:
filter: Union[TrajFlag, int]
current_flag: Union[TrajFlag, int]
seen_zero: Union[TrajFlag, int]
current_item: int
ranges_length: int
time_step: float
range_step: float
previous_mach: float
next_range_distance: float
look_angle: float

def __init__(self, filter_flags: Union[TrajFlag, int],
ranges_length: int, time_step: float = 0.0):
def __init__(self, filter_flags: Union[TrajFlag, int], range_step: float, time_step: float = 0.0):
"""If a time_step is indicated, then we will record a row at least that often in the trajectory"""
self.filter: Union[TrajFlag, int] = filter_flags
self.current_flag: Union[TrajFlag, int] = TrajFlag.NONE
self.seen_zero: Union[TrajFlag, int] = TrajFlag.NONE
self.time_step = time_step
self.current_item: int = 0
self.ranges_length: int = ranges_length
self.time_step: float = time_step
self.range_step: float = range_step
self.previous_mach: float = 0.0
self.previous_time: float = 0.0
self.next_range_distance: float = 0.0
Expand All @@ -86,19 +84,15 @@ def should_record(self,
range_vector: Vector,
velocity: float,
mach: float,
step: float,
time: float) -> bool:
self.check_zero_crossing(range_vector)
self.check_mach_crossing(velocity, mach)
if self.check_next_range(range_vector.x, step):
if self.check_next_range(range_vector.x, self.range_step):
self.previous_time = time
elif self.time_step > 0:
self.check_next_time(time)
return bool(self.current_flag & self.filter)

def should_break(self) -> bool:
return self.current_item == self.ranges_length

def check_next_range(self, next_range: float, step: float) -> bool:
"""
If we passed the next_range point, set the RANGE flag and update the next_range_distance
Expand All @@ -107,7 +101,6 @@ def check_next_range(self, next_range: float, step: float) -> bool:
if next_range >= self.next_range_distance:
self.current_flag |= TrajFlag.RANGE
self.next_range_distance += step
self.current_item += 1
return True
return False

Expand Down Expand Up @@ -221,7 +214,7 @@ def trajectory(self, shot_info: Shot, max_range: Distance, dist_step: Distance,
filter_flags = TrajFlag.RANGE

if extra_data:
dist_step = Distance.Foot(self._config.chart_resolution)
# dist_step = Distance.Foot(self._config.chart_resolution)
filter_flags = TrajFlag.ALL

self._init_trajectory(shot_info)
Expand Down Expand Up @@ -286,11 +279,11 @@ def zero_angle(self, shot_info: Shot, distance: Distance) -> Angular:
raise ZeroFindingError(zero_finding_error, iterations_count, Angular.Radian(self.barrel_elevation))
return Angular.Radian(self.barrel_elevation)

def _integrate(self, shot_info: Shot, maximum_range: float, step: float,
def _integrate(self, shot_info: Shot, maximum_range: float, record_step: float,
filter_flags: Union[TrajFlag, int], time_step: float = 0.0) -> List[TrajectoryData]:
"""Calculate trajectory for specified shot
:param maximum_range: Feet down range to stop calculation
:param step: Frequency (in feet down range) to record TrajectoryData
:param record_step: Frequency (in feet down range) to record TrajectoryData
:param time_step: If > 0 then record TrajectoryData after this many seconds elapse
since last record, as could happen when trajectory is nearly vertical
and there is too little movement downrange to trigger a record based on range.
Expand Down Expand Up @@ -325,16 +318,17 @@ def _integrate(self, shot_info: Shot, maximum_range: float, step: float,
).mul_by_const(velocity) # type: ignore
# endregion

# min step is used to handle situation, when record step is smaller than calc_step
# in order to prevent range breaking too early
min_step = min(self.calc_step, record_step)
# With non-zero look_angle, rounding can suggest multiple adjacent zero-crossings
data_filter = _TrajectoryDataFilter(filter_flags=filter_flags,
ranges_length=int(maximum_range / step) + 1,
time_step=time_step)
data_filter = _TrajectoryDataFilter(filter_flags=filter_flags, range_step=record_step, time_step=time_step)
data_filter.setup_seen_zero(range_vector.y, self.barrel_elevation, self.look_angle)

# region Trajectory Loop
warnings.simplefilter("once") # used to avoid multiple warnings in a loop
it = 0
while range_vector.x <= maximum_range + self.calc_step:
while range_vector.x <= maximum_range + min_step:
it += 1
data_filter.clear_current_flag()

Expand All @@ -350,14 +344,13 @@ def _integrate(self, shot_info: Shot, maximum_range: float, step: float,
if filter_flags: # require check before call to improve performance

# Record TrajectoryData row
if data_filter.should_record(range_vector, velocity, mach, step, time):
if data_filter.should_record(range_vector, velocity, mach, time):
ranges.append(create_trajectory_row(
time, range_vector, velocity_vector,
velocity, mach, self.spin_drift(time), self.look_angle,
density_factor, drag, self.weight, data_filter.current_flag
))
if data_filter.should_break():
break

# endregion

# region Ballistic calculation step (point-mass)
Expand All @@ -382,6 +375,11 @@ def _integrate(self, shot_info: Shot, maximum_range: float, step: float,
or range_vector.y < _cMaximumDrop
or self.alt0 + range_vector.y < _cMinimumAltitude
):
ranges.append(create_trajectory_row(
time, range_vector, velocity_vector,
velocity, mach, self.spin_drift(time), self.look_angle,
density_factor, drag, self.weight, data_filter.current_flag
))
if velocity < _cMinimumVelocity:
reason = RangeError.MinimumVelocityReached
elif range_vector.y < _cMaximumDrop:
Expand All @@ -393,8 +391,10 @@ def _integrate(self, shot_info: Shot, maximum_range: float, step: float,
# endregion

# endregion
# If filter_flags == 0 then all we want is the ending value
if not filter_flags:
# this check supercedes not filter_flags - as it will be true in any condition
# if not filter_flags:
if len(ranges)==0:
# we need to add end value in case of not filter flag and in case of usual trajectory
ranges.append(create_trajectory_row(
time, range_vector, velocity_vector,
velocity, mach, self.spin_drift(time), self.look_angle,
Expand Down
3 changes: 3 additions & 0 deletions py_ballisticcalc/unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ def __float__(self):
def __eq__(self, other):
return float(self) == other

def __hash__(self):
return hash((self._value, self._defined_units))

def __lt__(self, other):
return float(self) < other

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "py_ballisticcalc"
version = "2.1.0b4"
version = "2.1.0b5"

authors = [
{ name="o-murphy", email="[email protected]" },
Expand Down Expand Up @@ -50,7 +50,7 @@ include = ["py_ballisticcalc*"]


[project.optional-dependencies]
exts = ['py_ballisticcalc.exts==2.1.0b4']
exts = ['py_ballisticcalc.exts==2.1.0b5']
charts = ['matplotlib', 'pandas']
visualize = ['matplotlib', 'pandas']
dev = [
Expand Down
72 changes: 72 additions & 0 deletions tests/fixtures_and_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pytest

from py_ballisticcalc import HitResult, Distance, InterfaceConfigDict, Calculator, DragModel, TableG1, Weight, Ammo, \
Velocity, Weapon, Shot, Angular


def print_out_trajectory_compact(hit_result: HitResult, label="", distance_unit: Distance = Distance.Meter,
top_k: int = 5):
trajectory_length = len(hit_result.trajectory)
if label:
print(f'{label}: Length of trajectory: { trajectory_length=}')
else:
print(f'Length of trajectory: { trajectory_length=}')


trajectory = hit_result.trajectory
if top_k<trajectory_length:
end_start_top_k = top_k
start_end_top_k = trajectory_length - top_k-1
if end_start_top_k<start_end_top_k:
trajectory = trajectory[:end_start_top_k]+trajectory[start_end_top_k:]

for i, p in enumerate(trajectory):
if i<top_k:
index_to_print = i+1
else:
index_to_print = (trajectory_length-top_k)+i-top_k
if i==top_k and i!=trajectory_length-(top_k+1):
print("...")
print(f'{index_to_print}. ({p.distance>>distance_unit}, {p.height>>distance_unit})')


@pytest.fixture()
def zero_height_calc():
config = InterfaceConfigDict(
cMinimumVelocity=0,
cMinimumAltitude=Distance.Meter(0),
cMaximumDrop=Distance.Meter(0),
)
calc = Calculator(_config=config)
return calc


def create_shot():
drag_model = DragModel(
bc=0.759,
drag_table=TableG1,
weight=Weight.Gram(108),
diameter=Distance.Millimeter(23),
length=Distance.Millimeter(108.2),
)
ammo = Ammo(drag_model, Velocity.MPS(930))
gun = Weapon()
shot = Shot(
weapon=gun,
ammo=ammo,
)
return shot


def shot_with_relative_angle_in_degrees(angle_in_degrees: float):
shot = create_shot()
shot.relative_angle = Angular.Degree(angle_in_degrees)
return shot


@pytest.fixture()
def zero_min_velocity_calc():
config = InterfaceConfigDict(
cMinimumVelocity=0,
)
return Calculator(_config=config)
2 changes: 1 addition & 1 deletion tests/test_danger_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def setUp(self) -> None:
shot = Shot(weapon=Weapon(), ammo=ammo, winds=current_winds)
calc = Calculator()
calc.set_weapon_zero(shot, Distance.Foot(300))
self.shot_result = calc.fire(shot, trajectory_range=Distance.Yard(1000), trajectory_step=Distance.Yard(100), extra_data=True)
self.shot_result = calc.fire(shot, trajectory_range=Distance.Yard(1000), trajectory_step=Distance.Yard(1), extra_data=True)

def test_danger_space(self):
danger_space = self.shot_result.danger_space(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def test_find_index_for_distance(one_degree_shot):
)
# for reproducibility
random.seed(42)
random_indices = random.sample(range(len(shot.trajectory)), 100)
random_indices = random.sample(range(len(shot.trajectory)), min(100, len(shot.trajectory)))
start_time = time.time()

for i in random_indices:
Expand Down
Loading

0 comments on commit 3d4f616

Please sign in to comment.